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

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

Related

Can't handle bad request using doOnError WebFlux

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

I am trying to customize the Error format for Rest API with spring boot

Below is syntax of the code. It is throwing compilation error java: cannot infer type arguments for org.springframework.http.ResponseEntity<>
Can any one suggest how to fix this error please?
#ExceptionHandler(ValidationException.class)
public ResponseEntity<List<ErrorResponse>> process(ValidationException ex, WebRequest request) {
return new ResponseEntity<>(Arrays.asList(generateErrorResponse(ex, request)), HttpStatus.BAD_REQUEST);
}
private ErrorResponse generateErrorResponse(ValidationException ex, WebRequest request) {
ErrorResponse error = new ErrorResponse();
String message = ex.getMessage();
error.setTimestamp(LocalDateTime.now());
error.setError(ex.getMessage());
return new ResponseEntity<>(error, NOT_FOUND.value());
/* return new ErrorResponse(
ex.ge,
ex.getErrorCode(),
message,
request.getContextPath()
);*/
}
}
The error messages tells you not to use type arguments, to avoid them. Try
public ResponseEntity<ErrorResponse[]>
instead of
public ResponseEntity<List<ErrorResponse>>

Spring REST Exception - getting the response payload

I have the following:
The "400 Bad request" is converted to a ResourceAccessException in Spring.
Is there any way to retrieve the payload here? I want to send the "errorMessage" further up the call chain.
Code-wise the following is used to do the request:
public <T> T post(String url, Object request, Class<T> className) {
try {
return logEnhancedRestTemplate.postForObject(url, request, className);
} catch(RestClientException ex) {
throw handleErrors(ex, url);
}
}
It is in the "handleErrors" method I want to use the "errorMessage" from the body.
If you want to retrieve the message of an exception use the method getMessage().
In your specific example maybe is better if you catch a generic exception, since I suppose that every type of Runtime exception should call your method handleErrors(ex, url).
If this is the case, I suggest you to modify your code with:
public <T> T post(String url, Object request, Class<T> className) {
try {
return logEnhancedRestTemplate.postForObject(url, request, className);
} catch(Exception ex) {
throw handleErrors(ex, url);
}
}

Is it possible to apply dictionaries for Citrus static response adapter response template?

I'm using Citrus static response adapter to mock services, and I need to change values in its payload for every test case. Ideally I think about usage of dictionaries for each test case. There is sample of my current scenario:
#Autowired
#Qualifier("checkRegistrationEndpointAdapter")
public StaticResponseEndpointAdapter checkRegistrationEndpointAdapter;
protected void setAdapterResponse(StaticResponseEndpointAdapter adapter, String filenamepath){
URL url = this.getClass().getResource(filenamepath);
String payload = null;
try {
payload = Resources.toString(url, Charsets.UTF_8);
} catch (IOException e) {
e.printStackTrace();
}
adapter.setMessagePayload(payload);
}
#CitrusTest
public void TestCase02() throws IOException {
http()
.client(CLIENT)
.post()
.payload(new ClassPathResource("templates/thisStartRequestMsg.xml", getClass()))
.dictionary("TC02");
http()
.client(CLIENT)
.response()
.messageType("xml")
.payload(new ClassPathResource("templates/thisStartResponseMsg.xml", getClass()));
action(new AbstractTestAction() {
#Override
public void doExecute(TestContext testContext) {
setAdapterResponse(checkRegistrationEndpointAdapter, "templates/check-registration-v1CheckRegistrationResponseMsg.xml");
}
});
http()
.client(CLIENT)
.response()
.messageType("xml")
.payload(new ClassPathResource("templates/check-registration-v1CheckRegistrationRequestMsg.xml", getClass()))
.dictionary("TC02");
}
How can I apply dictionary to the payload set in my setAdapterResponse method?
Note: this question relates to Can I use Citrus variable in Citrus static response adapter payload?
Static response adapter has currently no support for data dictionaries. I wonder why you put so much effort into static response adapters? Why not using the full Citrus http server power with receiving the request and providing a response inside the test case?

.Net HttpClient not able to deserialize exceptions

I'm creating an "SDK/Proxy" class using HttpClient and would like to pass exceptions thrown by the service the SDK is consuming to the application that is using this SDK. The original exception needs to be preserved unaltered and not wrapped. I've tried EnsureSuccessStatusCode() but it is no good because the original exception is lost to the consumer.
Below is SDK code that is attempting to catch exceptions from base service and pass it on to the consumer;
public async Task<string> GetValue(string effect)
{
using (var client = GetHttpClient())
{
HttpResponseMessage resp = await client.GetAsync("api/values?effect=" + effect).ConfigureAwait(false);
if (resp.IsSuccessStatusCode)
{
return await resp.Content.ReadAsStringAsync();
}
throw await resp.Content.ReadAsAsync<Exception>();
}
}
The "consumer" service is using the SDK like so;
public async Task<string> Get(string effect)
{
return await baseSvc.GetValue(effect);
}
When testing this I am getting the following response;
{
Message: "An error has occurred."
ExceptionMessage: "Member 'ClassName' was not found."
ExceptionType: "System.Runtime.Serialization.SerializationException"...
the base service code throwing the exception is this;
public async Task<string> Get(string effect)
{
switch (effect.ToLower())
{
case "string":
return "this is a string";
case "exception":
throw new ApplicationException("this is an application exception.");
default:
return "effect requested does not exist.";
}
}
Is it possible to "flow" the original unaltered exception from the consumed service through the SDK to the consumer?
You can use following.
string responseBody = response.Content.ReadAsStringAync().Result;
throw new HttpException((int)response.StatusCode, responseBody);
I have referred this - web API and MVC exception handling