In one of the apps my team manages, there's a GraphQL orchestration layer that calls a downstream service.
We use Spring's webclient for it.
WebClient Config.
WebClient.Builder webClientBuilder(MetricsWebClientCustomizer metricsCustomizer) {
final HttpClient httpClient = HttpClient.create(ConnectionProvider.fixed("webClientPool", maxConnections))
.tcpConfiguration(client ->
client.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, connectionTimeoutMillis)
.option(EpollChannelOption.TCP_KEEPIDLE, tcpKeepIdleInSec)
.option(EpollChannelOption.TCP_KEEPINTVL, tcpKeepIntvlInSec)
.option(EpollChannelOption.TCP_KEEPCNT, tcpKeepCount)
.doOnConnected(conn -> conn
.addHandlerLast(new ReadTimeoutHandler(readTimeoutInSec))
));
final WebClient.Builder webClient = WebClient.builder()
.clientConnector(new ReactorClientHttpConnector(httpClient));
metricsCustomizer.customize(webClient);
return webClient;
}
return client.post()
.uri(uriBuilder -> buildUri())
.contentType(MediaType.APPLICATION_JSON)
.body(BodyInserters.fromObject(request))
.retrieve()
.bodyToMono(Result.class)
.compose(CircuitBreakerOperator.of(cb))
.block(Duration.ofSeconds(blockDuration));
This setup works well. However, I see a lot of
io.netty.handler.timeout.ReadTimeoutException: null
Suppressed: java.lang.Exception: #block terminated with an error
at reactor.core.publisher. .blockingGet(BlockingSingleSubscriber.java:133)
at reactor.core.publisher.Mono.block(Mono.java:1518)
at com.xxxx.c.g.xx.client.Client.get(Client.java:293)
at com.xxxx.c.g.xx.resolver.impl.xxQueryImpl.lambda$xx$171(xxQueryImpl.java:2187)
at io.micrometer.core.instrument.AbstractTimer.record(AbstractTimer.java:149)
Every timeout results in this chunky stacktrace. It's the same message that gets repeated. Does give any useful info. Is there a way to get WebClient/Netty print this once and ignore the rest?
BlockingSingleSubscriber.blockingGet seems to be doing this. Keeps track of previous exceptions.
Throwable e = this.error;
if (e != null) {
RuntimeException re = Exceptions.propagate(e);
re.addSuppressed(new Exception("#block terminated with an error"));
throw re;
} else {
return this.value;
}
I tried adding error handlers to the calling function bodyToMono(Result.class).doOnError("log").block();
This results in the line "log" from the doOnError consumer getting printed along with the chunky stacktraces.
Any ideas?
Full stacktrace:
https://pastebin.com/kdpspCEY
Not sure if you've solved the issue yet, but I had the same problem. For anyone with a similar issue, I solved it by setting the following in the application.properties file:
# Logging Information
logging.level.reactor.netty=off
This disabled Reactor Netty internal logging, which was a bit verbose for what I needed.
Related
In my Spring Boot project I defined some #ExceptionHandler classes in a #ControllerAdvice to handle specific exceptions. These build an application-specific JSON reponse instead of the default provided by Spring Boot. So far this works fine for e.g. the MethodArgumentNotValidException which is thrown in case the request validation fails.
#ExceptionHandler(value = {MethodArgumentNotValidException.class})
public ResponseEntity<ApplicationResponse> handleValidationException(MethodArgumentNotValidException ex) {
Map<String, String> errors = new HashMap<>();
ex.getBindingResult().getAllErrors().forEach((error) -> {
String fieldName = ((FieldError) error).getField();
String errorMessage = error.getDefaultMessage();
errors.put(fieldName, errorMessage);
});
log.error("Validation failed: {}", errors);
return buildErrorResponse(ApplicationError.REQUEST_BODY_VALIDATION_FAILED, errors.toString(), HttpStatus.BAD_REQUEST);
}
This approach seems reasonable to me for all exceptions I'm aware of. But what happens if an exception is thrown for which no #ExceptionHandler exists? Then Spring Boot's default will kick in resulting in a JSON response which looks different from my custom one.
My first idea was to add an #ExceptionHandler for the Exception class like this:
#ExceptionHandler(value = {Exception.class})
public ResponseEntity<ApplicationResponse> handleValidationException(Exception ex) {
log.error("An unhandled error occurred: {}", ex.getMessage());
return buildErrorResponse(ApplicationError.GENERIC_ERROR, HttpStatus.INTERNAL_SERVER_ERROR);
}
While this seems to be working I wonder if this approach could have some major drawbacks I'm not aware of. How would you response with a custom JSON structure for any error that might occur?
Spring has several ways to deal with an exception handler as you can see here. Use #ControllerAdvice with several #ExceptionHandler has the following advantages:
Centralized class to deal with not catched exceptions.
Customization about how you want to manage specific ones.
Regarding to include a #ExceptionHandler(value = {Exception.class}) to manage exceptions without an specific handler, a better option is create the next one: #ExceptionHandler(Throwable.class), in that way you will be able to manage all potential problems in your application.
About return a Json when in your application an error happens, you can configure it in the response itself. For example:
private ResponseEntity<ErrorResponseDto> buildErrorResponse(RestApiErrorCode errorCode, List<String> errorMessages, HttpStatus httpStatus) {
HttpHeaders headers = new HttpHeaders();
headers.setContentType(APPLICATION_JSON);
ErrorResponseDto error = new ErrorResponseDto(errorCode, errorMessages);
return new ResponseEntity<>(error, headers, httpStatus);
}
You can see the rest of the code here
hoping someone can steer me in the right direction in turning my code into a more reactive service call. for background I have a preceding function that will generate a list of users, will then use that list to call this getUserDetails function for each user in the list, and return a map or list of user + details.
#HystrixCommand(commandKey = "getUserDetails")
public getUserResponse getUserDetails(userRequest request) {
getUserResponse response = webClient.post()
.uri(uri)
.body(BodyInserters.fromObject(request))
.retrieve()
.onStatus(HttpStatus::isError, resp -> resp.bodyToMono(getUserError.class).map(errorHandler::mapRequestErrors))
.bodyToMono(getUserResponse.class).block();
return response;
}
Ideally I would also replace/remove the error mapping as only concerned with logging the returned error response and continuing.
so far I have thought something along the lines of this but I'm not sure the webflux/hystrix will play nice?
#HystrixCommand(commandKey = "getUserDetails", fallbackMethod = "getUserFallback")
public Mono<getUserResponse> getUserDetails(userRequest request) {
return = webClient.post()
.uri(uri)
.body(BodyInserters.fromObject(request))
.retrieve()
.bodyToMono(getUserResponse.class);
}
#HystrixCommand
public Mono<getUserResponse> getUserFallback(userRequest request, Throwable throwable) {
log.error(//contents of error message returned)
return mono.empty();
}
public Flux<UserMap> fetchUserDetails(List<Integer> userIds) {
return Flux.fromIterable(userIds)
.parallel()
.runOn(Schedulers.elastic())
.flatMap(userDetailsRepository::getUserDetails);
}
Hystrix is deprecated. If you have a chance, move to resilience4j which has support for Webflux/Reactor.
Spring also has dedicated support for resilience4j.
Regarding error handling you can leverage the rich set of operators from the Mono/Flux API like onErrorReturn or onErrorResume.
I am working in a Spring #Component class and I am trying to get the HTTP status code of a particular URL for further processing. I have a function as follows:
fun getStatus() : String
{
val webClient = WebClient.create("https://stackoverflow.com")
val result = webClient.get()
.exchange().map { res -> res.rawStatusCode() }
println(result)
return "statusGotten"
}
However, rather than getting the Int value of the status code (e.g. 200, or 401), I am simply getting: "MonoMap".
I am new to both Spring and Web Programming in general, so I'm a little confused how to proceed from here. I'm aware that "result" is being returned as a "Mono", but I'm less clear about what a "Mono" is, or how I might transform it into something with more scrutable properties, as even looking at "result" in the debugger doesn't shed any light as to whether the HTTP request was actually sent or was successful:
Am I calling the webclient incorrectly? Or merely failing to parse the resultant data in a meaningful way? Any suggestions on how or where I might learn more about the underlying topics would be much appreciated as well.
If you need a blocking way to do this is easy just
#Test
public void myTest(){
WebClient client = WebClient.builder().baseUrl("https://stackoverflow.com/").build();
ClientResponse resp = client
.get()
.uri("questions/")
.exchange()
.block();
System.out.println("Status code response is: "+resp.statusCode());
}
But for this you can use directly the RestTemplate instead the webclient... the recomended way to do this is non blocking what means you should return a Mono with the status and consume outside your method like for example:
public Mono<HttpStatus> myMethod(){
WebClient client = WebClient.builder().baseUrl("https://stackoverflow.com/").build();
return client
.get()
.uri("questions/")
.exchange()
.map( clientResp -> clientResp.statusCode());
}
The way of consume this Mono depends of your code...
I use this code for REST API requests.
WebClient.Builder builder = WebClient.builder().baseUrl(gatewayUrl);
ClientHttpConnector httpConnector = new ReactorClientHttpConnector(opt -> opt.sslContext(sslContext));
builder.clientConnector(httpConnector);
How I can add connection exception handler? I would like to implement some custom logic? Is this feature easy to implement?
If I understand your question in the context of failing the connection because of SSL credentials, then you should see the connection exception manifest itself on the REST response.
You can take care of that exception via the Flux result you get on WebClient.ResponseSpec#onStatus. The docs for #onStatus says:
Register a custom error function that gets invoked when the given
HttpStatus predicate applies. The exception returned from the function
will be returned from bodyToMono(Class) and bodyToFlux(Class). By
default, an error handler is register that throws a
WebClientResponseException when the response status code is 4xx or
5xx.
Take a look at this example:
Mono<Person> result = client.get()
.uri("/persons/{id}", id).accept(MediaType.APPLICATION_JSON)
.retrieve()
.onStatus(HttpStatus::is4xxServerError, response -> ...) // This is in the docs there but is wrong/fatfingered, should be is4xxClientError
.onStatus(HttpStatus::is5xxServerError, response -> ...)
.bodyToMono(Person.class);
Similarly for your question, the connection error should manifest itself after the call gets made and you can customize how it gets propogated in the reactive pipeline:
Mono<Person> result = client.get()
.uri("/persons/{id}", id).accept(MediaType.APPLICATION_JSON)
.retrieve()
.onStatus(HttpStatus::is4xxClientError, response -> {
... Code that looks at the response more closely...
return Mono.error(new MyCustomConnectionException());
})
.bodyToMono(Person.class);
Hope that helps.
I have a spring integration flow defined in the flow DSL syntax. One of my handlers is a Webflux.outboundGateway. When the remote URI is not accessible, an exception is thrown and sent to the "errorChannel". I'm trying to have the flow to retry, but so far with no success (the call is never retried). Here is what my configuration looks like:
#Bean
public IntegrationFlow retriableFlow() {
return IntegrationFlows
.from(...)
.handle(
WebFlux.outboundGateway(m ->
UriComponentsBuilder.fromUriString(remoteGateway + "/foo/bar")
.build()
.toUri(), webClient)
.httpMethod(HttpMethod.POST)
.expectedResponseType(String.class)
.replyPayloadToFlux(true), e -> e.advice(retryAdvice())
)
// [ ... ]
.get();
}
#Bean
public Advice retryAdvice() {
RequestHandlerRetryAdvice advice = new RequestHandlerRetryAdvice();
RetryTemplate retryTemplate = new RetryTemplate();
ExponentialBackOffPolicy retryPolicy = new ExponentialBackOffPolicy();
retryPolicy.setInitialInterval(1000);
retryPolicy.setMaxInterval(20000);
retryTemplate.setBackOffPolicy(retryPolicy);
advice.setRetryTemplate(retryTemplate);
return advice;
}
Should I be using something different than the RequestHandlerRetryAdvice? If so, what should it be?
Webflux is, by definition, async, which means the Mono (reply) is satisfied asynchronously when the request completes/fails, not on the calling thread. Hence the advice won't help because the "send" part of the request is always successful.
You would have to perform retries via a flow on the error channel (assigned somewhere near the start of the flow). With, perhaps, some header indicating how many times you have retried.
The ErrorMessage has properties failedMessage and cause; you can resend the failedMessage.
You could turn off async so the calling thread blocks, but that really defeats the whole purpose of using WebFlux.