Can't handle bad request using doOnError WebFlux - rest

I wanna send some DTO object to server. Server have "Valid" annotation, and when server getting not valid DTO, he should send validation errors and something like "HttpStatus.BAD_REQUEST", but when I'm trying to send HttpStatus.BAD_REQUEST doOnError just ignore it.
POST-request from client
BookDTO bookDTO = BookDTO
.builder()
.author(authorTf.getText())
.title(titleTf.getText())
.publishDate(LocalDate.parse(publishDateDp.getValue().toString()))
.owner(userAuthRepository.getUser().getLogin())
.fileData(file.readAllBytes())
.build();
webClient.post()
.uri(bookAdd)
.contentType(MediaType.APPLICATION_JSON)
.bodyValue(bookDTO)
.retrieve()
.bodyToMono(Void.class)
.doOnError(exception -> log.error("Error on server - [{}]", exception.getMessage()))
.onErrorResume(WebClientResponseException.class, throwable -> {
if (throwable.getStatusCode() == HttpStatus.BAD_REQUEST) {
log.error("BAD_REQUEST!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!"); --My log doesn't contain this error, but server still has errors from bindingResult
return Mono.empty();
}
return Mono.error(throwable);
})
.block();
Server-part
#PostMapping(value = "/add", consumes = {MediaType.APPLICATION_JSON_VALUE})
public HttpStatus savingBook(#RequestBody #Valid BookDTO bookDTO, BindingResult bindingResult) {
List<FieldError> errors = bindingResult.getFieldErrors();
if (bindingResult.hasErrors()) {
for (FieldError error : errors ) {
log.info("Client post uncorrected data [{}]", error.getDefaultMessage());
}
return HttpStatus.BAD_REQUEST;
}else{libraryService.addingBookToDB(bookDTO);}
return null;
}

doOnError is a so-called side effect operation that could be used for instrumentation before onError signal is propagated downstream. (e.g. to log error).
To handle errors you could use onErrorResume. The example, the following code handles the WebClientResponseException and returns Mono.empty instead.
...
.retrieve()
.doOnError(ex -> log.error("Error on server: {}", ex.getMessage()))
.onErrorResume(WebClientResponseException.class, ex -> {
if (ex.getStatusCode() == HttpStatus.BAD_REQUEST) {
return Mono.empty();
}
return Mono.error(ex);
})
...
As an alternative as #Toerktumlare mentioned in his comment, in case you want to handle http status, you could use onStatus method of the WebClient
...
.retrieve()
.onStatus(HttpStatus.BAD_REQUEST::equals, res -> Mono.empty())
...
Update
While working with block it's important to understand how reactive signals will be transformed.
onNext(T) -> T in case of Mono and List<T> for Flux
onError -> exception
onComplete -> null, in case flow completes without onNext
Here is a full example using WireMock for tests
class WebClientErrorHandlingTest {
private WireMockServer wireMockServer;
#BeforeEach
void init() {
wireMockServer = new WireMockServer(wireMockConfig().dynamicPort());
wireMockServer.start();
WireMock.configureFor(wireMockServer.port());
}
#Test
void test() {
stubFor(post("/test")
.willReturn(aResponse()
.withHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
.withStatus(400)
)
);
WebClient webClient = WebClient.create("http://localhost:" + wireMockServer.port());
Mono<Void> request = webClient.post()
.uri("/test")
.retrieve()
.bodyToMono(Void.class)
.doOnError(e -> log.error("Error on server - [{}]", e.getMessage()))
.onErrorResume(WebClientResponseException.class, e -> {
if (e.getStatusCode() == HttpStatus.BAD_REQUEST) {
log.info("Ignoring error: {}", e.getMessage());
return Mono.empty();
}
return Mono.error(e);
});
Void response = request.block();
assertNull(response);
}
}
The response is null because we had just complete signal Mono.empty() that was transformed to null by applying block

Related

How to check Webclient reposebody?

i developed external API by WebClient but i don't know how to check the response body..
public class Call {
public Mono<Object> get() {
Mono<Object> http = webClient.get()
.uri(EXTERNAL_URL)
.retrieve()
.bodyToMono(Object.class);
return http;
}
}
and test code
public class Test {
#Test
void test() {
Call call = new Call();
Mono<Object> mono = call.get();
mono.doOnSuccess(
r -> log.info(">>> r = {}", r) //
).subscribe() }
}
log content
>>> r = MonoMap
it just print "MonoMap".. how can i check response body??
Change your code as follows, it will deserialize the response to a string and return
public Mono<String> get() {
Mono<Object> http = webClient.get()
.uri(EXTERNAL_URL)
.retrieve()
.bodyToMono(String.class);
return String;
}

How to return either Uni<Object> or Uni<Void>?

#Route(...)
public Uni<?> call() {
return Uni.createFrom().voidItem();
}
throws a NullPointerException: Invalid value returned by Uni: null
However
#Route(...)
public Uni<Void> call() {
return Uni.createFrom().voidItem();
}
works perfectly fine and responds with HTTP 204
How do I manage to get either Uni<Void> or Uni<AnyObject> from the same method?
I need to return http 204 only in specific scenarios
You can't do that directly as the types are different.
I would recommend using RESTEasy Reactive and do:
#GET
public Uni<Response> call() {
Uni<AnyObject> uni = .... ;
return uni
.onItem().transform(res -> {
if (res == null) return Response.noContent().build();
return Response.ok(res).build();
});
}
By emitting a Response object, you can customize the response status.
Another solution if you want to keep using Reactive Routes is to not return a Uni, but get the RoutingContext as a parameter:
#Route(...)
public void call(RoutingContext rc) {
HttpServerResponse response = rc.response();
Uni<AnyObject> uni = .... ;
return uni
.subscribe().with(res -> {
if (res == null) response.setStatus(204).end();
else response.setStatus(200).end(res);
}, failure -> rc.fail(failure)); // will produce a 500.
}

How Spring Cloud Stream reactive processing works?

How to achieve reactive message processing in Spring Cloud Stream? I read about Spring Cloud Function and that I should use them for reactive processing so I created sample one:
#Bean
public Consumer<Flux<Message<Loan>>> loanProcess() {
return loanMessages ->
loanMessages
.flatMap(loanMessage -> Mono.fromCallable(() -> {
if (loanMessage.getPayload().getStatus() == null) {
log.error("Empty status");
throw new RuntimeException("Loan status is empty");
}
return "Good";
}))
.doOnError(throwable -> log.error("Exception occurred: {}", throwable))
.subscribe(status -> log.info("Message processed correctly: {}", status));
}
Afterwards I started to thinking what is the difference between the above function and the class with #StreamListener and usage of Reactor types:
#StreamListener(Sink.INPUT)
public void loanReceived(Message<Loan> message) {
Mono.just(message)
.flatMap(loanMessage -> Mono.fromCallable(() -> {
if (loanMessage.getPayload().getStatus() == null) {
log.error("Empty status");
throw new RuntimeException("Loan status is empty");
}
log.info("Correct message");
return "Correct message received";
}))
.doOnError(throwable -> log.error("Exception occurred: {}", throwable.getClass()))
.subscribe(status -> log.info("Message processed correctly: {}", status));
}
Additionally, in Spring Webflux I understand that there are few threads from netty which handle requests processing (running in event loop). However, I cannot find a documentation how thread model works in Spring Cloud Stream.

No handlers for address while using eventBus in communicating between verticles of a springboot project

I developed a project with Springboot and used Vertx as an asynchronous reactive toolkit. My ServerVerticle, create a httpServer which receives http requests from an Angular app and sends messages to it via eventBus. By the way, the time that received message arrives, ServerVerticle sends it to another verticle which has service instance in it (for connecting to repository). i tested it with postman and get "No handlers for address" error as a bad request.
here is my ServerVerticle:
HttpServerResponse res = routingContext.response();
res.setChunked(true);
EventBus eventBus = vertx.eventBus();
eventBus.request(InstrumentsServiceVerticle.FETCH_INSTRUMENTS_ADDRESS, "", result -> {
if (result.succeeded()) {
res.setStatusCode(200).write((Buffer) result.result().body()).end();
} else {
res.setStatusCode(400).write(result.cause().toString()).end();
}
});
My instrumentVerticle is as follows:
static final String FETCH_INSTRUMENTS_ADDRESS = "fetch.instruments.service";
// Reuse the Vert.x Mapper :)
private final ObjectMapper mapper = Json.mapper;
private final InstrumentService instrumentService;
public InstrumentsServiceVerticle(InstrumentService instrumentService) {
this.instrumentService = instrumentService;
}
private Handler<Message<String>> fetchInstrumentsHandler() {
return msg -> vertx.<String>executeBlocking(future -> {
try {
future.complete(mapper.writeValueAsString(instrumentService.getInstruments()));
} catch (JsonProcessingException e) {
logger.error("Failed to serialize result "+ InstrumentsServiceVerticle.class.getName());
future.fail(e);
}
},
result -> {
if (result.succeeded()) {
msg.reply(result.result());
} else {
msg.reply(result.cause().toString());
}
});
}
#Override
public void start() throws Exception {
super.start();
vertx.eventBus().<String>consumer(FETCH_INSTRUMENTS_ADDRESS).handler(fetchInstrumentsHandler());
}
and i deployed both verticles in the springbootApp starter.

How to throw an exception in on error part of reactive Spring WebClient call?

I would like the following method to throw a custom exception if an error occurs:
#Service
public class MyClass {
private final WebClient webClient;
public MatcherClient(#Value("${my.url}") final String myUrl) {
this.webClient = WebClient.create(myUrl);
}
public void sendAsync(String request) {
Mono<MyCustomResponse> result = webClient.post()
.header(HttpHeaders.CONTENT_TYPE, "application/json")
.body(BodyInserters.fromObject(request))
.retrieve()
.doOnError(throwable -> throw new CustomException(throwable.getMessage()))
.subscribe(response -> log.info(response));
}
}
I have also set up a unit test expecting the CustomException to be thrown. Unfortunately the test fails and the Exception is kind of wrapped into a Mono object. Here also the test code for reference:
#Test(expected = CustomException.class)
public void testSendAsyncRethrowingException() {
MockResponse mockResponse = new MockResponse()
.setHeader(HttpHeaders.CONTENT_TYPE, "application/json")
.setResponseCode(500).setBody("Server error");
mockWebServer.enqueue(mockResponse);
matcherService.matchAsync(track);
}
I'm using the MockWebServer to mock an error in the test.
So, how should I implement the doOnError or onError part if the call in order to make my method really to throw an exception?
I'd advise to expose a reactive API that returns the Mono<Void> from the webclient, especially if you name your method "sendAsync". It's not async if you have to block for the call to return/fail. If you want to provide a sendSync() alternative, you can always make it call sendAsync().block().
For the conversion of exception, you can use the dedicated onErrorMap operator.
For the test, the thing is, you can't 100% test asynchronous code with purely imperative and synchronous constructs (like JUnit's Test(expected=?) annotation). (although some reactive operator don't induce parallelism so this kind of test can sometimes work).
You can also use .block() here (testing is one of the rare occurrences where this is unlikely to be problematic).
But if I were you I'd get in the habit of using StepVerifier from reactor-test. To give an example that sums up my recommendations:
#Service
public class MyClass {
private final WebClient webClient;
public MatcherClient(#Value("${my.url}") final String myUrl) {
this.webClient = WebClient.create(myUrl);
}
public Mono<MyCustomResponse> sendAsync(String request) {
return webClient.post()
.header(HttpHeaders.CONTENT_TYPE, "application/json")
.body(BodyInserters.fromObject(request))
.retrieve()
.onErrorMap(throwable -> new CustomException(throwable.getMessage()))
//if you really need to hardcode that logging
//(can also be done by users who decide to subscribe or further add operators)
.doOnNext(response -> log.info(response));
}
}
and the test:
#Test(expected = CustomException.class)
public void testSendAsyncRethrowingException() {
MockResponse mockResponse = new MockResponse()
.setHeader(HttpHeaders.CONTENT_TYPE, "application/json")
.setResponseCode(500).setBody("Server error");
mockWebServer.enqueue(mockResponse);
//Monos are generally lazy, so the code below doesn't trigger any HTTP request yet
Mono<MyCustomResponse> underTest = matcherService.matchAsync(track);
StepVerifier.create(underTest)
.expectErrorSatisfies(t -> assertThat(t).isInstanceOf(CustomException.class)
.hasMessage(throwable.getMessage())
)
.verify(); //this triggers the Mono, compares the
//signals to the expectations/assertions and wait for mono's completion
}
The retrieve() method in WebClient throws a WebClientResponseException
whenever a response with status code 4xx or 5xx is received.
1. You can customize the exception using the onStatus() method
public Mono<JSONObject> listGithubRepositories() {
return webClient.get()
.uri(URL)
.retrieve()
.onStatus(HttpStatus::is4xxClientError, clientResponse ->
Mono.error(new MyCustomClientException())
)
.onStatus(HttpStatus::is5xxServerError, clientResponse ->
Mono.error(new MyCustomServerException())
)
.bodyToMono(JSONObject.class);
}
2. Throw the custom exception by checking the response status
Mono<Object> result = webClient.get().uri(URL).exchange().log().flatMap(entity -> {
HttpStatus statusCode = entity.statusCode();
if (statusCode.is4xxClientError() || statusCode.is5xxServerError())
{
return Mono.error(new Exception(statusCode.toString()));
}
return Mono.just(entity);
}).flatMap(clientResponse -> clientResponse.bodyToMono(JSONObject.class))
Reference: https://www.callicoder.com/spring-5-reactive-webclient-webtestclient-examples/
Instead of using doOnError I swiched to subscribe method accepting also an error consumer:
Mono<MyCustomResponse> result = webClient.post()
.header(HttpHeaders.CONTENT_TYPE, "application/json")
.body(BodyInserters.fromObject(request))
.retrieve()
.subscribe(response -> log.info(response),
throwable -> throw new CustomException(throwable.getMessage()));
This documentation helps a lot: https://projectreactor.io/docs/core/release/reference/index.html#_error_handling_operators