Resilience4j : Intercept 401 Response, refresh token and Retry - resilience4j

In Resilience4j, do we have support to intercept retry request? For e.g. when we make an API call and get Authentication failed with http status code as 401. Before retrying again, we would like to make a different API cal to get refresh token and retry again. Below is our RetryConfig and it works fine i.e. it retries the same request again if it gets any of the mentioned httpStatusCode in the exception. But for httpStatusCode 401, we want to intercept and make an API call to refresh the token.
public RetryConfig retryConfig() {
return RetryConfig.custom()
.maxAttempts(DEFAULT_MAX_RETRIES)
.retryOnException(shouldRetryHttpCode())
.intervalFunction(IntervalFunction.ofExponentialBackoff(
DEFAULT_BACKOFF_TIME_INTERVAL, DEFAULT_BACKOFF_TIME_MULTIPLIER))
.build();
}
private Predicate<Throwable> shouldRetryHttpCode() {
return result -> {
APIException exception = ((APIException) result);
return exception.httpStatusCode == 401
|| exception.httpStatusCode == 429
|| exception.httpStatusCode >= 500;
};
}
// wrapper method which is going to wrap our API request and use retryConfig
public <T> T executeApiCall(CheckedFunction0<T> checkedSupplier) {
return Try.of(() -> Retry.decorateCheckedSupplier(
retryCustomConfig("retryApiCall", this.retryConfig()),checkedSupplier).apply())
.getOrElseThrow(ex -> new RuntimeException("something went wrong while processing the client request", ex));
}
private Retry retryCustomConfig(String retryName, RetryConfig config) {
RetryRegistry registry = RetryRegistry.of(config);
return registry.retry(retryName, config);
}

Related

Spring WebClient - Stop retrying if an exception is thrown in the doOnError

I have the following code to make a request that is going to be retried a max number of times. This request needs an authorization header and I'm caching this information to prevent this method to call the method to retrieve this information every time.
What I'm trying to do is:
When calling myMethod I first retrieve the login information for the service I'm calling, in most cases that will come from the cache when calling the getAuthorizationHeaderValue method.
In the web client, if the response to send this request returns a 4xx response I need to login again to the service I'm calling, before retrying the request. For that, I'm calling the tryToLoginAgain method to set the value for the header again.
After doing that the retry of the request should work now that the header has been set.
If by any chance the call to login again fails I need to stop retrying as there no use on retrying the request.
public <T> T myMethod(...) {
...
try {
AtomicReference<String> headerValue = new AtomicReference<>(loginService.getAuthorizationHeaderValue());
Mono<T> monoResult = webclient.get()
.uri(uri)
.accept(MediaType.APPLICATION_JSON)
.header(HttpHeaders.AUTHORIZATION, headerValue.get())
.retrieve()
.onStatus(HttpStatus::is4xxClientError, response -> throwHttpClientLoginException())
.bodyToMono(type)
.doOnError(HttpClientLoginException.class, e -> tryToLoginAgain(headerValue))
.retryWhen(Retry.backoff(MAX_NUMBER_RETRIES, Duration.ofSeconds(5)));
result = monoResult.block();
} catch(Exception e) {
throw new HttpClientException("There was an error while sending the request");
}
return result;
}
...
private Mono<Throwable> throwHttpClientLoginException() {
return Mono.error(new HttpClientLoginException("Existing Authorization failed"));
}
private void tryToLoginAgain(AtomicReference<String> headerValue) {
loginService.removeAccessTokenFromCache();
headerValue.set(loginService.getAuthorizationHeaderValue());
}
I have some unit tests and the happy path works fine (unauthorized the first time, try to login again and send the request again) but the scenario where the login doesn't work at all is not working.
I thought that if the tryToLoginAgain method throws an Exception that would be caught by the catch I have in myMethod but it never reaches there, it just retries the request again. Is there any way to do what I want?
So at the end I found a way of doing what I wanted and now the code looks like this:
public <T> T myMethod() {
try {
HttpHeaders headers = new HttpHeaders();
headers.setBearerAuth(getAuthorizationHeaderValue());
final RetryBackoffSpec retrySpec = Retry.backoff(MAX_NUMBER_RETRIES, Duration.ofSeconds(5))
.doBeforeRetry(retrySignal -> {
//When retrying, if this was a login error, try to login again
if (retrySignal.failure() instanceof HttpClientLoginException) {
tryToLoginAgain(headers);
}
});
Mono<T> monoResult = Mono.defer(() ->
getRequestFromMethod(httpMethod, uri, body, headers)
.retrieve()
.onStatus(HttpStatus::is4xxClientError, response -> throwHttpClientLoginException())
.bodyToMono(type)
)
.retryWhen(retrySpec);
result = monoResult.block();
} catch (Exception e) {
String requestUri = uri != null ?
uri.toString() :
endpoint;
log.error("There was an error while sending the request [{}] [{}]", httpMethod.name(), requestUri);
throw new HttpClientException("There was an error while sending the request [" + httpMethod.name() +
"] [" + requestUri + "]");
}
return result;
}
private void tryToLoginAgain(HttpHeaders httpHeaders) {
//If there was an 4xx error, let's evict the cache to remove the existing access_token (if it exists)
loginService.removeAccessTokenFromCache();
//And let's try to login again
httpHeaders.setBearerAuth(getAuthorizationHeaderValue());
}
private Mono<Throwable> throwHttpClientLoginException() {
return Mono.error(new HttpClientLoginException("Existing Authorization failed"));
}
private WebClient.RequestHeadersSpec getRequestFromMethod(HttpMethod httpMethod, URI uri, Object body, HttpHeaders headers) {
switch (httpMethod) {
case GET:
return webClient.get()
.uri(uri)
.headers(httpHeaders -> httpHeaders.addAll(headers))
.accept(MediaType.APPLICATION_JSON);
case POST:
return body == null ?
webClient.post()
.uri(uri)
.headers(httpHeaders -> httpHeaders.addAll(headers))
.accept(MediaType.APPLICATION_JSON) :
webClient.post()
.uri(uri)
.headers(httpHeaders -> httpHeaders.addAll(headers))
.accept(MediaType.APPLICATION_JSON)
.contentType(MediaType.APPLICATION_JSON)
.bodyValue(body);
case PUT:
return body == null ?
webClient.put()
.uri(uri)
.headers(httpHeaders -> httpHeaders.addAll(headers))
.accept(MediaType.APPLICATION_JSON) :
webClient.put()
.uri(uri)
.headers(httpHeaders -> httpHeaders.addAll(headers))
.accept(MediaType.APPLICATION_JSON)
.contentType(MediaType.APPLICATION_JSON)
.bodyValue(body);
case DELETE:
return webClient.delete()
.uri(uri)
.headers(httpHeaders -> httpHeaders.addAll(headers))
.accept(MediaType.APPLICATION_JSON);
default:
log.error("Method [{}] is not supported", httpMethod.name());
throw new HttpClientException("Method [" + httpMethod.name() + "] is not supported");
}
}
private String getAuthorizationHeaderValue() {
return loginService.retrieveAccessToken();
}
By using Mono.defer() I can retry on that Mono and make sure I change the headers I'll use with the WebClient. The retry spec will check if the exception was of the HttpClientLoginException type, thrown when the request gets a 4xx status code and in that case it will try to login again and set the header for the next retry. If the status code was different it will retry again using the same authorization.
Also, if there's an error when we try to login again, that will be caught by the catch and it won't retry anymore.

Spring webclient POST method - Error handling

I have a Spring webflux service which expose a POST service.
RouterFunctions
.route(POST("/api/v1/service/order").and(accept(APPLICATION_JSON)), handler::submitOrder)
Handler code
Here calling a remote service as follows.
public Mono<ServerResponse> submitOrder(final ServerRequest request) {
try {
return orderService.submitOrder(orderDto, order.getOrderId(),
setting.getClientId()).flatMap(orderNum -> {
return noContent().build();
});
} catch (WebClientException e) {
return ServerResponse.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
}
}
Service code
public Mono<ClientResponse> submitOrder(final OrderDto orderDto,
return webClient.post().uri(BASE_URL + "/api/v1/orders").contentType(MediaType.APPLICATION_XML)
.bodyValue(report).retrieve().bodyToMono(ClientResponse.class);
}
The remote service returns 201 for success and 4xx for error with the error message.
Error Message
<error>
<message context="GLOBAL">Invalid order value</message>
</error>
I am not sure, how to read the remote service error message and send that to the client. The above code catches the exception and returns 500.

How do I make Spring Security return a 500 instead of a 403 when a InternalAuthenticationServiceException is thrown

I am using Spring Security to handle auth on my RESTful-ish webservice.
The goal is to create a /login endpoint for which the user provides a username/password and which returns a JWT. I'm loosely following this guide: https://auth0.com/blog/implementing-jwt-authentication-on-spring-boot/
I've got the happy-path working; when a user provides a valid user/pass, a valid JWT is returned to them.
The problem is my error case. If my UserService.loadUserByUsername method (which is called by Spring Security in order to validate the user/pass) throws an IOException, I want Spring to return a 500 error. Instead, Spring returns a 403. I stepped through the internal Spring classes a bit, and they clearly differentiate between a AuthenticationException (which is thrown when auth fails) and a InternalAuthenticationServiceException (which is thrown when auth is unable to be completed due to an internal error). I want a 500 error returned when an InternalAuthenticationServiceException is encountered. How to I configure Spring to do this? Will I need to implement my own AuthenticationFailureHandler?
As Thomas Andolf said, it might have been best for me to simply implement an OAuth2 flow instead of a custom authentication solution. However, if you want to implement a custom authentication solution, and if you want to return specific HTTP error codes for specific errors, then you can write a custom AuthenticationFailureHandler to accomplish this. I'm not sure if this is a good solution, but it is a solution. Something like this:
class CustomAuthenticationFilter(authManager: AuthenticationManager) : AbstractAuthenticationProcessingFilter(AntPathRequestMatcher("/login", "POST")) {
init{
this.authenticationManager = authManager;
this.setAuthenticationSuccessHandler(CustomAuthenticationSuccessHandler())
this.setAuthenticationFailureHandler(CustomAuthenticationFailureHandler())
}
#Throws(AuthenticationException::class, PreAuthenticatedCredentialsNotFoundException::class)
override fun attemptAuthentication(req: HttpServletRequest, res: HttpServletResponse): Authentication {
// insert code to parse the request into a username and password
return authenticationManager.authenticate(
UsernamePasswordAuthenticationToken(
username,
password,
ArrayList())
)
}
}
class CustomAuthenticationSuccessHandler: AuthenticationSuccessHandler{
override fun onAuthenticationSuccess(request: HttpServletRequest?, response: HttpServletResponse?, authentication: Authentication?) {
// these next three lines simply verify that none of the inputs are null; this is Kotlin syntax.
request!!
response!!
authentication!!
val username = (authentication.principal as User).getUsername();
val expiration = Date(System.currentTimeMillis() + EXPIRATION_DURATION_MILLIS)
// insert code to create a JWT and write it to the response
// no need to return anything
}
}
class CustomAuthenticationFailureHandler : AuthenticationFailureHandler{
override fun onAuthenticationFailure(request: HttpServletRequest?, response: HttpServletResponse?, exception: AuthenticationException?) {
// these next two lines simply verify that none of the inputs are null; this is Kotlin syntax.
request!!
response!!
when (exception) {
is PreAuthenticatedCredentialsNotFoundException -> {
response.status = 400;
}
is AuthenticationServiceException -> {
response.status = 500;
}
else -> {
response.status = 401;
// consider adding a WWW-Authenticate header as well
}
}
}
}

Retrotif2 + RxJava sending POST request failed

I want to send POST request with Retrofit + RxJava, but it is failing and I don't know the reason. In one activity it's working, in another - don't want to work:
private void sendMerchantInfo() {
try {
String advertiserOriginalDeepLink = "https://mywebsite.com/main-1?param1=value1&param2=value2";
String urlGetParams = LinkParser.getUrlGETParams(advertiserOriginalDeepLink);
Map<Object, Object> merchantInfo = LinkParser.parseUrlGetParams(urlGetParams);
String merchantInfoJson = new Gson().toJson(merchantInfo); //{"param1":"value1","param2":"value2"}
String url = "https://api.endpoint.com/v1/system/merchant/process";
userService = this.serviceGenerator.createService(UserService.class, true);
final Observable observable = userService.sendUserInfo(
url, new RetrofitMapBody(merchantInfo))
.doOnNext(new Consumer<ResponseBody>() {
#Override
public void accept(ResponseBody responseBody) throws Exception {
//handle 200 OK.
}
})
.onErrorResumeNext((ObservableSource<? extends ResponseBody>) v ->
Crashlytics.log("Send user info attempt failed."))
.subscribeOn(Schedulers.from(threadExecutor))
.observeOn(postExecutionThread.getScheduler());
addDisposable(observable.subscribe());
}
} catch (Exception exception) {
Crashlytics.log("Send user info attempt failed. " + exception.getMessage());
}
}
I suspect that problem in this part, I am trying to send request in OnCreate() method:
.subscribeOn(Schedulers.from(threadExecutor))
.observeOn(postExecutionThread.getScheduler());
Tried to use this, but no effect:
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread());
What I am doing wrong? It always call onErrorResumeNext() It's probably something with threads because one time I got exception: networkonmainthreadexception. Please help.
Try using RxJava2 Adapter, it will save you a lot!
Step 1: Retrofit client setup
private Retrofit getRetrofitClient() {
return new Retrofit.Builder()
.baseUrl(BASE_URL)
.addConverterFactory(GsonConverterFactory.create())
.addCallAdapterFactory(RxJava2CallAdapterFactory.create()) //option 1
.addCallAdapterFactory(RxJava2CallAdapterFactory.createWithScheduler(Schedulers.newThread())) //option 2
.build();
}
Step 2: APIService interface (Example)
#GET("endpoint")
Single<ResponseModel> fetch();
Step 3: Usage
Single<ResponseModel> fetch() {
return getRetrofitClient()
.create(APIService.class)
.fetch();
}
Any non-2xx HTTP response will be wrapped in HttpException from which you can extract the status code, the status message and the full HTTP response.
Any connection errors will be wrapped in IOException
And that is all you need to do to wrap your network call in any RxJava stream.

Get API response error message using Web Client Mono in Spring Boot

I am using webflux Mono (in Spring boot 5) to consume an external API. I am able to get data well when the API response status code is 200, but when the API returns an error I am not able to retrieve the error message from the API. Spring webclient error handler always display the message as
ClientResponse has erroneous status code: 500 Internal Server Error, but when I use PostMan the API returns this JSON response with status code 500.
{
"error": {
"statusCode": 500,
"name": "Error",
"message":"Failed to add object with ID:900 as the object exists",
"stack":"some long message"
}
}
My request using WebClient is as follows
webClient.getWebClient()
.post()
.uri("/api/Card")
.body(BodyInserters.fromObject(cardObject))
.retrieve()
.bodyToMono(String.class)
.doOnSuccess( args -> {
System.out.println(args.toString());
})
.doOnError( e ->{
e.printStackTrace();
System.out.println("Some Error Happend :"+e);
});
My question is, how can I get access to the JSON response when the API returns an Error with status code of 500?
If you want to retrieve the error details:
WebClient webClient = WebClient.builder()
.filter(ExchangeFilterFunction.ofResponseProcessor(clientResponse -> {
if (clientResponse.statusCode().isError()) {
return clientResponse.bodyToMono(ErrorDetails.class)
.flatMap(errorDetails -> Mono.error(new CustomClientException(clientResponse.statusCode(), errorDetails)));
}
return Mono.just(clientResponse);
}))
.build();
with
class CustomClientException extends WebClientException {
private final HttpStatus status;
private final ErrorDetails details;
CustomClientException(HttpStatus status, ErrorDetails details) {
super(status.getReasonPhrase());
this.status = status;
this.details = details;
}
public HttpStatus getStatus() {
return status;
}
public ErrorDetails getDetails() {
return details;
}
}
and with the ErrorDetails class mapping the error body
Per-request variant:
webClient.get()
.exchange()
.map(clientResponse -> {
if (clientResponse.statusCode().isError()) {
return clientResponse.bodyToMono(ErrorDetails.class)
.flatMap(errorDetails -> Mono.error(new CustomClientException(clientResponse.statusCode(), errorDetails)));
}
return clientResponse;
})
Just as #Frischling suggested, I changed my request to look as follows
return webClient.getWebClient()
.post()
.uri("/api/Card")
.body(BodyInserters.fromObject(cardObject))
.exchange()
.flatMap(clientResponse -> {
if (clientResponse.statusCode().is5xxServerError()) {
clientResponse.body((clientHttpResponse, context) -> {
return clientHttpResponse.getBody();
});
return clientResponse.bodyToMono(String.class);
}
else
return clientResponse.bodyToMono(String.class);
});
I also noted that there's a couple of status codes from 1xx to 5xx, which is going to make my error handling easier for different cases
Look at .onErrorMap(), that gives you the exception to look at. Since you might also need the body() of the exchange() to look at, don't use retrieve, but
.exchange().flatMap((ClientResponse) response -> ....);

Resources