Handling Global Scenarios in Spring WebFlux - spring-webflux

I have a Rest Web Client todo an API call and I handle the exceptions as given below.
I want to handle 404, 401 and 400 errors in a global way rather than handling at the individual client level. How can we achieve the same.
public Mono<ProductResponse> getProductInformation(String productId) {
return webClient.get()
.uri("/v1/products/"+productId)
.accept(MediaType.APPLICATION_JSON)
.retrieve()
.onStatus( httpStatus -> HttpStatus.NOT_FOUND.equals(httpStatus), clientResponse -> {
Mono<NotFound> notFound = clientResponse.bodyToMono(NotFound.class);
return notFound.flatMap( msg -> {
log.info(" Error Message {}" , msg.getErrorMsg());
return Mono.error(new NotFoundException(msg.getErrorMsg()));
});
}).onStatus( httpStatus -> HttpStatus.UNAUTHORIZED.equals(httpStatus), clientResponse -> {
Mono<NotFound> notFound = clientResponse.bodyToMono(NotFound.class);
return Mono.error(new NotAuthorisedException("Unauthorised"));
}).bodyToMono(ProductResponse.class);
}

Two approaches:
Exceptions with webclients are all wrapped in WebClientResponseException class. You can handle that using Spring's ExceptionHandler annotation like this.
#ExceptionHandler(WebClientResponseException.class)
public ResponseEntity handleWebClientException(WebClientResponseException ex){
return ResponseEntity.badRequest().body(ex.getResponseBodyAsString());
}
Note - Here you can write complex conditional logic based on the response status, by using methods like getStatusCode(), getRawStatusCode(), getStatusText(), getHeaders() and getResponseBodyAsString(). Also you can get reference of the request that was sent using the method getRequest.
Using ExchangeFilterFunction while constructing the webclient bean.
#Bean
public WebClient buildWebClient() {
Function<ClientResponse, Mono<ClientResponse>> webclientResponseProcessor =
clientResponse -> {
HttpStatus responseStatus = clientResponse.statusCode();
if (responseStatus.is4xxClientError()) {
System.out.println("4xx error");
return Mono.error(new MyCustomClientException());
} else if (responseStatus.is5xxServerError()) {
System.out.println("5xx error");
return Mono.error(new MyCustomClientException());
}
return Mono.just(clientResponse);
};
return WebClient.builder()
.filter(ExchangeFilterFunction.ofResponseProcessor(webclientResponseProcessor)).build();
}
Then you can either handle the MyCustomClientException using #ExceptionHandler or leave it as it is.

Related

How to return a Flux in async/reactive webclient request with subscribe method

I am using spring hexagonal architecture (port and adapter) as my application need to read the stream of data from the source topic, process/transforms the data, and send it to destination topic.
My application need to do the following actions.
Read the data (which will have the call back url)
Make an http call with the url in the incoming data (using webclient)
Get the a actual data and it needs to be transformed into another format.
Send the transformed data to the outgoing topic.
Here is my code,
public Flux<TargeData> getData(Flux<Message<EventInput>> message)
{
return message
.flatMap(it -> {
Event event = objectMapper.convertValue(it.getPayload(), Event.class);
String eventType = event.getHeader().getEventType();
String callBackURL = "";
if (DISTRIBUTOR.equals(eventType)) {
callBackURL = event.getHeader().getCallbackEnpoint();
WebClient client = WebClient.create();
Flux<NodeInput> nodeInputFlux = client.get()
.uri(callBackURL)
.headers(httpHeaders -> {
httpHeaders.setContentType(MediaType.APPLICATION_JSON);
List<MediaType> acceptTypes = new ArrayList<>();
acceptTypes.add(MediaType.APPLICATION_JSON);
httpHeaders.setAccept(acceptTypes);
})
.exchangeToFlux(response -> {
if (response.statusCode()
.equals(HttpStatus.OK)) {
System.out.println("Response is OK");
return response.bodyToFlux(NodeInput.class);
}
return Flux.empty();
});
nodeInputFlux.subscribe( nodeInput -> {
SourceData source = objectMapper.convertValue(nodeInput, SourceData.class);
// return Flux.fromIterable(this.TransformImpl.transform(source));
});
}
return Flux.empty();
});
}
The commented line in the above code is giving the compilation as subscribe method does not allow return types.
I need a solution "without using block" here.
Please help me here, Thanks in advance.
I think i understood the logic. What do you may want is this:
public Flux<TargeData> getData(Flux<Message<EventInput>> message) {
return message
.flatMap(it -> {
// 1. marshall and unmarshall operations are CPU expensive and could harm event loop
return Mono.fromCallable(() -> objectMapper.convertValue(it.getPayload(), Event.class))
.subscribeOn(Schedulers.parallel());
})
.filter(event -> {
// 2. Moving the if-statement yours to a filter - same behavior
String eventType = event.getHeader().getEventType();
return DISTRIBUTOR.equals(eventType);
})
// Here is the trick 1 - your request below return Flux of SourceData the we will flatten
// into a single Flux<SourceData> instead of Flux<List<SourceData>> with flatMapMany
.flatMap(event -> {
// This WebClient should not be created here. Should be a singleton injected on your class
WebClient client = WebClient.create();
return client.get()
.uri(event.getHeader().getCallbackEnpoint())
.accept(MediaType.APPLICATION_JSON)
.exchangeToFlux(response -> {
if (response.statusCode().equals(HttpStatus.OK)) {
System.out.println("Response is OK");
return response.bodyToFlux(SourceData.class);
}
return Flux.empty();
});
})
// Here is the trick 2 - supposing that transform return a Iterable of TargetData, then you should do this and will have Flux<TargetData>
// and flatten instead of Flux<List<TargetData>>
.flatMapIterable(source -> this.TransformImpl.transform(source));
}

Spring reactive web client REST request with oauth token in case of 401 response

I wanted to play around with Spring reactive web client and an actually simple example: Ask for a REST resource and in case of a 401 response get new OAuth access token.
The first part seemed to be easy:
return webClientBuilder
.baseUrl(targetInstance.getBaseUrl())
.build()
.get().uri(targetInstance.getItemEndpointUrl())
.retrieve()
.bodyToMono(ItemResponse.class)
....
But here the confusion already started. I tried something like
.onStatus(HttpStatus::is4xxClientError, (response) -> {
if(response.rawStatusCode() == 401) {
oAuthClient.initToken()
My token should then be saved within an instance JPA entity. But I have a lack of conceptual understanding here I guess. When the OAuth client receives the OAuth response I need to extract it first to persist it (as embedded object) within my instance entity. And therefore I need to block it, right?
.exchangeToMono(response -> {
if (response.statusCode().equals(HttpStatus.OK)) {
OAuthResponse oauthResponse = response.bodyToMono(OAuthResponse.class).block();
}
Based on the response result of the OAuth client I need some kind of Mono to tell the actual REST client then if it should start a retry? And which way should be the preferred on: .retrieve() or .exchangeToMono()? So I'm a bit lost here if I'm on the right path or if something like that should better be done with the classic RestTemplate? But I've also read that the RestTemplate is no deprecated...
Thanks for sharing some thoughts with me.
Ok, in the meantime I've found a non-blocking way. Maybe not the best, but it works out well for me.
The client:
class ApiClient {
public Mono<MyResponse> getResponse(Tenant tenant) {
return webClientBuilder
.baseUrl(tenant.getUrl())
.clientConnector(getClientConnector())
.build()
.get().uri("/api/my-content-entpoint")
.exchangeToMono(response -> {
if (response.statusCode().equals(HttpStatus.OK)) {
return response.bodyToMono(MyResponse.class);
} else if(response.statusCode().equals(HttpStatus.FORBIDDEN)) {
return Mono.error(new MyOAuthExcpetion());
} else {
return Mono.empty();
}
});
}
}
the service:
#Service
public class MyService {
private final ApiClient apiClient;
private final RetryStrategy retryStrategy;
private final TenantService tenantService;
public Mono<MyResponse> getResponse(String tenantId){
return tenantService.getTenant(tenantId)
.flatMap(tenant-> apiClient.getResponse(instance))
.retryWhen(Retry.from(signals -> signals
.flatMap(retrySignal -> retryStrategy.reconnect(retrySignal, tenantId))));
}
}
and the retry strategy
#Component
public class RetryStrategy {
private final TenantService tenantService;
public Publisher<? extends Long> reconnect(RetrySignal retrySignal, String tenantId) {
long count = retrySignal.totalRetriesInARow();
Throwable failure = retrySignal.failure();
if(count > 0) {
return Mono.error(new UnsupportedOperationException("Retry failed", failure));
}
Mono<Tenant> updatedTenant = null;
if(failure instanceof MyOAuthExcpetion) {
updatedTenant = tenantService.getTenant(tenantId)
.flatMap(tenant -> tenantService.refreshOAuth(tenant));
}
if(updatedTenant == null) {
return Mono.error(new UnsupportedOperationException("Retry failed", failure));
}
return updatedTenant.then(Mono.delay(Duration.ofSeconds(1)));
}
}
Happy for any feedback or improvements.
In my application I went with prechecking the token before requests are being made:
client.get()
.uri("...")
.header("Authorization", "Bearer " + authenticator.getToken(client,token))
.retrieve()
...
And in Authenticator Service I verify the validity of the token as follow:
String getToken(WebClient client, String token) {
if (token == null || isTokenExpired(token)) {
return this.fetchToken(client); // fetches a new token
}
return token;
}
private boolean isTokenExpired(String token) {
DecodedJWT jwt = JWT.decode(token);
return jwt.getExpiresAt().before(new Date());
}

How do I hook into micronaut server on error handling from a filter?

For any 4xx or 5xx response given out by my micronaut server, I'd like to log the response status code and endpoint it targeted. It looks like a filter would be a good place for this, but I can't seem to figure out how to plug into the onError handling
for instance, this filter
#Filter("/**")
class RequestLoggerFilter: OncePerRequestHttpServerFilter() {
companion object {
private val log = LogManager.getLogger(RequestLoggerFilter::class.java)
}
override fun doFilterOnce(request: HttpRequest<*>, chain: ServerFilterChain): Publisher<MutableHttpResponse<*>>? {
return Publishers.then(chain.proceed(request), ResponseLogger(request))
}
class ResponseLogger(private val request: HttpRequest<*>): Consumer<MutableHttpResponse<*>> {
override fun accept(response: MutableHttpResponse<*>) {
log.info("Status: ${response.status.code} Endpoint: ${request.path}")
}
}
}
only logs on a successful response and not on 4xx or 5xx responses.
How would i get this to hook into the onError handling?
You could do the following. Create your own ApplicationException ( extends RuntimeException), there you could handle your application errors and in particular how they result into http error codes. You exception could hold the status code as well.
Example:
class BadRequestException extends ApplicationException {
public HttpStatus getStatus() {
return HttpStatus.BAD_REQUEST;
}
}
You could have multiple of this ExceptionHandler for different purposes.
#Slf4j
#Produces
#Singleton
#Requires(classes = {ApplicationException.class, ExceptionHandler.class})
public class ApplicationExceptionHandler implements ExceptionHandler<ApplicationException, HttpResponse> {
#Override
public HttpResponse handle(final HttpRequest request, final ApplicationException exception) {
log.error("Application exception message={}, cause={}", exception.getMessage(), exception.getCause());
final String message = exception.getMessage();
final String code = exception.getClass().getSimpleName();
final ErrorCode error = new ErrorCode(message, code);
log.info("Status: ${exception.getStatus())} Endpoint: ${request.path}")
return HttpResponse.status(exception.getStatus()).body(error);
}
}
If you are trying to handle Micronaut native exceptions like 400 (Bad Request) produced by ConstraintExceptionHandler you will need to Replace the beans to do that.
I've posted example here how to handle ConstraintExceptionHandler.
If you want to only handle responses itself you could use this mapping each response code (example on #Controller so not sure if it works elsewhere even with global flag:
#Error(status = HttpStatus.NOT_FOUND, global = true)
public HttpResponse notFound(HttpRequest request) {
<...>
}
Example from Micronaut documentation.
Below code I used for adding custom cors headers in the error responses, in doOnError you can log errors
#Filter("/**")
public class ResponseCORSAdder implements HttpServerFilter {
#Override
public Publisher<MutableHttpResponse<?>> doFilter(HttpRequest<?> request, ServerFilterChain chain) {
return this.trace(request)
.switchMap(aBoolean -> chain.proceed(request))
.doOnError(error -> {
if (error instanceof MutableHttpResponse<?>) {
MutableHttpResponse<?> res = (MutableHttpResponse<?>) error;
addCorsHeaders(res);
}
})
.doOnNext(res -> addCorsHeaders(res));
}
private MutableHttpResponse<?> addCorsHeaders(MutableHttpResponse<?> res) {
return res
.header("Access-Control-Allow-Origin", "*")
.header("Access-Control-Allow-Methods", "OPTIONS,POST,GET")
.header("Access-Control-Allow-Credentials", "true");
}
private Flowable<Boolean> trace(HttpRequest<?> request) {
return Flowable.fromCallable(() -> {
// trace logic here, potentially performing I/O
return true;
}).subscribeOn(Schedulers.io());
}
}

I am at a loss of how to create an appropriate flow using webflux

Problem statement: I have a POST request to a booking-service API that gets a BookingRecord. I map it to extract the values so as to call another fare-service API using WebClient. I receive a Mono<Fare> from that call. I need to check whether the value of getFare() method of BookingRecord type is same as the getFare() of the Fare type returned by the WebClient. If not, I need to raise and exception, and pass it on to the caller. Here caller is another Microservice, ui-service calling the booking-service API (so how should I deal with this, pass the error back or else what is the best thing to do?) or else I will save the new BookingRecord and return the id of that record to the caller. What is the best flow sequence for this? I tried my best without much success and am pasting the code here.
public HandlerFunction<ServerResponse> book = request ->
{
request.bodyToMono(BookingRecord.class)
.map(br ->
{
this.webClient.get()
.uri("/fares/get/{flightNumber}/{flightDate}",
br.getFlightNumber(),
br.getFlightDate())
.retrieve()
.bodyToMono(Fare.class)
.map(f ->
{
if (!f.getFare()
.equals(br.getFare()))
{
throw new RuntimeException("Fare is tampered");
}
else
{
id = bookingRepository.save(br).getId();
}
return id;
})
.subscribe();
return id;
});
return ServerResponse.ok()
.body(BodyInserters.fromObject(id));
};
After much tweaking, this is what I did. Hope its the right thing to do. 1. I raise a 500 Http error from the fare-service itself instead of checking in the booking-service.
public HandlerFunction<ServerResponse> getFare = request ->
{
String flightNumber = request.pathVariable("flightNumber");
String flightDate = request.pathVariable("flightDate");
String fare = request.pathVariable("fare");
Mono<ServerResponse> notFound = ServerResponse.notFound()
.build();
return Mono
.justOrEmpty(faresRepository.getFareByFlightNumberAndFlightDateAndFare(flightNumber,
flightDate,
fare))
.flatMap(f -> ServerResponse.ok()
.contentType(APPLICATION_JSON)
.body(fromObject(f)))
.switchIfEmpty(notFound);
};
Handled the exception like so in booking-service using onStatus() method
public HandlerFunction<ServerResponse> book = request ->
{
logger.info("Inside Book function");
return request.bodyToMono(BookingRecord.class)
.flatMap(br ->
{
logger.info("Calling fare-service");
return this.webClient.get()
.uri("/fares/get/{flightNumber}/{flightDate}/{fare}",
br.getFlightNumber(),
br.getFlightDate(),
br.getFare())
.retrieve()
.onStatus(HttpStatus::isError,
x -> Mono
.error(new RuntimeException("Fare has been tampered with!!")))
.bodyToMono(Fare.class);
})
.map(fare ->
{
logger.info("Saving a BookingRecord");
BookingRecord br = new BookingRecord();
br.setFlightNumber(fare.getFlightNumber());
br.setFlightDate(fare.getFlightDate());
br.setFare(fare.getFare());
long id = bookingRepository.save(br)
.getId();
return id;
})
.flatMap(id -> ServerResponse.ok()
.body(BodyInserters.fromObject(id)));
};
This way, I get an exception for fare tampering or get the id for a successful db save.

how to return something from a Ninject Interceptor

I have written a common validator as part of Ninject interceptor. My requirement is that I have to return a response object, just like how any service method in my project returns, for consistency sake. By returning a response object also helps me to send back an appropriate validation message when the validator fails. How do I do that in the interceptor? I understood that the Intercept() returns nothing. I tried throwing an exception but I don't know where to catch it. Can someone help me?
public void Intercept(IInvocation invocation)
{
var validationFails = false;
if (validationFails)
{
// return an object
// response.ErrorMessage = "Validation Error"
// Or throw exception, but where should I catch it
throw new Exception(statusMessage);
}
else
{
invocation.Proceed();
}
}
Assign the ReturnValue and don't call Proceed when validation fails.
public class MyRequestHandler
{
Response ProcessRequest(string input) { return new Response(); }
}
public MyValidationInterceptor : IInterceptor
{
public void Intercept( IInvocation invocation )
{
if (NeedsValidation(invocation.Method) &&
!IsValidRequest((string)invocation.Arguments[0]))
{
invocation.ReturnValue =
new Response { ErrorMessage = "Validation Error" };
return;
}
invocation.Proceed();
}
}
I had to hook up my interceptor to business layer methods, instead of service methods, and am able to return proper return value as part of my response.