handling error messages with spring-boot webflux - spring-boot

i'm writing a java adapter for posting something to a webService.
In the http service i use an ExceptionHandler:
#org.springframework.web.bind.annotation.ExceptionHandler
public ResponseEntity<String> handle(BadCommandException ex) {
return ResponseEntity.badRequest().body(ex.getMessage());
}
That works well, if i call it with curl/postman.
But how can i get the error message that is in the body with a reactive WebClient ?
Part of my client-lib:
private Mono<Optional<String>> post(String bearerToken, Model model, IRI outbox) {
return WebClient.builder()
.build()
.post()
.uri(outbox.stringValue())
.contentType(new MediaType("text","turtle"))
.body(BodyInserters.fromValue(modelToTurtleString(model)))
.header("Authorization", "Bearer " + bearerToken)
.header("profile", "https://www.w3.org/ns/activitystreams")
.accept(new MediaType("text", "turtle"))
.retrieve()
.toEntity(String.class)
.map(res->res.getHeaders().get("Location").stream().findFirst());
}
In success case, i want to return the string from the location header. But how can i for example throw an Exception if the http-status is 400 and add the error message from the body to the exception?
Thanks

the spring documentation on webclient retreive() has examples of how to handle errors. I suggest you start there.
Mono<Person> result = client.get()
.uri("/persons/{id}", id).accept(MediaType.APPLICATION_JSON)
.retrieve()
.onStatus(HttpStatus::is4xxClientError, response -> ...)
.onStatus(HttpStatus::is5xxServerError, response -> ...)
.bodyToMono(Person.class);

.onStatus(HttpStatus::is4xxClientError,
clientResponse -> { return clientResponse.createException();
Javadoc:
/**
* Create a {#link WebClientResponseException} that contains the response
* status, headers, body, and the originating request.
* #return a {#code Mono} with the created exception
* #since 5.2
*/
Mono<WebClientResponseException> createException();
My problem was my testcase 'test bad request'. Because of a refactoring it was sending an empty request body instead of a bad one and the annotation #RequestBody has an attribute 'required' which is default true. So the response body was always empty and did not contain my error message as expected ;-)
public Mono<ResponseEntity<Void>> doIt(Principal principal, #PathVariable String actorId, #RequestBody String model) {
...
that cost me hours ;-)

Related

Spring boot - WebFlux - WebTestClient - convert response to responseEntity

I have a Reactive controller which returns:
#ResponseBody
#GetMapping("/data")
public Mono<ResponseEntity<Data>> getData() {
//service call which returns Mono<Data> dataMono
Mono<ResponseEntity<Data>> responseEntityMono = dataMono
.map(data -> ResponseEntity.ok(data))
.doFinally(signalType -> {});
return responseEntityMono;
}
I am using WebTestClient to test this endpoint, but I want to extract the response entity for cucumber to validate further.
I tried this:
#Autowired private WebTestClient webTestClient;
public ResponseEntity get() {
EntityExchangeResult < ResponseEntity > response = webTestClient.get()
.uri(uriBuilder ->
uriBuilder
.path(VISUALIZATION_URL)
.build())
.header("Accepts", "application/json")
.exchange()
.expectStatus().isOk()
.expectHeader().contentType(MediaType.APPLICATION_JSON_VALUE)
.expectBody(ResponseEntity.class)
.returnResult();
return response.getResponseBody();
}
but I am getting an error. I can get the JSON by doing:
public String get() {
BodyContentSpec bodyContentSpec = webTestClient.get()
.uri(uriBuilder ->
uriBuilder
.path(URL)
.build())
.header("Accepts", "application/json")
.exchange()
.expectStatus().isOk()
.expectHeader().contentType(MediaType.APPLICATION_JSON_VALUE)
.expectBody();
return new String(bodyContentSpec.returnResult().getResponseBody());
}
But I am trying to see if I can get the whole ResponseEntity so that I can validate headers, caching headers, and body.
You will never be able to get a ResponseEntity in your test. WebTestClient.ResponseSpec returned from exchange() is the only way to check the answer from your Controller. ResponseEntity is just the object you return in your method but once Jackson serializes it, it is converted to a regular HTTP response (in your case with JSON in his body and regular HTTP headers).

Pass-through API / Preserve backend headers in Spring Webflux

I am building an application to call a back-end which responds with a mime-type response.
#Override
public Mono<String> getDocument() {
return webClient.get()
.uri(path)
.retrieve()
.bodyToMono(String.class);
}
From this request, I need to preserve the response headers and pass it through as the response. This is mostly because the response headers contain the dynamic content type of the file. I need to forward these headers (all as received) to the API response. For example :
Content-Type : application/pdf
Content-Disposition: attachment; filename="test.pdf"
Following is my handler.
public Mono<ServerResponse> getDocument(ServerRequest request) {
return ServerResponse
.ok()
.contentType(MediaType.APPLICATION_PDF)
.header("Content-Disposition", "attachment; filename=\"test.pdf\"")
.body(BodyInserters.fromPublisher(documentService.getDocument(), String.class));
}
The file is coming through from the API as an attachment as expected, but I do not want to hard code the content-type header. How can I achieve this?
Update with the handler code :
public Mono<ServerResponse> getDocument(ServerRequest request) {
return ServerResponse
.ok()
.contentType(MediaType.APPLICATION_JSON)
.body(BodyInserters.fromPublisher(documentService.getDocument(), String.class));
}
I was able to resolve the problem by returning a ResponseEntity from the service instead of the body and using that to construct the ServerResponse in the handler.
Service :
public Mono<ResponseEntity<String>> getDocument() {
return webClient.get()
.uri(path)
.retrieve()
.toEntity(String.class);
}
Handler :
public Mono<ServerResponse> getDocument(ServerRequest request) {
return documentService
.getDocument()
.flatMap(r -> ServerResponse
.ok()
.headers(httpHeaders -> httpHeaders.addAll(r.getHeaders()))
.body(BodyInserters.fromValue(r.getBody()))
);
}

concurrent calls using spring webflux and hystrix

hoping someone can steer me in the right direction in turning my code into a more reactive service call. for background I have a preceding function that will generate a list of users, will then use that list to call this getUserDetails function for each user in the list, and return a map or list of user + details.
#HystrixCommand(commandKey = "getUserDetails")
public getUserResponse getUserDetails(userRequest request) {
getUserResponse response = webClient.post()
.uri(uri)
.body(BodyInserters.fromObject(request))
.retrieve()
.onStatus(HttpStatus::isError, resp -> resp.bodyToMono(getUserError.class).map(errorHandler::mapRequestErrors))
.bodyToMono(getUserResponse.class).block();
return response;
}
Ideally I would also replace/remove the error mapping as only concerned with logging the returned error response and continuing.
so far I have thought something along the lines of this but I'm not sure the webflux/hystrix will play nice?
#HystrixCommand(commandKey = "getUserDetails", fallbackMethod = "getUserFallback")
public Mono<getUserResponse> getUserDetails(userRequest request) {
return = webClient.post()
.uri(uri)
.body(BodyInserters.fromObject(request))
.retrieve()
.bodyToMono(getUserResponse.class);
}
#HystrixCommand
public Mono<getUserResponse> getUserFallback(userRequest request, Throwable throwable) {
log.error(//contents of error message returned)
return mono.empty();
}
public Flux<UserMap> fetchUserDetails(List<Integer> userIds) {
return Flux.fromIterable(userIds)
.parallel()
.runOn(Schedulers.elastic())
.flatMap(userDetailsRepository::getUserDetails);
}
Hystrix is deprecated. If you have a chance, move to resilience4j which has support for Webflux/Reactor.
Spring also has dedicated support for resilience4j.
Regarding error handling you can leverage the rich set of operators from the Mono/Flux API like onErrorReturn or onErrorResume.

How to handle HTTP status code in Spring Webclient

I'm stuck trying to do simple error handling when calling a remote service. The service returns a Map. The behaviour I'm looking for is:
HTTP 200 --> Return body (Map<String, String>).
HTTP 500 --> Throw a particular exception
HTTP 404 --> Simply return Null.
Here's my code:
private Map<String, String> loadTranslations(String languageTag) {
try {
WebClient webClient = WebClient.create(serviceUrl);
Map<String, String> result = webClient.get()
.uri("/translations/{language}", languageTag)
.accept(MediaType.APPLICATION_JSON)
.retrieve()
.onStatus(httpStatus -> HttpStatus.NOT_FOUND.equals(httpStatus),
clientResponse -> Mono.error(new MyServiceException(HttpStatus.NOT_FOUND)))
.onStatus(HttpStatus::is5xxServerError, response -> Mono.error(new MyServiceException(response.statusCode())))
.bodyToMono(Map.class)
.block();
return result;
} catch (MyServiceException ex) { // doesn't work as in reality it throws ReactiveException
....
}
}
I don't know how to have the result of block() return NULL (or something that I can interpret as "404 was received"). The idea would be to just return NULL on 404 and throw an exception on 500.
I tried returning Mono.empty() but in that case the result variable contains the body of the response as Dictionary (I'm using standard Spring error bodies that contain timestamp, path, message).
What I'm doing wrong?
Thank you,

How to print responseBody with java reactor webclient before deserialization

We are planing to migrate from spring Rest-Template to Reactor-webclient.
With Rest-template we have written custom logging interceptors where we were printing request and response with uniqueId, before desrialization.
Now weblient provide filters, but in filters I can't access responseBody to log it.
We have some third party APIs where they send strings in case of error and some objects in case of success. In this case I can't wait to log response after deserialization, because it will break and we will not be able to log what response we got.
You can try creating a wrapper WebClient which will first log the response and then will deserialize.
The success response will fall on doOnSuccess and the error will fall on onErrorResume.
public <T> Mono<T> get(String url, Map<String, String> headersMap, Class<T> type) {
Mono<T> responseMono = client.get().uri(url).headers((h) -> headersMap.forEach(h::set)).retrieve()
.bodyToMono(type);
return responseMono.doOnSuccess(response -> log.debug("REST GET call to {} is successfull and response is {}",
url,response).onErrorResume(err -> {
log.error("Exception occurred calling get({}): {}", url,
err.getMessage());
return Mono.error(err);
});
}
Here is some sudo code for something I use to test with:
WebClient client = WebClient.create("www.google.com");
ObjectMapper mapper = new ObjectMapper();
client.get()
.retrieve()
.bodyToMono(String.class)
.map(rawBody -> {
try {
return mapper.readValue(rawBody, Response.class);
} catch (JsonProcessingException e) {
throw new RuntimeException("Cannot deserialize string: " + rawBody);
}
});

Resources