I had a sense from tutorials that returning a throwable, shouldn't change method return type.
Here are my tries:
When using handle, everything is fine until I add .timeout(), then function return type is changed to Flux<Object>
private Flux<String> exampleHandle()
{
MutableHttpRequest<String> req = HttpRequest.GET("http://localhost:8080");
return httpClient.exchange(req, TokenResponse.class)
.handle((response, sink) -> {
Optional<TokenResponse> optionalBody = response.getBody();
if (optionalBody.isEmpty()) {
sink.error(new InitializationException("Failed to fetch authentication token. Body is null."));
} else {
TokenResponse tokenResponse = optionalBody.get();
String accessToken = tokenResponse.getAccessToken();
if (accessToken != null) {
sink.next(accessToken);
} else {
sink.error(new InitializationException("Failed to fetch authentication token. Authentication token is null."));
}
}
});
// .timeout(Duration.ofSeconds(10)); // Timeout changes return type to Flux<Object>
}
When using map and Flux.error (i tried Mono.error also), function return type is changed to Flux<Object> when I introduce Flux.error in map
private Flux<String> exampleMap()
{
MutableHttpRequest<String> req = HttpRequest.GET("http://localhost:8080");
return httpClient.exchange(req, TokenResponse.class)
.map(response -> {
Optional<TokenResponse> optionalBody = response.getBody();
if (optionalBody.isEmpty()) {
return Flux.error(new InitializationException("Failed to fetch authentication token. Body is null."));
} else {
TokenResponse tokenResponse = optionalBody.get();
String accessToken = tokenResponse.getAccessToken();
if (accessToken != null) {
return accessToken;
} else {
return Flux.error(new InitializationException("Failed to fetch authentication token. Authentication token is null."));
}
}
});
}
Can someone more knowledgeable explain me what am I doing wrong? Thank you!
You should move timeout before handle:
return httpClient.exchange(req, TokenResponse.class)
.timeout(Duration.ofSeconds(10))
.handle((response, sink) -> {
As for the map case, take a look at the method signature:
Flux map(Function<? super T,? extends V> mapper)
map takes a Function<T, U> and returns a Flux<U>, returning Flux.error is invalid. You could simply use throw inside map and Reactor will turn it into a proper error signal but I think handle fits better in this case.
Useful links:
Correct way of throwing exceptions with Reactor
map vs flatMap in reactor
Related
I am trying to update the token when I get a response with 401 status code.
In order to do that, I used web client. I know that this mainly used to do reactive development but since resttemplate will soon be deprecated I went for this option.
The issue I am facing is that when it does call the api endpoint to get the new token, it throws a 'java.lang.IllegalStateException: block()/blockFirst()/blockLast() '. And make sense as it stated in the exception message It is not supported in thread reactor-http-nio-3.
I saw that there is a map and flatmap option, but I couldn't figure out how to use it inside the doBeforeRetry() to make it process in a different stream.
I need to have that new token before retrying.
So the question is : How can I get the token via another call and then still do the retry ?
I was able to make it work by using a try catch but I would like to find the solution how to use it inside that retry method.
I also try to block the token request by replacing the token response by a Mono and block it by using myMono.toFuture().get() as stated here block()/blockFirst()/blockLast() are blocking error when calling bodyToMono AFTER exchange()
Here is the code :
Method responsible for the call :
public String getValueFromApi(HashMap<String, Object> filter) {
String response = "";
response = webclient
.post()
.uri(endpoint)
.header("token", token.getToken())
.bodyValue(filter)
.retrieve()
.bodyToMono(String.class)
.retryWhen(Retry.max(3).doBeforeRetry(
retrySignal -> tokenService.getTokenFromApi(env)
).filter(InvalidTokenException.class::isInstance))
.block();
return response;
}
Method that retrieve the token :
public void getTokenFromApi(Environment env) {
HashMap<String, String> requestBody = new HashMap<>();
requestBody.put("name", "name");
requestBody.put("password", "password");
String response = WebClient
.builder()
.baseUrl(BASE_PATH)
.defaultHeader(HttpHeaders.CONTENT_TYPE, "application/json")
.build()
.post()
.uri(tokenUri)
.body(BodyInserters.fromValue(requestBody))
.retrieve()
.bodyToMono(String.class)
.block();
getTokenFromResponse(response);
}
private void getTokenFromResponse(String reponse) {
JsonObject tokenObject = new Gson().fromJson(reponse, JsonObject.class);
setToken(tokenObject.get("token").getAsString());
}
WebClient Builder :
#Bean
public WebClient webClientForApi(WebClient.Builder webClientBuilder) {
return webClientBuilder
.clientConnector(new ReactorClientHttpConnector(httpClient))
.filter(errorHandler())
.filter(logRequest())
.clone()
.baseUrl(BASE_PATH)
.defaultHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
.defaultHeader(HttpHeaders.ACCEPT, "application/json")
.build();
}
public ExchangeFilterFunction errorHandler() {
return ExchangeFilterFunction.ofResponseProcessor(clientResponse -> {
if (clientResponse.statusCode().equals(HttpStatus.UNAUTHORIZED)) {
return Mono.error(InvalidTokenException::new);
} else if (clientResponse.statusCode() == HttpStatus.INTERNAL_SERVER_ERROR) {
return Mono.error(ApiInternalServerException::new);
} else {
return Mono.just(clientResponse);
}
});
}
private ExchangeFilterFunction logRequest() {
return ExchangeFilterFunction.ofRequestProcessor(clientRequest -> {
logger.info("Request: {} {}", clientRequest.method(), clientRequest.url());
clientRequest.headers().forEach((name, values) -> values.forEach(value -> logger.info("{}={}", name, value)));
return Mono.just(clientRequest);
});
}
I have implemented the error handling in a filter that looks like this:
public Mono<ClientResponse> filter(ClientRequest request, ExchangeFunction next) {
URI url = request.url();
HttpMethod method = request.method();
return next.exchange(request).flatMap(response -> {
if (response.statusCode().isError()) {
return response.bodyToMono(String.class).flatMap(responseBody -> {
Optional<Exception> exception = errorResponseHandler.handleError(method, response.statusCode(), url, responseBody);
if (exception.isPresent()) {
return Mono.error(exception.get());
} else {
// fallback
return Mono.error(new UnsupportedOperationException("The fallback functionality is still missing"));
}
});
} else {
return Mono.just(response);
}
});
}
This should work fine in the case where the response comes with a body as then the response.bodyToMono(String.class).flatMap(...) is executed. However when the body is empty nothing happens, but what I want is to also deal with the error. It is my understanding that I would do this something like this;
response.bodyToMono(String.class).flatMap(...)
.switchIfEmpty(Mono.error(new UnsupportedOperationException("The body was empty")));
This does not work as the expected type to be returned is Mono instead of Mono.
How can I achieve the handling of errors with and without response body, which is needed to construct to correct exception?
This question brought me onto the right track:
The switchIfEmpty invocation has to come before the flatMap. As there is no body, flatMap is not executed and neither is anything after, therefore the switchIfEmpty has to come first:
public Mono<ClientResponse> filter(ClientRequest request, ExchangeFunction next) {
URI url = request.url();
HttpMethod method = request.method();
return next.exchange(request).flatMap(response -> {
if (response.statusCode().isError()) {
return response.bodyToMono(String.class)
.switchIfEmpty(Mono.error(new UnsupportedOperationException("The body was empty")));
.flatMap(responseBody -> {
Optional<Exception> exception = errorResponseHandler.handleError(method, response.statusCode(), url, responseBody);
if (exception.isPresent()) {
return Mono.error(exception.get());
} else {
// fallback
return Mono.error(new UnsupportedOperationException("The fallback functionality is still missing"));
}
});
} else {
return Mono.just(response);
}
});
}
I am trying to build a retry logic using Spring WebClient. The problem that I am trying to solve is very simple. I am calling an API endpoint to get some values. If the API returns an error with say 401 response, then I will have to make call to Token service and renew my token and use the new token and make the same API call.
The general psudo is
try {
GET /locations data
} catch(401 Unauthorized) {
POST /token and get renew Token --> This is another WebClient API call With New Token
call again GET /locations and return value
} catch (Another Exception) {
throw Application Error
}
Here is the Spring code that I am trying to do and it does not look like it is working.
Any suggestion on how to do it.
public List<Location> getLocations(final User user) {
if (null == user) {
throw new ApplicationException("User cannot be null");
}
if (null == user.getHoneyWellLinkToken()) {
throw new ApplicationException(String.format("%s has not linked the account with Honeywell", user.getUsername()));
}
List<Location> locations = getLocationsAPI(user).block();
return locations;
}
private Mono<List<Location>> getLocationsAPI(final User user) {
String endpoint = config.getApi().getLocationsEndpoint()
.concat("?apikey=")
.concat(config.getCredentials().getClientId());
return WebClient.builder().baseUrl(endpoint)
.build()
.get()
.headers(httpHeaders -> httpHeaders.setBearerAuth(user.getHoneyWellLinkToken().getAccessToken()))
.retrieve()
.bodyToFlux(Location.class)
.collectList()
.doOnError(err -> {
WebClient.builder().baseUrl(endpoint)
.build()
.get()
.headers(httpHeaders -> httpHeaders.setBearerAuth(honeywellService.renewToken(user).block().getHoneyWellLinkToken().getAccessToken()))
.retrieve().bodyToFlux(Location.class);
});
}
This code is hosted on GitHub https://github.com/reflexdemon/home-use/blob/main/src/main/java/io/vpv/homeuse/service/HoneywellThermostatService.java
Use onErrorResume instead of doOnError
Do not block when renewing token
private Mono<List<Location>> getLocationsAPI(final User user) {
String endpoint = config.getApi().getLocationsEndpoint()
.concat("?apikey=")
.concat(config.getCredentials().getClientId());
return getLocations(endpoint, user)
.onErrorResume(err -> honeywellService.renewToken(user)
.flatMap(newUser -> getLocations(endpoint, newUser)));
}
private Mono<List<Location>> getLocations(String endpoint, User user) {
return WebClient.builder()
.baseUrl(endpoint)
.build()
.get()
.headers(httpHeaders -> httpHeaders.setBearerAuth(user
.getHoneyWellLinkToken()
.getAccessToken()))
.retrieve()
.bodyToFlux(Location.class)
.collectList();
}
Also, it's a good idea to use a single WebClient instance instead of building a new one for each request.
I'm developing a Java application (using Spring Boot) and I need some help:
This application receives as input a JWT token which I process in a method. The method for now is the following:
private UsernamePasswordAuthenticationToken myMethod(HttpServletRequest request) {
// Code that gets the 'token' String
try{
Map<String, String> registry = ((Map<String, String>) (token.getBody().get("registry")));
String sub = ((String) (parsedToken.getBody().get("sub")));
UsernamePasswordAuthenticationToken finalToken = new UsernamePasswordAuthenticationToken(sub, null, null);
return finalToken;
} catch (ExpiredJwtException exception) { // Only here I have the certainty that the token has expired!
// Code that handles the exception
}
}
However, I need to implement a logic that must check in several places whether the token obtained has expired or not, without running this method every time. The only way I have to know if token has expired is the exception raised by ExpiredJwtException.
Is there any way to know if the token has expired without going through the catched exception? For example, it would be very useful if there was a "token" class that has an .isExpired attribute, or something like that.
I don't want to go into handling the exception because it means that I would always depend on the (long) code of the try block every time I need to check if a token has expired or not, and I don't want it to be.
If you use a different JWT library, you can do it easily enough. The auth0 JWT library has methods to parse and, optionally, verify the token:
import com.auth0.jwt.JWT;
import com.auth0.jwt.exceptions.JWTDecodeException;
import com.auth0.jwt.interfaces.DecodedJWT;
DecodedJWT jwt = JWT.decode(token);
if( jwt.getExpiresAt().before(new Date())) {
System.out.println("token is expired");
}
For this you can create another class for handling the token validation like below, in the class you will handle the ExpiredJwtException and give the extracted value back , other exceptions will throw it from the class
public class TokenValidator {
Boolean tokenExpired = true;
public boolean isTokenValid(String token) throws Exception {
validateToken(token);
return tokenExpired;
}
public Map<String,String> getExtractedData(String token) throws Exception {
return validateToken(token);
}
private Map<String, String> validateToken (String token) throws Exception{
try {
Claims claims = Jwts.parser().setSigningKey("jwtSecretKey")
.parseClaimsJws(token).getBody();
tokenExpired = false;
return getClaimsInMap(claims);
} catch (ExpiredJwtException ex) {
DefaultClaims claims = (DefaultClaims) ex.getClaims();
return getClaimsInMap(claims);
} catch (Exception e) {
throw new Exception(e);
}
}
private Map<String,String> getClaimsInMap(Claims claims) {
Map<String,String> expectedMap = new HashMap<>();
claims.entrySet().stream().forEach(entry -> expectedMap.put(entry.getKey(),entry.getValue().toString()));
return expectedMap;
}
}
This can be achieved by using claims. Below sample code can help.
// Get Expiration and compare it with new Date()
public boolean isTokenExpired(String token) {
return extractExpiration(token).before(new Date());
}
public Date extractExpiration(String token) {
return extractClaim(token, Claims::getExpiration);
}
public <T> T extractClaim(String token , Function<Claims, T> claimResolver) {
final Claims claim= extractAllClaims(token);
return claimResolver.apply(claim);
}
private Claims extractAllClaims(String token) {
return Jwts.parser().setSigningKey(SECRET_KEY).parseClaimsJws(token).getBody();
}
I want ot implement WebFlux example client which can make request with http params and get the response body and http response code. I tried this:
public ClientResponse execute(NotificationMessage nm)
Mono<String> transactionMono = Mono.just(convertedString);
return client.post().uri(builder -> builder.build())
.header(HttpHeaders.USER_AGENT, "agent")
.body(transactionMono, String.class).exchange().block();
}
private static String convert(Map<String, String> map) throws UnsupportedEncodingException {
String result = map.entrySet().stream().map(e -> encode(e.getKey()) + "=" + encode(e.getValue()))
.collect(Collectors.joining("&"));
return result;
}
private static String encode(String s) {
try {
return URLEncoder.encode(s, "UTF-8");
} catch (UnsupportedEncodingException e) {
throw new IllegalStateException(e);
}
}
Can you give me some advice after .exchange() how I can get the http status code and all available body.
From the ClientResponse object returned by exchange you can use response.statusCode() to get the status and use response.bodyToMono() or bodyToFlux() to get the actual body. You should avoid using .block() in reactive programming and use .subscribe() or .flatMap() or other operators to get the data from Mono or Flux objects. Read more about reactive programming and Project reactor (used by spring webflux) here.
For eg:
public Mono<Data> execute(NotificationMessage nm)
return client.post().uri(builder -> builder.build())
.header(HttpHeaders.USER_AGENT, "agent")
.body(transactionMono, String.class).exchange()
.flatMap(response -> {
HttpStatus code = response.statusCode();
Data data = response.bodyToMono(Data.class);
return data;
});
}