Spring WebClient Retry Logic with new Headers - java

I am trying to build a retry logic using Spring WebClient. The problem that I am trying to solve is very simple. I am calling an API endpoint to get some values. If the API returns an error with say 401 response, then I will have to make call to Token service and renew my token and use the new token and make the same API call.
The general psudo is
try {
GET /locations data
} catch(401 Unauthorized) {
POST /token and get renew Token --> This is another WebClient API call With New Token
call again GET /locations and return value
} catch (Another Exception) {
throw Application Error
}
Here is the Spring code that I am trying to do and it does not look like it is working.
Any suggestion on how to do it.
public List<Location> getLocations(final User user) {
if (null == user) {
throw new ApplicationException("User cannot be null");
}
if (null == user.getHoneyWellLinkToken()) {
throw new ApplicationException(String.format("%s has not linked the account with Honeywell", user.getUsername()));
}
List<Location> locations = getLocationsAPI(user).block();
return locations;
}
private Mono<List<Location>> getLocationsAPI(final User user) {
String endpoint = config.getApi().getLocationsEndpoint()
.concat("?apikey=")
.concat(config.getCredentials().getClientId());
return WebClient.builder().baseUrl(endpoint)
.build()
.get()
.headers(httpHeaders -> httpHeaders.setBearerAuth(user.getHoneyWellLinkToken().getAccessToken()))
.retrieve()
.bodyToFlux(Location.class)
.collectList()
.doOnError(err -> {
WebClient.builder().baseUrl(endpoint)
.build()
.get()
.headers(httpHeaders -> httpHeaders.setBearerAuth(honeywellService.renewToken(user).block().getHoneyWellLinkToken().getAccessToken()))
.retrieve().bodyToFlux(Location.class);
});
}
This code is hosted on GitHub https://github.com/reflexdemon/home-use/blob/main/src/main/java/io/vpv/homeuse/service/HoneywellThermostatService.java

Use onErrorResume instead of doOnError
Do not block when renewing token
private Mono<List<Location>> getLocationsAPI(final User user) {
String endpoint = config.getApi().getLocationsEndpoint()
.concat("?apikey=")
.concat(config.getCredentials().getClientId());
return getLocations(endpoint, user)
.onErrorResume(err -> honeywellService.renewToken(user)
.flatMap(newUser -> getLocations(endpoint, newUser)));
}
private Mono<List<Location>> getLocations(String endpoint, User user) {
return WebClient.builder()
.baseUrl(endpoint)
.build()
.get()
.headers(httpHeaders -> httpHeaders.setBearerAuth(user
.getHoneyWellLinkToken()
.getAccessToken()))
.retrieve()
.bodyToFlux(Location.class)
.collectList();
}
Also, it's a good idea to use a single WebClient instance instead of building a new one for each request.

Related

Throws java.lang.IllegalStateException: block()/blockFirst()/blockLast() when retrying invalid token

I am trying to update the token when I get a response with 401 status code.
In order to do that, I used web client. I know that this mainly used to do reactive development but since resttemplate will soon be deprecated I went for this option.
The issue I am facing is that when it does call the api endpoint to get the new token, it throws a 'java.lang.IllegalStateException: block()/blockFirst()/blockLast() '. And make sense as it stated in the exception message It is not supported in thread reactor-http-nio-3.
I saw that there is a map and flatmap option, but I couldn't figure out how to use it inside the doBeforeRetry() to make it process in a different stream.
I need to have that new token before retrying.
So the question is : How can I get the token via another call and then still do the retry ?
I was able to make it work by using a try catch but I would like to find the solution how to use it inside that retry method.
I also try to block the token request by replacing the token response by a Mono and block it by using myMono.toFuture().get() as stated here block()/blockFirst()/blockLast() are blocking error when calling bodyToMono AFTER exchange()
Here is the code :
Method responsible for the call :
public String getValueFromApi(HashMap<String, Object> filter) {
String response = "";
response = webclient
.post()
.uri(endpoint)
.header("token", token.getToken())
.bodyValue(filter)
.retrieve()
.bodyToMono(String.class)
.retryWhen(Retry.max(3).doBeforeRetry(
retrySignal -> tokenService.getTokenFromApi(env)
).filter(InvalidTokenException.class::isInstance))
.block();
return response;
}
Method that retrieve the token :
public void getTokenFromApi(Environment env) {
HashMap<String, String> requestBody = new HashMap<>();
requestBody.put("name", "name");
requestBody.put("password", "password");
String response = WebClient
.builder()
.baseUrl(BASE_PATH)
.defaultHeader(HttpHeaders.CONTENT_TYPE, "application/json")
.build()
.post()
.uri(tokenUri)
.body(BodyInserters.fromValue(requestBody))
.retrieve()
.bodyToMono(String.class)
.block();
getTokenFromResponse(response);
}
private void getTokenFromResponse(String reponse) {
JsonObject tokenObject = new Gson().fromJson(reponse, JsonObject.class);
setToken(tokenObject.get("token").getAsString());
}
WebClient Builder :
#Bean
public WebClient webClientForApi(WebClient.Builder webClientBuilder) {
return webClientBuilder
.clientConnector(new ReactorClientHttpConnector(httpClient))
.filter(errorHandler())
.filter(logRequest())
.clone()
.baseUrl(BASE_PATH)
.defaultHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
.defaultHeader(HttpHeaders.ACCEPT, "application/json")
.build();
}
public ExchangeFilterFunction errorHandler() {
return ExchangeFilterFunction.ofResponseProcessor(clientResponse -> {
if (clientResponse.statusCode().equals(HttpStatus.UNAUTHORIZED)) {
return Mono.error(InvalidTokenException::new);
} else if (clientResponse.statusCode() == HttpStatus.INTERNAL_SERVER_ERROR) {
return Mono.error(ApiInternalServerException::new);
} else {
return Mono.just(clientResponse);
}
});
}
private ExchangeFilterFunction logRequest() {
return ExchangeFilterFunction.ofRequestProcessor(clientRequest -> {
logger.info("Request: {} {}", clientRequest.method(), clientRequest.url());
clientRequest.headers().forEach((name, values) -> values.forEach(value -> logger.info("{}={}", name, value)));
return Mono.just(clientRequest);
});
}

Spring Boot web Client

I am having an issue with the web client.
I am trying to make a post request. The request is good. The thing is, if I add onStatus in order to handle http error codes, I am getting a NPE when calling bodyToMono. If I remove onStatus, I get the response.
we could take this as an example:
Employee createdEmployee = webClient.post()
.uri("/employees")
.header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
.body(Mono.just(empl), Employee.class)
.retrieve()
.onStatus(HttpStatus::isError, clientResponse -> {
if (clientResponse.statusCode() == HttpStatus.resolve(402)) {
return Mono.error(new Exception("402"));
}
log.error("Error endpoint with status code {}", clientResponse.statusCode());
if (clientResponse.statusCode() == HttpStatus.resolve(500)) {
return Mono.error(new Exception("500"));
}
if (clientResponse.statusCode() == HttpStatus.resolve(512)) {
return Mono.error(new Exception("512"));
}
return Mono.error(new Exception("Error while processing request"));
})
.bodyToMono(Employee.class);
I wan to handle 4xx and 5xx errors including their specific subtypes (404,402) 500, 512
Before bodyToMono() you can use onStatus()
.onStatus(httpStatus -> {
return httpStatus.is4xxClientError(); // handle 4xx or use .is5xxServerError() or .value()
}, clientResponse -> Mono.error(new RuntimeException("custom exception")))
Another option is to use ExchangeFilterFunction and apply it to your webclient
ExchangeFilterFunction responseErrorHandler =
ExchangeFilterFunction.ofResponseProcessor(clientResponse -> {
clientResponse.statusCode(); // handle any status manually
return Mono.just(clientResponse);
});
WebClient webClient = WebClient.builder()
.filter(responseErrorHandler)

Testing onErrorResume() Spring Webflux

I have a service layer using Spring Webflux and reactor and I am writing unit test for this. I was able to test the good response scenario but not sure how to test onErrorResume() using StepVerifier. Also please let me know if there is a better way of handling exceptions in my controller(e.g: using switchIfEmpty())
Here is my controller method
public Mono<SomeType> getInfo(Integer id) {
return webClient
.get()
.uri(uriBuilder -> uriBuilder.path())
.header("", "")
.header("", "")
.header("", "")
.header(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON_VALUE)
.retrieve()
.bodyToMono(POJO.class)
.onErrorResume(ex -> {
if (ex instanceof WebFaultException)
return Mono.error(ex);
return Mono.error(new WebFaultException(ex.getMessage(), "Error on API Call", HttpStatus.INTERNAL_SERVER_ERROR));
});
}
}
You can mock the webclient and use Mockito.doThrow when webclientMock.get() is called.
YourWebClient webclientMock = mock(YourWebClient.class);
doThrow(RuntimeException.class)
.when(webclientMock)
.get();
// Call your method here
Exception exception = assertThrows(RuntimeException.class, () -> {
YourController.getInfo(someIntValue);
});
// If you chose to raise WebFaultException, addittionaly assert that the return values ( message, status) are the one you expected
An alternate way to test your WebClient code without having to mock the WebClient class itself, as that can quickly become very messy, is to build your WebClient with an ExchangeFunction that returns whatever response or error you expect. I've found this to be a happy medium between mocking out the WebClient and spinning up Wiremock for unit tests.
#Test
void ourTest() {
ExchangeFunction exchangeFunction = mock(ExchangeFunction.class);
// this can be altered to return either happy or unhappy responses
given(exchangeFunction.exchange(any(ClientRequest.class))).willReturn(Mono.error(new RuntimeException()));
WebClient webClient = WebClient.builder()
.exchangeFunction(exchangeFunction)
.build();
// code under test - this should live in your service
Mono<String> mono = webClient.get()
.uri("http://someUrl.com")
.retrieve()
.bodyToMono(POJO.class)
.onErrorResume(ex -> {
if (ex instanceof WebFaultException)
return Mono.error(ex);
return Mono.error(new WebFaultException(ex.getMessage(), "Error on API Call", HttpStatus.INTERNAL_SERVER_ERROR));
});
StepVerifier.create(mono)
.expectError(RuntimeException.class)
.verify();
}

Refreshing access token when REST API returns 401 in Retrofit2 RxJava

I have the following endpoints in REST API:
public interface AutomoticzAPI {
#POST("/api/beacon_auth/login")
Single<LoginResponse> login(#Body LoginRequest request);
#GET("/api/system/ws_devices")
Single<WSDevicesResponse> wsDeviceList(#Header("Authorization") String tokenHeader);
}
When I call login endpoint, in response I recieve access token that I save into ClientSession holder object. Later I can retrieve token from ClientSession use to call server's protected resources:
api.login(ClientSession.getInstance().getLoginRequest(login, password))
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(loginResponse -> {
String accessToken = loginResponse.getAccessToken();
ClientSession.getInstance().setAccessToken(accessToken);
view.onLoginSuccess();
}, throwable -> {
RetrofitException exception = (RetrofitException) throwable;
if (exception.getKind().equals(RetrofitException.Kind.HTTP)){
view.onLoginFailed(exception.getMessage());
} else if(exception.getKind().equals(RetrofitException.Kind.NETWORK))
{
view.onLoginFailed("Network error...");
} else {
view.onLoginFailed("Unknown error occurred...");
}
});
When I'm calling wsDeviceList endpoint, server could return 401 HTTP response code and json body with error code and message:
{
"code": "EXPIRED-TOKEN",
"message": "Token expired"
}
If that happens I want to call login endpoint once again to get new access token. Here is my code so far:
ClientSession clientSession = ClientSession.getInstance();
String token = "Bearer "+clientSession.getAccessToken();
String url = ClientSession.getInstance().getUrl();
AutomoticzAPI api = NetworkManager.getApiClient(url);
api.wsDeviceList(token)
.retryWhen(throwableFlowable -> throwableFlowable.flatMap(
new Function<Throwable, Publisher<?>>() {
#Override
public Publisher<?> apply(Throwable throwable) throws Exception {
RetrofitException exception = (RetrofitException) throwable;
if (exception.isUnauthorizedError()){
return relogin(api, clientSession.getLoginRequest());
}
return (Publisher<?>) throwable;
}
}
))
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(wsDevicesResponse -> {
view.onDeviceListLoaded(wsDevicesResponse.getWsdevices());
}, throwable -> {
RetrofitException exception = (RetrofitException) throwable;
view.onError(exception);
});
}
public Publisher<?> relogin(AutomoticzAPI api, LoginRequest loginRequest){
return (Publisher<?>) api.login(loginRequest)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(loginResponse -> {
String accessToken = loginResponse.getAccessToken();
ClientSession.getInstance().setAccessToken(accessToken);
}, throwable -> {
RetrofitException exception = (RetrofitException) throwable;
view.onError(exception);
});
}
But when relogin method gets executed my program crashes.
I'm not proficient in RxJava and probably doing this wrong. How I can make recall login to refresh access token and then call wsDeviceList once again?
Use Authenticator API of retrofit and inside this call access token api to get access token and re try the fail API call using this access token.

Spring Webflux and Keycloak JWT rest api

I am building a userinfo endpoint on my Webflux rest api, how do I access the access_token passed in through the Authorization header in the rest call. Also need a similar endpoint to update the user.
All the examples I have found with latest spring 5/boot 2 are about securing a webapp.
#GetMapping("/api/user-info")
public Map userInfo(OAuth2AuthenticationToken authentication) {
OAuth2AuthorizedClient authorizedClient = this.getAuthorizedClient(authentication);
Map userAttributes = Collections.emptyMap();
String userInfoEndpointUri = authorizedClient
.getClientRegistration()
.getProviderDetails()
.getUserInfoEndpoint()
.getUri();
if (!StringUtils.isEmpty(userInfoEndpointUri)) {
// userInfoEndpointUri is optional for OIDC Clients
userAttributes = WebClient.builder()
.filter(oauth2Credentials(authorizedClient))
.build()
.get()
.uri(userInfoEndpointUri)
.retrieve()
.bodyToMono(Map.class)
.block();
}
return userAttributes;
}
private OAuth2AuthorizedClient getAuthorizedClient(OAuth2AuthenticationToken authentication) {
return this.authorizedClientService.loadAuthorizedClient(
authentication.getAuthorizedClientRegistrationId(), authentication.getName());
}
private ExchangeFilterFunction oauth2Credentials(OAuth2AuthorizedClient authorizedClient) {
return ExchangeFilterFunction.ofRequestProcessor(
clientRequest -> {
ClientRequest authorizedRequest = ClientRequest.from(clientRequest)
.header(HttpHeaders.AUTHORIZATION, "Bearer " + authorizedClient.getAccessToken().getTokenValue())
.build();
return Mono.just(authorizedRequest);
});
}
OAuth2AuthenticationToken object defined in the method is null which is understandable but not sure what else need configuring.
Thanks for your help.

Categories