Spring WebClient throw error based on response body - spring

I am using Spring WebClient to call REST API. I want to throw an error based on the response. For example, if there is an error (400) with body
{"error": "error message1 "}
then I want to throw an error with "error message1". Same way if there is an error(400) with the body
{"error_code": "100020"}
then I want to throw an error with error_cde 100020. I want to do it in a non-blocking way.
public Mono<Response> webclient1(...) {
webClient.post().uri(createUserUri).header(CONTENT_TYPE, APPLICATION_JSON)
.body(Mono.just(request), Request.class).retrieve()
.onStatus(HttpStatus::isError, clientResponse -> {
//Error Handling
}).bodyToMono(Response.class);
}

A body from ClientResponse should be extracted in a reactive way (javadoc) and lambda in onStatus method should return another Mono (javadoc). To sum up, take a look at below example
onStatus(HttpStatus::isError, response -> response
.bodyToMono(Map.class)
.flatMap(body -> {
var message = body.toString(); // here you should probably use some JSON mapper
return Mono.error(new Exception(message));
})
);

Related

How to change response HTTP status of Spring WebClient

I have got a situation that is needed to return HTTP 2XX when WebClient returns any kind of 4XX.
My existing code below,
public Mono<ResponseEntity<>String> postMethodA(String valueA) {
return webClient
.put()
.uri'/')
.bodyValue(valueA)
.retrieve()
.toEntity(String.class);
}
I added onStatus method like this.
public Mono<ResponseEntity<>String> postMethodA(String valueA) {
return webClient
.put()
.uri'/')
.bodyValue(valueA)
.retrieve()
.onStatus(HttpStatus::is4XXClientError response -> Mono.empty())
.toEntity(String.class);
}
If I added
onStatus(HttpStatus::is4XXClientError response -> Mono.empty())
still, it is not gonna work because it is not able to return 2XX.
Is there a way to change the Http status when returning the response? and can you please show some example?
To ignore an error response completely, and propagate neither response
nor error, use a filter, or add onErrorResume downstream, for example:
webClient.get()
.uri("https://someUrl.com/account/123")
.retrieve()
.bodyToMono(Account.class)
.onErrorResume(WebClientResponseException.class,
ex -> ex.getRawStatusCode() == 404 ? Mono.empty() : Mono.error(ex));
Reference: JavaDoc

Getting: org.springframework.core.io.buffer.DataBufferLimitException: Exceeded limit on max bytes to buffer : 262144

I'm getting the DataBufferLimitException on receipt of a response to a HTTP request. I am using spring-boot-starter-parent:2.5.0, spring-cloud.version:2020.0.2.
I have tried practically all of the options described here(DataBufferLimitException: Exceeded limit on max bytes to buffer webflux error) and here(configure spring.codec.max-in-memory-size When using ReactiveElasticsearchClient), with no success. Is there anything else I can try? Here is my code to create the webclient:
private WebClient buildWebClient(long custId) {
return WebClient.builder()
.clientConnector(createWebClientWithTimeout())
// Add base url to all requests (callers only need to add the path and query params)
.baseUrl(baseUrl)
// Filter to add bearer token to auth header before sending request
.filter((request, next) -> getToken(custId).map(setAuthHeader(request)).flatMap(next::exchange))
// Filter to send the request, and try again if it has an auth error
.filter((request, next) -> next.exchange(request).flatMap(clientResponse -> {
if (clientResponse.statusCode() == HttpStatus.UNAUTHORIZED) {
logger.error("Received 401 Unauthorized from linkedin for request: {} {} with X-LI-UUID header: {}", request.method(), request.url(),
clientResponse.headers().header(LINKEDIN_HEADER));
// Retry once if auth failed
return clientResponse.bodyToMono(String.class)
.doOnNext(err -> logger.warn("Received 401 from linkedin; retrying once. Error body: {}", err))
.then(refreshToken(custId).map(setAuthHeader(request)).flatMap(next::exchange));
} else if (clientResponse.statusCode().isError()) {
logger.error("Received error status code: {} from linkedin for request: {} {} with X-LI-UUID header: {}", clientResponse.statusCode(), request.method(),
request.url(), clientResponse.headers().header(LINKEDIN_HEADER));
} else {
logger.debug("Received status code: {} from linkedin for request: {} {}", clientResponse.statusCode(), request.method(), request.url());
}
// If not a 401, just return the response
return Mono.just(clientResponse);
})).build();
}
Adding spring.codec.max-in-memory-size=16MB to the properties does not work, explicitly setting the value using ExchangeStrategies does not work, implementing WebFluxConfigurer does not work.
This code was working fine with spring-boot-starter-parent:2.1.6.RELEASE.
Any suggestions as to what I can try next?
Spring Boot preconfigures the WebClienter.Builder for you, which makes settings like spring.codec.max-in-memory-size work after all.
To make use of this preconfigured WebClient.Builder, you need to have it injected into your service, which does not look like what you are doing in the above example. You seem to use the WebClient.builder() method straight.
This code together with the application property spring.codec.max-in-memory-size could work:
#Service
public class Foo(WebClient.Builder injectedPreConfiguredBuilder) {
private WebClient buildWebClient(long custId) {
return injectedPreConfiguredBuilder
.clientConnector(createWebClientWithTimeout())
// Add base url to all requests (callers only need to add the path and query params)
.baseUrl(baseUrl)
// Filter to add bearer token to auth header before sending request
.filter((request, next) -> getToken(custId).map(setAuthHeader(request)).flatMap(next::exchange))
// Filter to send the request, and try again if it has an auth error
.filter((request, next) -> next.exchange(request).flatMap(clientResponse -> {
if (clientResponse.statusCode() == HttpStatus.UNAUTHORIZED) {
logger.error("Received 401 Unauthorized from linkedin for request: {} {} with X-LI-UUID header: {}", request.method(), request.url(),
clientResponse.headers().header(LINKEDIN_HEADER));
// Retry once if auth failed
return clientResponse.bodyToMono(String.class)
.doOnNext(err -> logger.warn("Received 401 from linkedin; retrying once. Error body: {}", err))
.then(refreshToken(custId).map(setAuthHeader(request)).flatMap(next::exchange));
} else if (clientResponse.statusCode().isError()) {
logger.error("Received error status code: {} from linkedin for request: {} {} with X-LI-UUID header: {}", clientResponse.statusCode(), request.method(),
request.url(), clientResponse.headers().header(LINKEDIN_HEADER));
} else {
logger.debug("Received status code: {} from linkedin for request: {} {}", clientResponse.statusCode(), request.method(), request.url());
}
// If not a 401, just return the response
return Mono.just(clientResponse);
})).build();
}
}
Also, you need to keep in mind that you need to use an injected / autowired WebClient.Builder in your tests for the property to work!
If you want to roll your own WebClient.Builder, it's possible to set the buffer size programmatically.
kotlin example:
val webClient = WebClient.builder()
.exchangeStrategies(
ExchangeStrategies.builder().codecs {
it.defaultCodecs().maxInMemorySize(1000000) // in bytes
}.build()
).build()
Java example:
WebClient.builder()
.exchangeStrategies(ExchangeStrategies.builder().codecs(
clientCodecConfigurer ->
clientCodecConfigurer.defaultCodecs().maxInMemorySize(1000000))
.build())
.baseUrl("https://stackoverflow.com/posts/68986553/")
.build();

Spring WebFlux and WebClient change response on error

I have some controller method like
#PostMapping("/*")
fun proxy(#RequestBody body: String): Mono<ByteArray> {
return roundRobinBean.getNext()
.post()
.uri("/api")
.body(BodyInserters.fromObject(body))
.retrieve()
.bodyToMono<ByteArray>()
.doOnSuccess{
threadPool.submit(PutToCacheJob(body, it, cacheBean))
}
.doOnError{
logger.error(it.message, it)
}
}
roundRobinBean return WebClient for some host. If i get connection timeout exception or get 500 response i need call another host or return data from cache. Have mono some handler for changing inner data?
You can use onErrorResume operator which lets you define a fallback in case of errors.

Fetch Request Body from org.springframework.web.reactive.function.BodyInserter

I have the below code where I am able to log the headers and URL. But the body() method is returning an object of type BodyInserter. In the debug mode(STS), we can see the request body object. Is there any way to log the request?
[private ExchangeFilterFunction logRequest() {
return ExchangeFilterFunction.ofRequestProcessor(clientRequest -> { loggingService.info(clientRequest.url());
loggingService.info(clientRequest.headers());
BodyInserter<?, ? super ClientHttpRequest> bodyInserters= clientRequest.body();
return Mono.just(clientRequest);
});
}

Enable authenticator manually

Currently my client authenticates request only on case of 401 response:
this.client.authenticator(new okhttp3.Authenticator() {
public Request authenticate(Route route, Response response) throws IOException {
String credentials = authenticator.getCredentials();
if (credentials.equals(response.request().header("Authorization"))) {
throw new TraversonException(401, "Unauthorized", response.request().url().toString());
} else {
defaultHeader("Authorization", credentials);
Request.Builder newRequest = response.request().newBuilder()
.headers(Headers.of(defaultHeaders));
return newRequest.build();
}
});
But I'd like to change this behavior and be able to call it either manually or auto per first call? Is it possible somehow?
If the authentication is predictably required and not related to a proxy, then the solution is to implement an Interceptor instead of Authenticator.
OkHttpClient.Builder clientBuilder = ...;
clientBuilder.networkInterceptors().add(0, myInterceptor);
client = clientBuilder.build();
Example Interceptor https://github.com/yschimke/oksocial/blob/48e0ca53b85e608443eab614829cb0361c79aa47/src/main/java/com/baulsupp/oksocial/uber/UberAuthInterceptor.java
n.b. There is discussion around possible support for this usecase in https://github.com/square/okhttp/pull/2458. One issue with current Authenticator API is that it assumes a Response from the failed (401) request.

Resources