Retry request using webclient in case of 401 error in spring boot - spring

I have Api calls which uses OAUTH token this auth tokens are specific to different user and have a expiry period of 24 hrs. But sometimes even if the expire time is not up when the api call is made it returns invalid token. Is their a way in which I can retry the api call one more time in the above scenerio by getting the new user access token. The user access token is also got by making an api call. I am using Java reactive webclient with spring boot.
public Mono<Abc> create(Long customerId, Abc abc) {
return profileRepo.findByCustomerId(customerId)
.map(profile -> refreshTokens(customerId)
.flatMap(tokens ->
client.create(token,getProfile(customerId))))
.orElseThrow(ResourceNotFoundException::new);
}
public Mono<Token> refreshTokens(final Long customerId) {
Token token = service.findByCustomerId(customerId);
if (LocalDateTime.now().isAfter(token.getExpiryTime())) {
newToken = client.refresh(token);
}
return newToken;
}
Api call for token refresh and create
public Mono<Token> refresh(final Token token) {
return client.post()
.uri(OAUTH_TOKEN_PATH)
.header(AUTHORIZATION, basicAuth()) // The master token of the service provider
.body(forRefreshToken(new RefreshToken(token.getRefreshToken())))
.retrieve()
.onStatus(HttpStatus::is4xxClientError, response -> response.bodyToMono(String.class)
.flatMap(error -> Mono.error(new ClientResponseException(response.statusCode().value(),response.statusCode(),error))))
.onStatus(HttpStatus::is5xxServerError, response -> response.bodyToMono(String.class)
.flatMap(error -> Mono.error(new ClientResponseException(response.statusCode().value(),response.statusCode(),error))))
.bodyToMono(Token.class);
}
public Mono<Abc> create(final Token token, Profile pro) {
return client.post()
.uri(PATH_V2)
.header(AUTHORIZATION, token.bearer())
.contentType(APPLICATION_JSON)
.body(fromObject(pro))
.retrieve()
.onStatus(HttpStatus::is4xxClientError, response -> response.bodyToMono(String.class)
.flatMap(error -> Mono.error(new ClientResponseException(response.statusCode().value(),response.statusCode(),error))))
.onStatus(HttpStatus::is5xxServerError, response -> response.bodyToMono(String.class)
.flatMap(error -> Mono.error(new ClientResponseException(response.statusCode().value(),response.statusCode(),error))))
.bodyToMono(Abc.class);
}
Thanks in advance,
Sameekshya

Related

Keycloak API using admin user

I'm working with keycloak API to access offline user's sessions; I noticed a strange behavior and thus my question:
a. When I use postman, I get the access token with this url: http://localhost:8080/realms/master/protocol/openid-connect/token
b. From the above, I use said token in postman to retrieve the offline sessions:
http://localhost:8080/admin/realms/master/clients/5729288b-c789-45ac-8915-da32b7b9fe49/offline-sessions
where '5729288b-c789-45ac-8915-da32b7b9fe49' is the admin-cli ID; username and password are all the defaults of the admin user and the client is 'admin-cli'
Everything works fine in postman, and I'm able to retrieve the offline sessions. However, when I do the same with the Keycloak API using the springboot webclient I get 403 Forbidden
a. Get the token from the below:
private String getToken(){
MultiValueMap<String, String> map = new LinkedMultiValueMap<>();
map.add("username", username);
map.add("password", password);
map.add("client_id", clientId);
map.add("grant_type", grantType);
map.add("scope", "openid");
ResponseEntity<LoginResponse> loginResponse = webclient.post()
.uri(uriBuilder -> UriBuilder.fromUri(tokenEndpoint).build())
.contentType(MediaType.APPLICATION_JSON)
.body(BodyInserters.fromFormData(map))
.retrieve()
.toEntity(LoginResponse.class)
.block();
return loginResponse.getBody().getAccess_token();
}
b. Try to retrieve offline sessions with the above access-token
public UserSessionRepresentation[] getMasterOfflineSessions(){
UserSessionRepresentation[] response = webclient.get()
.uri(uriBuilder -> UriBuilder.fromUri(offlineSessionsUrl)
.build(cliId))
.headers(h -> h.setBearerAuth(getToken()))
.retrieve()
.bodyToMono(UserSessionRepresentation[].class)
.block();
return response;
}
offlineSessionsUrl is: http://localhost:8080/admin/realms/master/clients/5729288b-c789-45ac-8915-da32b7b9fe49/offline-sessions
5729288b-c789-45ac-8915-da32b7b9fe49:is the id for the admin-cli client
What I don't understand is that I can retrieve the sessions in postman, but I can't do so using the API and the springboot webclient with all configurations being equal.
Please help
Answering my own question; the issue here was was the: webclient spring property
In springboot, it was using the definition within the configuration that pointed to another client. To make it work for the admin-cli client, I had to use a clean object of webclient as illustrated in the below code:
public UserSessionRepresentation[] getMasterOfflineSessions(){
UserSessionRepresentation[] response = WebClient.create().get()
.uri(uriBuilder -> UriBuilder.fromUri(offlineSessionsUrl)
.build(cliId))
.headers(h -> h.setBearerAuth(getToken()))
.retrieve()
.bodyToMono(UserSessionRepresentation[].class)
.block();
return response;
}
The WebClient.create() is the piece of code I changed to resolve the issue

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.

Stackoverflow when retrieving jwt token in WebTestClient and seting it in ExchangeFilterFunction

The latest Spring Boot 2.3.1.RELEASE, Java 11.
private ExchangeFilterFunction userJwtAuthentication() {
return ExchangeFilterFunction.ofRequestProcessor(
request -> generateToken("user")
.map(jwt -> ClientRequest.from(request)
.headers(headers -> headers.setBearerAuth(jwt))
.build()
)
);
}
private Mono<String> generateToken(String username) {
return this.client
.post().uri("/auth/login")
.bodyValue(AuthenticationRequest.builder().username(username).password("password").build())
.exchange()
.returnResult(new ParameterizedTypeReference<Map<String, String>>() {
})
.getResponseBody()
.last()
.map(d -> d.get("access_token"))
.doOnSubscribe(
jwt -> log.debug("generated jwt token::" + jwt)
);
}
And use it in tests
client.mutate().filter(userJwtAuthentication()).build()
When testing my APIs, it returns.
ava.lang.StackOverflowError
at java.base/java.util.regex.Pattern$Branch.match(Pattern.java:4749)
at java.base/java.util.regex.Pattern$GroupTail.match(Pattern.java:4863)
at java.base/java.util.regex.Pattern$CharPropertyGreedy.match(Pattern.java:4306)
at java.base/java.util.regex.Pattern$GroupHead.match(Pattern.java:4804)
at java.base/java.util.regex.Pattern$Branch.match(Pattern.java:4747)
at java.base/java.util.regex.Pattern$Branch.match(Pattern.java:4747)
at java.base/java.util.regex.Pattern$Begin.match(Pattern.java:3683)
at java.base/java.util.regex.Matcher.match(Matcher.java:1756)
at java.base/java.util.regex.Matcher.matches(Matcher.java:713)
at org.springframework.web.util.UriComponentsBuilder.fromUriString(UriComponentsBuilder.java:215)
at org.springframework.web.util.DefaultUriBuilderFactory$DefaultUriBuilder.initUriComponentsBuilder(DefaultUriBuilderFactory.java:242)
at org.springframework.web.util.DefaultUriBuilderFactory$DefaultUriBuilder.<init>(DefaultUriBuilderFactory.java:233)
at org.springframework.web.util.DefaultUriBuilderFactory.uriString(DefaultUriBuilderFactory.java:160)
at org.springframework.web.util.DefaultUriBuilderFactory.expand(DefaultUriBuilderFactory.java:153)
at org.springframework.web.reactive.function.client.DefaultWebClient$DefaultRequestBodyUriSpec.uri(DefaultWebClient.java:176)
at org.springframework.web.reactive.function.client.DefaultWebClient$DefaultRequestBodyUriSpec.uri(DefaultWebClient.java:151)
at org.springframework.test.web.reactive.server.DefaultWebTestClient$DefaultRequestBodyUriSpec.uri(DefaultWebTestClient.java:163)
at org.springframework.test.web.reactive.server.DefaultWebTestClient$DefaultRequestBodyUriSpec.uri(DefaultWebTestClient.java:146)
at com.example.demo.IntegrationTests.generateToken(IntegrationTests.java:246)
at com.example.demo.IntegrationTests.lambda$adminJwtAuthentication$5(IntegrationTests.java:236)
at org.springframework.web.reactive.function.client.ExchangeFilterFunction.lambda$ofRequestProcessor$3(ExchangeFilterFunction.java:79)
at org.springframework.web.reactive.function.client.ExchangeFilterFunction.lambda$apply$2(ExchangeFilterFunction.java:68)
at org.springframework.web.reactive.function.client.DefaultWebClient$DefaultRequestBodyUriSpec.lambda$exchange$0(DefaultWebClient.java:338)
at reactor.core.publisher.MonoDefer.subscribe(MonoDefer.java:44)
at reactor.core.publisher.Mono.subscribe(Mono.java:4219)
at reactor.core.publisher.Mono.block(Mono.java:1702)
at org.springframework.test.web.reactive.server.DefaultWebTestClient$DefaultRequestBodyUriSpec.exchange(DefaultWebTestClient.java:307)
...
If I used blockLast to retrieve token firstly, it worked., my question is how to use Reactor API to do the same work.
The complete codes is here.

I need to fetch the auth token and set it in the header

I'm new to Spring boot and reactive programming.
I'm using spring webflux webclient for an external api service. I need to fetch the auth token and set it in the header
WebClient.builder()
.baseUrl(baseUrl)
.filter((request, next) -> {
return next.exchange(request)
.flatMap((Function<ClientResponse, Mono<ClientResponse>>) clientResponse -> {
if (clientResponse.statusCode().value() == 401) {
return authenticate().map(token -> {
Token accessToken = authenticate().block();
ClientRequest retryRequest = ClientRequest.from(request).header("Authorisation", "Bearer " + accessToken.getAccessToken()).build();
return next.exchange(retryRequest);
}).
} else {
return Mono.just(clientResponse);
}
});
})
.defaultHeader("Authorization", "Bearer " + authToken.getAccessToken())
.build();
private Mono<Token> authenticate() {
MultiValueMap<String, String> params = new LinkedMultiValueMap();
params.add("client_id", clientId);
params.add("client_secret", clientSecret);
params.add("grant_type", "password");
params.add("username", username);
params.add("password", password);
WebClient client = WebClient.create(baseUrl);
return client
.post()
.uri(tokenUri)
.accept(MediaType.APPLICATION_JSON)
.contentType(MediaType.APPLICATION_FORM_URLENCODED)
.syncBody(params)
.retrieve()
.bodyToMono(Token.class);
}
private static class Token {
#JsonProperty("access_token")
private String accessToken;
public String getAccessToken() { return accessToken; }
}
During the application startup, I'll fetch the access token and set it in the webclient builder. I've created a filter to handle authentication failures after token expiry. But the above code throws error because I've used block() which is not supposed to be used in a reactor thread. How else can I handle it? I'm using oauth2 resource owner password grant flow. Is there is any other way to handle the flow?
Hi I had the same issue (Adding a retry all requests of WebClient) which looks like you have reused.
but here flatmap is your friend, if you have a Mono<Mono<T>> you can flatten it with flatMap
builder.baseUrl("http://localhost:8080")
//sets the header before the exchange
.filter(((request, next) -> tokenProvider.getAccessToken()
.map(setBearerTokenInHeader(request))
.flatMap(next::exchange)))
//do the exchange
.filter((request, next) -> next.exchange(request)
.flatMap(clientResponse -> {
if (clientResponse.statusCode().value() == 401) {
//If unauthenicated try again
return authenticate()
.flatMap(Token::getAccessToken)
.map(setBearerTokenInHeader(request))
.flatMap(next::exchange);
} else {
return Mono.just(clientResponse);
}
}))
.build();
private Function<String, ClientRequest> setBearerTokenInHeader(ClientRequest request) {
return token -> ClientRequest.from(request).header("Bearer ", token).build();
}
I know this is an old thread, but I could not find any other working examples for the initial question
Basically, I was not able to write a working code from the above examples...
With the main task: Use WebClient instance to get protected resource by providing Bearer token. The Bearer token can be requested by a separate request.
The Mono authenticate() should work fine to get a new token.
WebClient client2 = WebClient.builder()
.baseUrl(SERVER_URL)
.filter((request, next) -> {
return next.exchange(request)
.flatMap( clientResponse -> {
if (clientResponse.statusCode().value() == 401) {
return authenticate().map(token -> {
Token accessToken = authenticate().block();
ClientRequest retryRequest = ClientRequest.from(request).header("Authorisation", "Bearer " + accessToken.getAccessToken()).build();
return next.exchange(retryRequest);
});
} else {
return Mono.just(clientResponse);
}
});
})
.defaultHeader("Authorization", "Bearer " + token.getAccessToken())
.build();
For the above example was not able to replace the ".block()" with flatMap()
And the second example
WebClient client3 = WebClient.builder().baseUrl("http://localhost:8080")
//sets the header before the exchange
.filter(((request, next) -> tokenProvider.getAccessToken()
.map(setBearerTokenInHeader(request))
.flatMap(next::exchange)))
//do the exchange
.filter((request, next) -> next.exchange(request)
.flatMap(clientResponse -> {
if (clientResponse.statusCode().value() == 401) {
//If unauthenicated try again
return authenticate()
.flatMap(Token::getAccessToken)
.map(setBearerTokenInHeader(request))
.flatMap(next::exchange);
} else {
return Mono.just(clientResponse);
}
}))
.build();
Not sure what is the "tokenProvider.getAccessToken()" and ".flatMap(Token::getAccessToken)" won't accept
Due to
class Token {
String token = "";
public String getAccessToken() { return token; }
}
Sorry I'm new to this. If you had a working example please share in this thread

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