How to change response HTTP status of Spring WebClient - spring-boot

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

Related

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 WebClient throw error based on response body

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));
})
);

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);
});
}

Spring + Angular: How to parse ResponseEntity in angular?

I'm using Spring Boot to create an API that needs to be consumed in Angular 4. Spring and Angular are on different ports.
The problem is that Spring's ResponseEntity raises an error in Angular.
#RequestMapping(value = "/{id}", method = RequestMethod.GET)
public ResponseEntity getFlow(#PathVariable int id) {
Flow flow = flowService.findById(id);
return new ResponseEntity(flow, HttpStatus.FOUND);
}
Now, I can perfectly use Postman to test the API and it works.
But when I make a request from Angular, it returns an error:
Strangely, it returns an error alongside the requested object.
Now, the cause of the problem is that the Spring Boot application returns a ResponseEntity and not a normal object (like String), and Angular doesn't know how to interpret it. If the controller returns just a Flow object, it works.
How can it be solved using ResponseEntity? Or, how else can I send the object alongside the HTTP status code?
Also, in #RequestMapping put produces = "application/json", and in get request in angular, add http options :
const httpOptions = {
headers: new HttpHeaders({
'Accept': 'application/json',
'Content-Type': 'application/json'
})
};
So your get request looks like this:
this.http.get(url, httpOptions)
As per the document mentioned here
https://docs.angularjs.org/api/ng/service/$http
A response status code between 200 and 299 is considered a success status and will result in the success callback being called. Any response status code outside of that range is considered an error status and will result in the error callback being called. Also, status codes less than -1 are normalized to zero. -1 usually means the request was aborted, e.g. using a config.timeout. Note that if the response is a redirect, XMLHttpRequest will transparently follow it, meaning that the outcome (success or error) will be determined by the final response status code.
As you are sending an instance of ResponseEntity(HttpStatus.Found) whose Http status code is 302 which doesnt fall under the success range thats why error callback is called.
Try returning the content like this
return new ResponseEntity(flow, HttpStatus.OK);

Resources