Spring Webflux : Webclient : Get body on error - java

I am using the webclient from spring webflux, like this :
WebClient.create()
.post()
.uri(url)
.syncBody(body)
.accept(MediaType.APPLICATION_JSON)
.headers(headers)
.exchange()
.flatMap(clientResponse -> clientResponse.bodyToMono(tClass));
It is working well.
I now want to handle the error from the webservice I am calling (Ex 500 internal error). Normally i would add an doOnError on the "stream" and isu the Throwable to test the status code,
But my issue is that I want to get the body provided by the webservice because it is providing me a message that i would like to use.
I am looking to do the flatMap whatever happen and test myself the status code to deserialize or not the body.

I prefer to use the methods provided by the ClientResponse to handle http errors and throw exceptions:
WebClient.create()
.post()
.uri( url )
.body( bodyObject == null ? null : BodyInserters.fromValue( bodyObject ) )
.accept( MediaType.APPLICATION_JSON )
.headers( headers )
.exchange()
.flatMap( clientResponse -> {
//Error handling
if ( clientResponse.statusCode().isError() ) { // or clientResponse.statusCode().value() >= 400
return clientResponse.createException().flatMap( Mono::error );
}
return clientResponse.bodyToMono( clazz )
} )
//You can do your checks: doOnError (..), onErrorReturn (..) ...
...
In fact, it's the same logic used in the DefaultResponseSpec of DefaultWebClient to handle errors. The DefaultResponseSpec is an implementation of ResponseSpec that we would have if we made a retrieve() instead of exchange().

Don't we have onStatus()?
public Mono<Void> cancel(SomeDTO requestDto) {
return webClient.post().uri(SOME_URL)
.body(fromObject(requestDto))
.header("API_KEY", properties.getApiKey())
.retrieve()
.onStatus(HttpStatus::isError, response -> {
logTraceResponse(log, response);
return Mono.error(new IllegalStateException(
String.format("Failed! %s", requestDto.getCartId())
));
})
.bodyToMono(Void.class)
.timeout(timeout);
}
And:
public static void logTraceResponse(Logger log, ClientResponse response) {
if (log.isTraceEnabled()) {
log.trace("Response status: {}", response.statusCode());
log.trace("Response headers: {}", response.headers().asHttpHeaders());
response.bodyToMono(String.class)
.publishOn(Schedulers.elastic())
.subscribe(body -> log.trace("Response body: {}", body));
}
}

I got the error body by doing like this:
webClient
...
.retrieve()
.onStatus(HttpStatus::isError, response -> response.bodyToMono(String.class) // error body as String or other class
.flatMap(error -> Mono.error(new RuntimeException(error)))) // throw a functional exception
.bodyToMono(MyResponseType.class)
.block();

You could also do this
return webClient.getWebClient()
.post()
.uri("/api/Card")
.body(BodyInserters.fromObject(cardObject))
.exchange()
.flatMap(clientResponse -> {
if (clientResponse.statusCode().is5xxServerError()) {
clientResponse.body((clientHttpResponse, context) -> {
return clientHttpResponse.getBody();
});
return clientResponse.bodyToMono(String.class);
}
else
return clientResponse.bodyToMono(String.class);
});
Read this article for more examples link, I found it to be helpful when I experienced a similar problem with error handling

I do something like this:
Mono<ClientResponse> responseMono = requestSpec.exchange()
.doOnNext(response -> {
HttpStatus httpStatus = response.statusCode();
if (httpStatus.is4xxClientError() || httpStatus.is5xxServerError()) {
throw new WebClientException(
"ClientResponse has erroneous status code: " + httpStatus.value() +
" " + httpStatus.getReasonPhrase());
}
});
and then:
responseMono.subscribe(v -> { }, ex -> processError(ex));

Note that as of writing this, 5xx errors no longer result in an exception from the underlying Netty layer. See https://github.com/spring-projects/spring-framework/commit/b0ab84657b712aac59951420f4e9d696c3d84ba2

I had just faced the similar situation and I found out webClient does not throw any exception even it is getting 4xx/5xx responses. In my case, I use webclient to first make a call to get the response and if it is returning 2xx response then I extract the data from the response and use it for making the second call. If the first call is getting non-2xx response then throw an exception. Because it is not throwing exception so when the first call failed and the second is still be carried on. So what I did is
return webClient.post().uri("URI")
.header(HttpHeaders.CONTENT_TYPE, "XXXX")
.header(HttpHeaders.ACCEPT, "XXXX")
.header(HttpHeaders.AUTHORIZATION, "XXXX")
.body(BodyInserters.fromObject(BODY))
.exchange()
.doOnSuccess(response -> {
HttpStatus statusCode = response.statusCode();
if (statusCode.is4xxClientError()) {
throw new Exception(statusCode.toString());
}
if (statusCode.is5xxServerError()) {
throw new Exception(statusCode.toString());
}
)
.flatMap(response -> response.bodyToMono(ANY.class))
.map(response -> response.getSomething())
.flatMap(something -> callsSecondEndpoint(something));
}

Using what I learned this fantastic SO answer regarding the "Correct way of throwing exceptions with Reactor", I was able to put this answer together. It uses .onStatus, .bodyToMono, and .handle to map the error response body to an exception.
// create a chicken
webClient
.post()
.uri(urlService.getUrl(customer) + "/chickens")
.contentType(MediaType.APPLICATION_JSON)
.body(Mono.just(chickenCreateDto), ChickenCreateDto.class) // outbound request body
.retrieve()
.onStatus(HttpStatus::isError, clientResponse ->
clientResponse.bodyToMono(ChickenCreateErrorDto.class)
.handle((error, sink) ->
sink.error(new ChickenException(error))
)
)
.bodyToMono(ChickenResponse.class)
.subscribe(
this::recordSuccessfulCreationOfChicken, // accepts ChickenResponse
this::recordUnsuccessfulCreationOfChicken // accepts throwable (ChickenException)
);

We have finally understood what is happening :
By default the Netty's httpclient (HttpClientRequest) is configured to fail on server error (response 5XX) and not on client error (4XX), this is why it was always emitting an exception.
What we have done is extend AbstractClientHttpRequest and ClientHttpConnector to configure the httpclient behave the way the want and when we are invoking the WebClient we use our custom ClientHttpConnector :
WebClient.builder().clientConnector(new CommonsReactorClientHttpConnector()).build();

The retrieve() method in WebClient throws a WebClientResponseException
whenever a response with status code 4xx or 5xx is received.
You can handle the exception by checking the response status code.
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/

I stumbled across this so figured I might as well post my code.
What I did was create a global handler that takes career of request and response errors coming out of the web client. This is in Kotlin but can be easily converted to Java, of course. This extends the default behavior so you can be sure to get all of the automatic configuration on top of your customer handling.
As you can see this doesn't really do anything custom, it just translates the web client errors into relevant responses. For response errors the code and response body are simply passed through to the client. For request errors currently it just handles connection troubles because that's all I care about (at the moment), but as you can see it can be easily extended.
#Configuration
class WebExceptionConfig(private val serverProperties: ServerProperties) {
#Bean
#Order(-2)
fun errorWebExceptionHandler(
errorAttributes: ErrorAttributes,
resourceProperties: ResourceProperties,
webProperties: WebProperties,
viewResolvers: ObjectProvider<ViewResolver>,
serverCodecConfigurer: ServerCodecConfigurer,
applicationContext: ApplicationContext
): ErrorWebExceptionHandler? {
val exceptionHandler = CustomErrorWebExceptionHandler(
errorAttributes,
(if (resourceProperties.hasBeenCustomized()) resourceProperties else webProperties.resources) as WebProperties.Resources,
serverProperties.error,
applicationContext
)
exceptionHandler.setViewResolvers(viewResolvers.orderedStream().collect(Collectors.toList()))
exceptionHandler.setMessageWriters(serverCodecConfigurer.writers)
exceptionHandler.setMessageReaders(serverCodecConfigurer.readers)
return exceptionHandler
}
}
class CustomErrorWebExceptionHandler(
errorAttributes: ErrorAttributes,
resources: WebProperties.Resources,
errorProperties: ErrorProperties,
applicationContext: ApplicationContext
) : DefaultErrorWebExceptionHandler(errorAttributes, resources, errorProperties, applicationContext) {
override fun handle(exchange: ServerWebExchange, throwable: Throwable): Mono<Void> =
when (throwable) {
is WebClientRequestException -> handleWebClientRequestException(exchange, throwable)
is WebClientResponseException -> handleWebClientResponseException(exchange, throwable)
else -> super.handle(exchange, throwable)
}
private fun handleWebClientResponseException(exchange: ServerWebExchange, throwable: WebClientResponseException): Mono<Void> {
exchange.response.headers.add("Content-Type", "application/json")
exchange.response.statusCode = throwable.statusCode
val responseBodyBuffer = exchange
.response
.bufferFactory()
.wrap(throwable.responseBodyAsByteArray)
return exchange.response.writeWith(Mono.just(responseBodyBuffer))
}
private fun handleWebClientRequestException(exchange: ServerWebExchange, throwable: WebClientRequestException): Mono<Void> {
if (throwable.rootCause is ConnectException) {
exchange.response.headers.add("Content-Type", "application/json")
exchange.response.statusCode = HttpStatus.BAD_GATEWAY
val responseBodyBuffer = exchange
.response
.bufferFactory()
.wrap(ObjectMapper().writeValueAsBytes(customErrorWebException(exchange, HttpStatus.BAD_GATEWAY, throwable.message)))
return exchange.response.writeWith(Mono.just(responseBodyBuffer))
} else {
return super.handle(exchange, throwable)
}
}
private fun customErrorWebException(exchange: ServerWebExchange, status: HttpStatus, message: Any?) =
CustomErrorWebException(
Instant.now().toString(),
exchange.request.path.value(),
status.value(),
status.reasonPhrase,
message,
exchange.request.id
)
}
data class CustomErrorWebException(
val timestamp: String,
val path: String,
val status: Int,
val error: String,
val message: Any?,
val requestId: String,
)

Actually, you can log the body easily in the onError call:
.doOnError {
logger.warn { body(it) }
}
and:
private fun body(it: Throwable) =
if (it is WebClientResponseException) {
", body: ${it.responseBodyAsString}"
} else {
""
}

For those that wish to the details of a WebClient request that triggered a 500 Internal System error, override the DefaultErrorWebExceptionHandler like as follows.
The Spring default is to tell you the client had an error, but it does not provide the body of the WebClient call, which can be invaluable in debugging.
/**
* Extends the DefaultErrorWebExceptionHandler to log the response body from a failed WebClient
* response that results in a 500 Internal Server error.
*/
#Component
#Order(-2)
public class ExtendedErrorWebExceptionHandler extends DefaultErrorWebExceptionHandler {
private static final Log logger = HttpLogging.forLogName(ExtendedErrorWebExceptionHandler.class);
public FsErrorWebExceptionHandler(
ErrorAttributes errorAttributes,
Resources resources,
ServerProperties serverProperties,
ApplicationContext applicationContext,
ServerCodecConfigurer serverCodecConfigurer) {
super(errorAttributes, resources, serverProperties.getError(), applicationContext);
super.setMessageWriters(serverCodecConfigurer.getWriters());
super.setMessageReaders(serverCodecConfigurer.getReaders());
}
/**
* Override the default error log behavior to provide details for WebClientResponseException. This
* is so that administrators can better debug WebClient errors.
*
* #param request The request to the foundation service
* #param response The response to the foundation service
* #param throwable The error that occurred during processing the request
*/
#Override
protected void logError(ServerRequest request, ServerResponse response, Throwable throwable) {
// When the throwable is a WebClientResponseException, also log the body
if (HttpStatus.resolve(response.rawStatusCode()) != null
&& response.statusCode().equals(HttpStatus.INTERNAL_SERVER_ERROR)
&& throwable instanceof WebClientResponseException) {
logger.error(
LogMessage.of(
() ->
String.format(
"%s 500 Server Error for %s\n%s",
request.exchange().getLogPrefix(),
formatRequest(request),
formatResponseError((WebClientResponseException) throwable))),
throwable);
} else {
super.logError(request, response, throwable);
}
}
private String formatRequest(ServerRequest request) {
String rawQuery = request.uri().getRawQuery();
String query = StringUtils.hasText(rawQuery) ? "?" + rawQuery : "";
return "HTTP " + request.methodName() + " \"" + request.path() + query + "\"";
}
private String formatResponseError(WebClientResponseException exception) {
return String.format(
"%-15s %s\n%-15s %s\n%-15s %d\n%-15s %s\n%-15s '%s'",
" Message:",
exception.getMessage(),
" Status:",
exception.getStatusText(),
" Status Code:",
exception.getRawStatusCode(),
" Headers:",
exception.getHeaders(),
" Body:",
exception.getResponseBodyAsString());
}
}

You have to cast the "Throwable e" parameter to WebClientResponseException, then you can call getResponseBodyAsString() :
WebClient webClient = WebClient.create("https://httpstat.us/404");
Mono<Object> monoObject = webClient.get().retrieve().bodyToMono(Object.class);
monoObject.doOnError(e -> {
if( e instanceof WebClientResponseException ){
System.out.println(
"ResponseBody = " +
((WebClientResponseException) e).getResponseBodyAsString()
);
}
}).subscribe();
// Display : ResponseBody = 404 Not Found

Related

Spring webclient onErrorReturn for specific type of exception

I wanted to return a fallback value from webclient after retries for only specific type of exception. I tried the below code but it doesnt return the fallback value.
public Mono<String> getEmployee(String request) {
return WebClient.create()
.get()
.uri("/accounts/" + request)
.exchangeToMono(clientResponse -> {
if (clientResponse.statusCode().value() == HttpStatus.INTERNAL_SERVER_ERROR.value()) {
throw new IllegalStateException();
}
if (clientResponse.statusCode().value() == HttpStatus.OK.value()) {
return clientResponse
.bodyToMono(String.class);
}
return Mono.error(new IllegalCallerException());
})
.retryWhen(Retry.max(3).filter(throwable -> throwable instanceof IllegalStateException))
.onErrorReturn(IllegalStateException.class, "TECHNICAL_ERROR")
.log();
}
As you see above, when Http status is 500, i wanted to retry only for IllegalStateException for 3 times and return fallback value when exception type is IllegalStateException. I used onErrorReturn with fallback value and specific error to match. But above code doesn't work, it retries for 3 times but fallback value is not returned.
In cases like that the best option for finding an issue is checking the instance of errors in debugger.
After all of retry tries are exhausted RetryExhaustedException will be throwed
Simple example for visualization:
Mono.error(new IllegalStateException("example"))
.retryWhen(Retry.max(-1))
.doOnError(throwable -> {
assert Objects.equals(throwable.getClass().getSimpleName(), "RetryExhaustedException");
})
.subscribe();
Summary:
Your retry mechanism works but after there are exhausted it is not IllegalStateExcpetion
If you want to handle logic on IllegalStateException later you can use onErtryExhaustedThrow method like:
.retryWhen(Retry.max(3)
.filter(throwable -> throwable instanceof IllegalStateException)
.onRetryExhaustedThrow((retrySpec, retrySignal) -> new IllegalStateException()))
You should do the following. I have similar code in our service and it is working as expected.
public Mono<String> getEmployee(String request) {
return WebClient.create()
.get()
.uri("/accounts/" + request)
.retrieve()
.onStatus(HttpStatus::is5xxServerError, res -> {
return Mono.error(new IllegalStateException());
})
.bodyToMono(String.class)
.retryWhen(
Retry.max(3)
.filter(IllegalStateException.class::isInstance)
.onRetryExhaustedThrow((retrySpec, retrySignal) -> new MaxRetryAchievedException("Max retried"))
).onErrorReturn(MaxRetryAchievedException.class, "Techinal Error")
.log();
}
public class MaxRetryAchievedException extends RuntimeException {
public MaxRetryAchievedException(String message) {
super(message);
}
}

Retry only when 5xx error is encountered Spring boot WebClient

I have followed few suggestions around to make a retry attempt only when I get 5xx error and straight away throw exception otherwise. But the code example I got doesn't seems to be working. It's not retrying for any of the exceptions (be it 4xx or 5xx).
Here is my code
public Mono<String> call(String input) {
log.info("Sending echo call for: {}", input);
return webClient.get()
.uri(format("/echo/%s", input))
.accept(MediaType.APPLICATION_JSON).acceptCharset(StandardCharsets.UTF_8)
.retrieve()
.onStatus(HttpStatus::is5xxServerError,
response -> Mono.error(new ServiceException("Server error", response.rawStatusCode())))
.bodyToMono(String.class)
.timeout(ofMillis(60_000L))
.retryWhen(
backoff(3L, ofSeconds(4L))
.jitter(0d)
.filter(ServiceException.class::isInstance)
.doAfterRetry(retrySignal -> log.info("Retry call: " + retrySignal))
.onRetryExhaustedThrow((retryBackoffSpec, retrySignal) -> retrySignal.failure())
)
.doOnError(throwable -> log.error("Call failure: {}", throwable.getMessage()))
.doOnSuccess(response -> log.info("Success: {}", response));
}
Anyone can help me figure out what's wrong with the code or can suggest an alternative approach. Thanks a lot!
Please try this, it should work.
final WebClient webClient = WebClient.builder().build();
final Mono<String> body = webClient
.get()
.uri("URL")
.retrieve()
.bodyToMono(String.class)
.retryWhen(Retry
.backoff(3, Duration.ofSeconds(2))
.filter(this::is5xxServerError)
);
System.out.println(body.block());
private boolean is5xxServerError(Throwable throwable) {
return throwable instanceof WebClientResponseException &&
((WebClientResponseException) throwable).getStatusCode().is5xxServerError();
}

"IllegalStateException: Only one connection receive subscriber allowed" when response body is empty

I have a Spring Boot 2.3.1 project, in which I use WebClient to call a remote service.
The remote service is not very reliable and tends to return 500 errors, with and without response bodies. My goal is throw a custom exception that contains the response body (or a default message) so that I can log it, and here's my code :
webClient.get()
.uri(targetServiceUri)
.retrieve()
.onStatus(HttpStatus::is5xxServerError, clientResponse ->
clientResponse.bodyToMono(String.class)
.flatMap(error ->
Mono.error(new MyCustomServiceException(error))
)
)
.toEntity(String.class)
.block();
I have 2 tests using wiremock, the first one works :
#Test
void shouldThrowCustomExceptionWhenServiceReturns500ServerErrorWithNoBody() {
setStubForInValidCheckCall(HttpStatus.INTERNAL_SERVER_ERROR,"{'Error':'invalid request'}");
Throwable thrown =
catchThrowable(() -> myClient.performComplianceCheck(getCompany()));
assertThat(thrown)
.isInstanceOf(MyCustomServiceException.class)
.hasMessageContaining("{'Error':'invalid request'}");
}
private void setStubForInValidCheckCall(HttpStatus httpStatus, String body) {
var response= aResponse().withStatus(httpStatus.value());
if(body!=null){
response=response.withBody(body);
}
stubFor(
get(urlPathMatching("/targetCompliance"))
.willReturn(response));
}
However, the second test in which the response is 500 but there's no body (or if it's an empty string), fails with "java.lang.IllegalStateException: Only one connection receive subscriber allowed.
#Test
void shouldThrowCustomExceptionWhenServiceReturns500ServerErrorWithNoBody() {
setStubForInValidCheckCall(HttpStatus.INTERNAL_SERVER_ERROR,null);
Throwable thrown =
catchThrowable(() -> myClient.performComplianceCheck(getCompany()));
assertThat(thrown)
.isInstanceOf(MyCustomServiceException.class)
.hasMessageContaining("service returned status 500");
}
I am struggling to understand why this happens, and how to fix it..
is it "normal" ? or am I missing something obvious (is it a problem with my test ?) ?
I have found a workaround, but it doesn't feel "webFlux-y" at all, and I still don't understand why the Only one connection receive subscriber allowed was happening :
try {
ResponseEntity<String> responseEntity =
webClient.get()
.uri(targetServiceUri)
.retrieve()
.toEntity(String.class)
.block();
}
catch (WebClientException e) {
if(e instanceof InternalServerError){
var internalServerError=(InternalServerError) e;
if(internalServerError.getStatusCode().is5xxServerError()){
var respBody=internalServerError.getResponseBodyAsString();
if(StringUtils.isEmpty(respBody)){
respBody=MY_STANDARD_MESSAGE +internalServerError.getRawStatusCode() ;
}
throw new MyCustomServiceException(respBody);
}
}
}

Getting the response body in error case with Spring WebClient

I am trying to replace the existing client code with RestTemplate with a WebClient. For that reason, most of the calls need to be blocking, so that the main portion of the application does not need to change. When it comes to error handling this poses a bit of a problem. There are several cases that have to be covered:
In a successful case the response contains a JSON object of type A
In an error case (HTTP status 4xx or 5xx) the response may contain a JSON object of type B
On certain requests with response status 404 I need to return an empty List matching the type of a successful response
In order to produce the correct error (Exception) the error response needs to be considered. So far I am unable to get my hands on the error body.
I am using this RestController method to produce the error response:
#GetMapping("/error/404")
#ResponseStatus(HttpStatus.NOT_FOUND)
public ResponseEntity error404() {
return ResponseEntity
.status(HttpStatus.NOT_FOUND)
.body(new ErrorResponse());
}
With this response object:
public class ErrorResponse {
private String message = "Error message";
public String getMessage() {
return message;
}
}
The WebClient is defined as follows:
WebClient.builder()
.baseUrl("http://localhost:8081")
.clientConnector(connector)
.defaultHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
.build();
With the connector being of type CloseableHttpAsyncClient (Apache Http client5).
From my test application I make the call like this:
public String get(int httpStatus) {
try {
return webClient.get()
.uri("/error/" + httpStatus)
.retrieve()
.onStatus(HttpStatus::isError, clientResponse -> {
clientResponse.bodyToMono(String.class).flatMap(responseBody -> {
log.error("Body from within flatMap within onStatus: {}", responseBody);
return Mono.just(responseBody);
});
return Mono.error(new RuntimeException("Resolved!"));
})
.bodyToMono(String.class)
.flatMap(clientResponse -> {
log.warn("Body from within flatMap: {}", clientResponse);
return Mono.just(clientResponse);
})
.block();
} catch (Exception ex) {
log.error("Caught Error: ", ex);
return ex.getMessage();
}
}
What I get is the RuntimeException from the onStatus return and of course the caught exception in the end.
I am missing the processing from the bodyToMono from within the onStatus. My suspicion is that this is not executed due to the blocking nature, as the response body is dealt with the bodyToMono after the onStatus.
When commenting out the onStatus I would expect that we log the warning in the flatMap, which does not happen either.
In the end I would like to define the handling of errors as a filter so that the code does not need to be repeated on every call, but I need to get the error response body, so that the exception can be populated with the correct data.
How can I retrieve the error response in a synchronous WebClient call?
This question is similar to Spring Webflux : Webclient : Get body on error, which has no accepted answer and some of the suggested approaches use methods that are no deprecated.
Here is one approach to handle error responses:
use onStatus to capture error http status
deserialize error response clientResponse.bodyToMono(ErrorResponse.class)
generate new error signal based on the error response Mono.error(new RuntimeException(error.getMessage())). Example uses RuntimeException but I would suggest to use custom exception to simplify error handling downstream.
webClient.get()
.uri("/error/" + httpStatus)
.retrieve()
.onStatus(HttpStatus::isError, clientResponse ->
clientResponse.bodyToMono(ErrorResponse.class)
.flatMap(error ->
Mono.error(new RuntimeException(error.getMessage()))
)
)
.bodyToMono(Response.class)
You don't really need try-catch. If you block the above code would return Response in case of the non-error response and throws exception with custom message in case of error response.
Update
Here is a full test using WireMock
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)
.withBody("{\"message\":\"Request error\",\"errorCode\":\"10000\"}")
)
);
WebClient webClient = WebClient.create("http://localhost:" + wireMockServer.port());
Mono<Response> request = webClient.post()
.uri("/test")
.retrieve()
.onStatus(HttpStatus::isError, clientResponse ->
clientResponse.bodyToMono(ErrorResponse.class)
.flatMap(error ->
Mono.error(new RuntimeException(error.getMessage() + ": " + error.getErrorCode()))
)
)
.bodyToMono(Response.class);
RuntimeException ex = assertThrows(RuntimeException.class, () -> request.block());
assertEquals("Request error: 10000", ex.getMessage());
}
#Data
private static class ErrorResponse {
private String message;
private int errorCode;
}
#Data
private static class Response {
private String result;
}
}

Spring Gatewayfilter (AUTH) How to Catch Error 500 and return the response from Auth-Service

Is there a way to catch the 500 Error and return the auth/userservice response instead of throwing?
eg. "Token invalid" / "Token expired" - the userservice already provides the correct response
#Override
public GatewayFilter apply(Config config) {
return (exchange, chain) -> {
String authHeader = exchange.getRequest().getHeaders().getFirst("Authorization");
String[] parts = authHeader.split(" ");
return webClientBuilder.build()
.post()
.uri("http://USER-SERVICE/user/validateToken?token=" + parts[1])
.retrieve()
.bodyToMono(UserDto.class)
.map(userDto -> {
exchange.getRequest()
.mutate()
.header("X-auth-user-id", String.valueOf(userDto.getId()));
return exchange;
}).flatMap(chain::filter);
};
}
On successful auth, it works like a charm. But i want to get the response from my userservice if anything goes wrong.
I havent worked with webclient before... its driving me crazy.
Edit: I also tried to get the response before to test it, unsuccessful. I used a Dto which should reflect the Error Response from my userservice.
var test = webClientBuilder.build()
.post()
.uri("http://USER-SERVICE/user/validateToken?token=" + parts[1])
.retrieve()
.bodyToMono(ApiExceptionDto.class);
var y = test.subscribe(p -> {
System.out.println(p.getMessage());
});
I just get a
reactor.core.Exceptions$ErrorCallbackNotImplemented: org.springframework.web.reactive.function.client.WebClientResponseException$NotFound: 404 Not Found from POST http://192.168.0.69:9001/user/validateToken?token=xx
Caused by: org.springframework.web.reactive.function.client.WebClientResponseException$NotFound: 404 Not Found from POST http://192.168.0.69:9001/user/validateToken?token=xx
at org.springframework.web.reactive.function.client.WebClientResponseException.create(WebClientResponseException.java:202) ~[spring-webflux-5.3.9.jar:5.3.9]
Suppressed: reactor.core.publisher.FluxOnAssembly$OnAssemblyException:
Error has been observed at the following site(s):
|_ checkpoint ⇢ 404 from POST http://USER-SERVICE/user/validateToken?token=xx [DefaultWebClient]
The Expected Response: (Postman)
POST : http://192.168.0.69:9001/user/validateToken?token=xx
{
"message": "The token was expected to have 3 parts, but got 1.",
"status": "NOT_FOUND",
"time": "Wed, 18 Aug 2021 01:05:21 GMT",
"path": "/user/validateToken"
}
I'll found a better solution, which will be more accurate:
#Override
public GatewayFilter apply(Config config) {
return (exchange, chain) -> {
String authHeader = exchange.getRequest().getHeaders().getFirst("Authorization");
String[] parts = authHeader.split(" ");
return webClientBuilder.build()
.post()
.uri("http://USER-SERVICE/user/validateToken?token=" + parts[1])
.retrieve()
.bodyToMono(UserDto.class)
.onErrorResume(e -> Mono.just(new RuntimeException())
.map(userDto -> {
exchange.getRequest()
.mutate()
.header("X-auth-user-id", String.valueOf(userDto.getId()));
return exchange;
}).flatMap(chain::filter);
};
}
It's after bodyToMono() and before map(). In onErrorResume() you will throw a new exception, if your downstream service will report any error (code >= 300). If you subclass RuntimeException and annotate it with HttpStatus.UNAUTHORIZED your client will receive the correct status and can react to it.
Guess i did it.
If someone has improvements to offer i would appreciate any input.
return webClientBuilder.build()
.post()
.uri("http://USER-SERVICE/user/validateToken?token=" + parts[1])
.exchangeToMono(clientResponse -> {
if (clientResponse.statusCode().isError()) {
return clientResponse.bodyToMono(AuthResponseDto.class);
}
return clientResponse.bodyToMono(UserDto.class);
}).flatMap(dto -> {
if (dto.getClass() != UserDto.class) {
AuthResponseDto authResponseDto = (AuthResponseDto) dto;
// returns the response to from userservice
return writeResponse(exchange.getResponse(), ApiExceptionDto.builder()
.message(authResponseDto.getMessage())
.status(authResponseDto.getStatus())
.time(Instant.now())
.path(authResponseDto.getPath())
.build()
.getJsonAsBytes());
}
return chain.filter(exchange);
});

Categories

Resources