Spring Cloud Gateway chaining nested Mono - spring-webflux

I have a web app (Spring Cloud Gateway with Project Reactor) where I have to logout (send another http request) when something goes wrong and set 401 to the main response. The problem is when I execute another request in onErrorResume block, the root response seems to ignore finishWithStatus() logic entirely and returns 200.
#Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
return someFunctionWhichReturnsMono()
.flatMap(chain::filter)
.onErrorResume(e -> {
log.error("Unexpected Filter Error, logging out user", e);
// A. this doesn't set 401, seems like ignoring finishWithStatus(..)
// called inside this method in onErrorResume block
return logout(exchange);
// B. this works fine and I get 401 as a response
// return finishWithStatus(exchange, HttpStatus.UNAUTHORIZED);
});
}
protected Mono<Void> finishWithStatus(ServerWebExchange exchange, HttpStatus status) {
exchange.getResponse().setStatusCode(status);
return exchange.getResponse().setComplete();
}
protected void logout(ServerWebExchange exchange) {
webClient
.post()
.uri(....)
.retrieve()
.bodyToMono(Void.class)
.doOnSuccess(any -> {
log.info("Successfully logged out user");
})
.then(finishWithStatus(exchange, HttpStatus.UNAUTHORIZED))
.onErrorResume(e -> {
log.error("Failed to logout user", e);
//the following line has no effect when error happens
return finishWithStatus(exchange, HttpStatus.UNAUTHORIZED);
});
}
Could somebody explain why is that despite I return Mono in both cases. However, in case A I have nested onErrorResume (at onErrorResume of the "root" Mono I create another Mono with its own onErrorResume).
I feel I miss something fundamental like I need to "join" two Monos or say bubble up some Mono.error from the deepest onErrorResume to the top one?
What would be a generic approach to handle nested errors (like the case above when on an error you have to send another request which in turn might end up with an error).
I would greatly appreciate any advice or sample on this matter.

Fundamental rule in reactive programming: Nothing happens until you subscribe
finishWithStatus() works because you are passing the publisher value to the next filter by returning Mono<Void> type.
You are making chain of reactive operators in logout() method but never subscribing or passing the publisher to the next filter. Therefore nothing happens even if you call this method
Another point is you can't return logout() method in filter() method since the return type for filter() is Mono<Void> not void.
You should change the return type for logout() method:
protected Mono<Void> logout(ServerWebExchange exchange) {
return webClient
.post()
.uri(...)
.retrieve()
.bodyToMono(Void.class)
.doOnSuccess(any -> {
log.info("Successfully logged out user");
})
.then(finishWithStatus(exchange, HttpStatus.UNAUTHORIZED))
.onErrorResume(e -> {
log.error("Failed to logout user", e);
return finishWithStatus(exchange, HttpStatus.UNAUTHORIZED);
});
}

Related

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

Handling Global Scenarios in 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.

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.

Lagom http status code / header returned as json

I have a sample where I make a client request to debug token request to the FB api, and return the result to the client.
Depending on whether the access token is valid, an appropriate header should be returned:
#Override
public ServerServiceCall<LoginUser, Pair<ResponseHeader, String>> login() {
return this::loginUser;
}
public CompletionStage<Pair<ResponseHeader, String>> loginUser(LoginUser user) {
ObjectMapper jsonMapper = new ObjectMapper();
String responseString = null;
DebugTokenResponse.DebugTokenResponseData response = null;
ResponseHeader responseHeader = null;
try {
response = fbClient.verifyFacebookToken(user.getFbAccessToken(), config.underlying().getString("facebook.app_token"));
responseString = jsonMapper.writeValueAsString(response);
} catch (ExecutionException | InterruptedException | JsonProcessingException e) {
LOG.error(e.getMessage());
}
if (response != null) {
if (!response.isValid()) {
responseHeader = ResponseHeader.NO_CONTENT.withStatus(401);
} else {
responseHeader = ResponseHeader.OK.withStatus(200);
}
}
return completedFuture(Pair.create(responseHeader, responseString));
}
However, the result I get is:
This isn't really what I expected. What I expect to receive is an error http status code of 401, and the json string as defined in the code.
Not sure why I would need header info in the response body.
There is also a strange error that occurs when I want to return a HeaderServiceCall:
I'm not sure if this is a bug, also I am a bit unclear about the difference between a ServerServiceCall and HeaderServiceCall.
Could someone help?
The types for HeaderServiceCall are defined this way:
interface HeaderServiceCall<Request,Response>
and
CompletionStage<Pair<ResponseHeader,Response>> invokeWithHeaders(RequestHeader requestHeader,
Request request)
What this means is that when you define a response type, the return value should be a CompletionStage of a Pair of the ResponseHeader with the response type.
In your code, the response type should be String, but you have defined it as Pair<ResponseHeader, String>, which means it expects the return value to be nested: CompletionStage<Pair<ResponseHeader,Pair<ResponseHeader, String>>>. Note the extra nested Pair<ResponseHeader, String>.
When used with HeaderServiceCall, which requires you to implement invokeWithHeaders, you get a compilation error, which indicates the mismatched types. This is the error in your screenshot above.
When you implement ServerServiceCall instead, your method is inferred to implement ServiceCall.invoke, which is defined as:
CompletionStage<Response> invoke()
In other words, the return type of the method does not expect the additional Pair<ResponseHeader, Response>, so your implementation compiles, but produces the incorrect result. The pair including the ResponseHeader is automatically serialized to JSON and returned to the client that way.
Correcting the code requires changing the method signature:
#Override
public HeaderServiceCall<LoginUser, String> login() {
return this::loginUser;
}
You also need to change the loginUser method to accept the RequestHeader parameter, even if it isn't used, so that it matches the signature of invokeWithHeaders:
public CompletionStage<Pair<ResponseHeader, String>> loginUser(RequestHeader requestHeader, LoginUser user)
This should solve your problem, but it would be more typical for a Lagom service to use domain types directly and rely on the built-in JSON serialization support, rather than serializing directly in your service implementation. You also need to watch out for null values. You shouldn't return a null ResponseHeader in any circumstances.
#Override
public ServerServiceCall<LoginUser, Pair<ResponseHeader, DebugTokenResponse.DebugTokenResponseData>> login() {
return this::loginUser;
}
public CompletionStage<Pair<ResponseHeader, DebugTokenResponse.DebugTokenResponseData>> loginUser(RequestHeader requestHeader, LoginUser user) {
try {
DebugTokenResponse.DebugTokenResponseData response = fbClient.verifyFacebookToken(user.getFbAccessToken(), config.underlying().getString("facebook.app_token"));
ResponseHeader responseHeader;
if (!response.isValid()) {
responseHeader = ResponseHeader.NO_CONTENT.withStatus(401);
} else {
responseHeader = ResponseHeader.OK.withStatus(200);
}
return completedFuture(Pair.create(responseHeader, response));
} catch (ExecutionException | InterruptedException | JsonProcessingException e) {
LOG.error(e.getMessage());
throw e;
}
}
Finally, it appears that fbClient.verifyFacebookToken is a blocking method (it doesn't return until the call completes). Blocking should be avoided in a Lagom service call, as it has the potential to cause performance issues and instability. If this is code you control, it should be written to use a non-blocking style (that returns a CompletionStage). If not, you should use CompletableFuture.supplyAsync to wrap the call in a CompletionStage, and execute it in another thread pool.
I found this example on GitHub that you might be able to adapt: https://github.com/dmbuchta/empty-play-authentication/blob/0a01fd1bd2d8ef777c6afe5ba313eccc9eb8b878/app/services/login/impl/FacebookLoginService.java#L59-L74

WSO2 API Manager - Handle Authentication failure with Custom Authenticator

I'm implementing a custom Authentication Handler for wso2-am following this guide https://docs.wso2.com/display/AM190/Writing+Custom+Handlers
But it's not clear how to handle the case when my authentication handler returns false. The sample code of the handleRequest is
public boolean handleRequest(MessageContext messageContext) {
try {
if (authenticate(messageContext)) {
return true;
}
} catch (APISecurityException e) {
e.printStackTrace();
}
return false;
}
If I try calling an API with valid credentials everything goes well (the method returns true) and I get an "HTTP 200 OK" response. If I try with invalid credentials the method returns false but I get an HTTP 202 ACCEPTED" response. I would like to receive another response code (for example 400). How do I handle this authentication failure path?
Thank you.
Ideally you need to call handleAuthFaliure method with message context.
private void handleAuthFailure(MessageContext messageContext, APISecurityException e)
When you call that method please create APISecurityException object(as listed below) and pass it. Then error code and error messages will be picked from there and automatically send error to client(by default we return 401 for security exceptions and if defined then send defined error code and message).
public class APISecurityException extends Exception {
private int errorCode;
public APISecurityException(int errorCode, String message) {
super(message);
this.errorCode = errorCode;
}
public APISecurityException(int errorCode, String message, Throwable cause) {
super(message, cause);
this.errorCode = errorCode;
}
public int getErrorCode() {
return errorCode;
}
}
Hope this will work for you.
Thanks
sanjeewa.