Spring 5 Webclient Handle 500 error and modify the response - java

I have a Spring app acting as a passthrough from one app to another, making a http request and returning a result to the caller using WebClient. If the client http call returns a 500 internal server error I want to be able to catch this error and update the object that gets returned rather than re-throwing the error or blowing up the app.
This is my Client:
final TcpClient tcpClient = TcpClient.create()
.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, connectionTimeout)
.doOnConnected(connection -> connection.addHandlerLast(new ReadTimeoutHandler(readTimeout))
.addHandlerLast(new WriteTimeoutHandler(writeTimeout)));
this.webClient = WebClient.builder()
.clientConnector(new ReactorClientHttpConnector(HttpClient.from(tcpClient)))
.defaultHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
.filter(logRequest())
.filter(logResponse())
.filter(errorHandler())
.build();
And this is my error handler. I've commented where I want to modify the result, where ResponseDto is the custom object that is returned from the client call happy path
public static ExchangeFilterFunction errorHandler(){
return ExchangeFilterFunction.ofResponseProcessor(clientResponse -> {
ResponseDto resp = new ResponseDto();
if(nonNull(clientResponse.statusCode()) && clientResponse.statusCode().is5xxServerError()){
//this is where I want to modify the response
resp.setError("This is the error");
}
//not necessarily the correct return type here
return Mono.just(clientResponse);
});
}
How can I achieve this? I can't find any tutorials or any information in the docs to help explain it.
Disclaimer, I'm new to webflux. We're only starting to look at reactive programming

Related

Close a spring reactive WebClient call inside a request handler

I am attempting to proxy a streaming HTTP call
by placing a Flux emitting client in a streaming controller method as follows:
#GetMapping(value = "/api/v1/data/flux", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public Flux<String> streamDataFlux() {
WebClient cl = WebClient.builder()
.baseUrl("https://example.com")
.build();
return cl.post().uri("/api/events")
.accept(MediaType.TEXT_EVENT_STREAM)
.header(HttpHeaders.CONNECTION, "keep-alive")
.bodyValue("")
.retrieve()
.bodyToFlux(String.class);
}
How do I ensure the client is cancelled if and when the calling response handler is cancelled?
I.e. If I cURL my URL, (/api/v1/data/flux) then CTRL-C that request ... the WebClient remains connected seemingly permanently...
Thx

Mocking WebClient when sending request with mockito

I have an endpoint that makes a request to a server to check the status. I use WebClient with vertex. I create a client like this.
WebClient client = WebClient.create(vertx);
and the request looks like the following
client.get(check.url).send(ar -> {
HttpResponse<Buffer> resp = ar.result();
if (resp != null) {
check.response = resp.bodyAsString();
} else {
check.response = "";
}
logger.debug("Received response from app: {} with result:{} on url:{} : {}",
check.appName, check.result, check.url, check.response);
check.result.complete(ar.succeeded());
});
The problem i have is when the client tries to send the request.
EDIT: I want to mock that client.send() and return my WebClient with the status I want like 200 or 400. Please be advised that I can't access this WebClient as it's declared under the method I want to test.
I have this test right now that I'm trying to fix.
TEST
#Test
public void testWongUrlsAndNormalMode() throws Exception{
when(healthCheckEndpoint.configuration.getMode()).thenReturn(HealthCheckerConfiguration.HealthCheckMode.NORMAL);
when(healthCheckEndpoint.configuration.getHealthCheckUrls()).thenReturn(urls);
Response a = healthCheckEndpoint.checkApplications(); //here is when the client.get(url).send() returns nullPointerException
assertEquals(a.getStatus(), 500);
assertEquals(a.getEntity(), "normal mode no app urls");
}
but as mentioned i get a null pointer exception when the WebClient tries to send the request.
I have also tried to mock the WebClient but I can't mock the send() method as it is a void method

Spring webflux WebClient post a file to a client

I'm trying to figure out how to write a method to simply send a file from a webflux controller to a 'regular' controller.
I'm constantly getting a common error back, but nothing I've tried has resolved it.
The method I'm sending the file from:
#GetMapping("process")
public Flux<String> process() throws MalformedURLException {
final UrlResource resource = new UrlResource("file:/tmp/document.pdf");
MultiValueMap<String, UrlResource> data = new LinkedMultiValueMap<>();
data.add("file", resource);
return webClient.post()
.uri(LAMBDA_ENDPOINT)
.contentType(MediaType.MULTIPART_FORM_DATA)
.body(BodyInserters.fromMultipartData(data))
.exchange()
.flatMap(response -> response.bodyToMono(String.class))
.flux();
}
I'm consuming it in a AWS Lambda with the following endpoint:
#PostMapping(path = "/input", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
public ResponseEntity<List<?>> input(#RequestParam("file") MultipartFile file) throws IOException {
final ByteBuffer byteBuffer = ByteBuffer.wrap(file.getBytes());
//[..]
return new ResponseEntity<>(result, HttpStatus.OK);
}
But I'm constantly just getting:
{
"timestamp":1549395273838,
"status":400,
"error":"Bad Request",
"message":"Required request part 'file' is not present",
"path":"/detect-face"
}
back from the lambda;
Have I just setup the sending of the file incorrectly, Or do I need to configure something on the API Gateway to allow the request parameters in?
This was a interesting one for me. As I'm using a lambda function on the receiving end, and making use of aws-serverless-java-container-spring I actually had to declare the MultipartResolver manually.
The code in my question worked correctly once I added
#Bean
public MultipartResolver multipartResolver() {
return new CommonsMultipartResolver();
}
to my configuration.
Maybe someone will stumble on this and find it useful.

API call with Java + STS returning "Content type 'application/octet-stream' not supported"

I am working on part of an API, which requires making a call to another external API to retrieve data for one of its functions. The call was returning an HTTP 500 error, with description "Content type 'application/octet-stream' not supported." The call is expected to return a type of 'application/json."
I found that this is because the response received doesn't explicitly specify a content type in its header, even though its content is formatted as JSON, so my API defaulted to assuming it was an octet stream.
The problem is, I'm not sure how to adjust for this. How would I get my API to treat the data it receives from the other API as an application/json even if the other API doesn't specify a content type? Changing the other API to include a contenttype attribute in its response is infeasible.
Code:
The API class:
#RestController
#RequestMapping(path={Constants.API_DISPATCH_PROFILE_CONTEXT_PATH},produces = {MediaType.APPLICATION_JSON_VALUE})
public class GetProfileApi {
#Autowired
private GetProfile GetProfile;
#GetMapping(path = {"/{id}"})
public Mono<GetProfileResponse> getProfile(#Valid #PathVariable String id){
return GetProfile.getDispatchProfile(id);
}
The service calling the external API:
#Autowired
private RestClient restClient;
#Value("${dispatch.api.get_profile}")
private String getDispatchProfileUrl;
#Override
public Mono<GetProfileResponse> getDispatchProfile(String id) {
return Mono.just(id)
.flatMap(aLong -> {
MultiValueMap<String, String> headers = new HttpHeaders();
headers.add(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE);
return restClient.get(getDispatchProfileUrl, headers);
}).flatMap(clientResponse -> {
HttpStatus status = clientResponse.statusCode();
log.info("HTTP Status : {}", status.value());
return clientResponse.bodyToMono(GetProfileClientResponse.class);
// the code does not get past the above line before returning the error
}).map(GetProfileClientResponse -> {
log.debug("Response : {}",GetProfileClientResponse);
String id = GetProfileClientResponse.getId();
log.info("SubscriberResponse Code : {}",id);
return GetProfileResponse.builder()
// builder call to be completed later
.build();
});
}
The GET method for the RestClient:
public <T> Mono<ClientResponse> get(String baseURL, MultiValueMap<String,String> headers){
log.info("Executing REST GET method for URL : {}",baseURL);
WebClient client = WebClient.builder()
.baseUrl(baseURL)
.defaultHeaders(httpHeaders -> httpHeaders.addAll(headers))
.build();
return client.get()
.exchange();
}
One solution I had attempted was setting produces= {MediaType.APPLICATION_JSON_VALUE} in the #RequestMapping of the API to produces= {MediaType.APPLICATION_OCTET_STREAM_VALUE}, but this caused a different error, HTTP 406 Not Acceptable. I found that the server could not give the client the data in a representation that was requested, but I could not figure out how to correct it.
How would I be able to treat the response as JSON successfully even though it does not come with a content type?
Hopefully I have framed my question well enough, I've kinda been thrust into this and I'm still trying to figure out what's going on.
Are u using jackson library or jaxb library for marshalling/unmarshalling?
Try annotating Mono entity class with #XmlRootElement and see what happens.

Reactive WebClient GET Request with text/html response

Currently I’m having an issue with new Spring 5 WebClient and I need some help to sort it out.
The issue is:
I request some url that returns json response with content type text/html;charset=utf-8.
But unfortunately I’m still getting an exception:
org.springframework.web.reactive.function.UnsupportedMediaTypeException:
Content type 'text/html;charset=utf-8' not supported. So I can’t
convert response to DTO.
For request I use following code:
Flux<SomeDTO> response = WebClient.create("https://someUrl")
.get()
.uri("/someUri").accept(MediaType.APPLICATION_JSON)
.retrieve()
.bodyToFlux(SomeDTO.class);
response.subscribe(System.out::println);
Btw, it really doesn’t matter which type I point in accept header, always returning text/html. So how could I get my response converted eventually?
As mentioned in previous answer, you can use exchangeStrategies method,
example:
Flux<SomeDTO> response = WebClient.builder()
.baseUrl(url)
.exchangeStrategies(ExchangeStrategies.builder().codecs(this::acceptedCodecs).build())
.build()
.get()
.uri(builder.toUriString(), 1L)
.retrieve()
.bodyToFlux( // .. business logic
private void acceptedCodecs(ClientCodecConfigurer clientCodecConfigurer) {
clientCodecConfigurer.customCodecs().encoder(new Jackson2JsonEncoder(new ObjectMapper(), TEXT_HTML));
clientCodecConfigurer.customCodecs().decoder(new Jackson2JsonDecoder(new ObjectMapper(), TEXT_HTML));
}
If you need to set the maxInMemorySize along with text/html response use:
WebClient invoicesWebClient() {
return WebClient.builder()
.exchangeStrategies(ExchangeStrategies.builder().codecs(this::acceptedCodecs).build())
.build();
}
private void acceptedCodecs(ClientCodecConfigurer clientCodecConfigurer) {
clientCodecConfigurer.defaultCodecs().maxInMemorySize(BUFFER_SIZE_16MB);
clientCodecConfigurer.customCodecs().registerWithDefaultConfig(new Jackson2JsonDecoder(new ObjectMapper(), TEXT_HTML));
clientCodecConfigurer.customCodecs().registerWithDefaultConfig(new Jackson2JsonEncoder(new ObjectMapper(), TEXT_HTML));
}
Having a service send JSON with a "text/html" Content-Type is rather unusual.
There are two ways to deal with this:
configure the Jackson decoder to decode "text/html" content as well; look into the WebClient.builder().exchangeStrategies(ExchangeStrategies) setup method
change the "Content-Type" response header on the fly
Here's a proposal for the second solution:
WebClient client = WebClient.builder().filter((request, next) -> next.exchange(request)
.map(response -> {
MyClientHttpResponseDecorator decorated = new
MyClientHttpResponseDecorator(response);
return decorated;
})).build();
class MyClientHttpResponseDecorator extends ClientHttpResponseDecorator {
private final HttpHeaders httpHeaders;
public MyClientHttpResponseDecorator(ClientHttpResponse delegate) {
super(delegate);
this.httpHeaders = new HttpHeaders(this.getDelegate().getHeaders());
// mutate the content-type header when necessary
}
#Override
public HttpHeaders getHeaders() {
return this.httpHeaders;
}
}
Note that you should only use that client in that context (for this host).
I'd strongly suggest to try and fix that strange content-type returned by the server, if you can.

Categories