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();
}
Related
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);
});
}
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)
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.
I'm trying to make WebClient return an Optional.empty() when I get a 404, not found, from the server. But instead I get a Optional with a User object with all properties set to null.
What am I missing?
#Override
public Optional<User> getUser(Username username) {
return webClient
.get()
.uri(buildUrl(username))
.retrieve()
.onStatus(HttpStatus.NOT_FOUND::equals, response -> Mono.empty())
.onStatus(HttpStatus::is4xxClientError, response -> createError(response, CLIENTERROR))
.onStatus(HttpStatus::is5xxServerError, response -> createError(response, SERVRERROR))
.bodyToMono(User.class)
.blockOptional();
}
You can make use of onError* functions from Mono to handle these cases.
onErrorResume to create a empty/error Mono on exception and onErrorMap to transform exception to a different exception type.
For example:
#Override
public Optional<User> getUser(Username username) {
return webClient
.get()
.uri(buildUrl(username))
.retrieve()
.onStatus(httpStatus -> httpStatus.is4xxClientError() && httpStatus != HttpStatus.NOT_FOUND, response -> createError(response, CLIENTERROR))
.onStatus(HttpStatus::is5xxServerError, response -> createError(response, SERVRERROR))
.bodyToMono(User.class)
.onErrorResume(WebClientResponseException.NotFound.class, notFound -> Mono.empty())
.blockOptional();
}
Have a look at the sample code from WebClient Javadoc (javadoc). It does exactly that using Mono's onErrorResume Method:
webClient.get()
.uri("https://example.com/account/123")
.retrieve()
.bodyToMono(Account.class)
.onErrorResume(WebClientResponseException.class,
ex -> ex.getRawStatusCode() == 404 ? Mono.empty() : Mono.error(ex));
I have a post request which I want it to map the response to different objects for 2 api who call it. The request always return BookResponse.
Here I map the response to return the book name:
public Mono<String> getBookName(BookRequest bookRequest) {
return client
.post()
.uri("PATH")
.contentType(MediaType.APPLICATION_JSON)
.accept(MediaType.APPLICATION_JSON_UTF8)
.body(bookRequest))
.retrieve()
.bodyToMono(BookResponse.class)
.doOnNext(this::validateResponseStatus)
.map(BookResponse::getBookName)
.doOnError(throwable -> logError(throwable));
}
and here same call just map the response to other Object which contains the book author + the request object:
public Mono<BookObject> getBookName(BookRequest bookRequest) {
return client
.post()
.uri("PATH")
.contentType(MediaType.APPLICATION_JSON)
.accept(MediaType.APPLICATION_JSON_UTF8)
.body(requestBody))
.retrieve()
.bodyToMono(BookResponse.class)
.doOnNext(this::validateResponseStatus)
.map(bookResponse -> new BookObject(bookResponse.getName(), bookRequest)
.doOnError(throwable -> logError(throwable));
Is there a way to do it without copy the code?
Thanks
I would extract the common code into a separate method:
public Something getSomething() {
client
.post()
.uri("PATH")
.contentType(MediaType.APPLICATION_JSON)
.accept(MediaType.APPLICATION_JSON_UTF8)
.body(requestBody))
.retrieve()
.bodyToMono(BookResponse.class)
.doOnNext(this::validateResponseStatus)
}
Then, in your methods you simply need to do something like:
Something something = getSomething();
return something.map(bookResponse -> new BookObject(bookResponse.getName(), bookRequest).doOnError(throwable -> logError(throwable));
or
Something something = getSomething();
return something.map(BookResponse::getBookName).doOnError(throwable -> logError(throwable));