Throws java.lang.IllegalStateException: block()/blockFirst()/blockLast() when retrying invalid token - java

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

Related

Spring Boot web Client

I am having an issue with the web client.
I am trying to make a post request. The request is good. The thing is, if I add onStatus in order to handle http error codes, I am getting a NPE when calling bodyToMono. If I remove onStatus, I get the response.
we could take this as an example:
Employee createdEmployee = webClient.post()
.uri("/employees")
.header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
.body(Mono.just(empl), Employee.class)
.retrieve()
.onStatus(HttpStatus::isError, clientResponse -> {
if (clientResponse.statusCode() == HttpStatus.resolve(402)) {
return Mono.error(new Exception("402"));
}
log.error("Error endpoint with status code {}", clientResponse.statusCode());
if (clientResponse.statusCode() == HttpStatus.resolve(500)) {
return Mono.error(new Exception("500"));
}
if (clientResponse.statusCode() == HttpStatus.resolve(512)) {
return Mono.error(new Exception("512"));
}
return Mono.error(new Exception("Error while processing request"));
})
.bodyToMono(Employee.class);
I wan to handle 4xx and 5xx errors including their specific subtypes (404,402) 500, 512
Before bodyToMono() you can use onStatus()
.onStatus(httpStatus -> {
return httpStatus.is4xxClientError(); // handle 4xx or use .is5xxServerError() or .value()
}, clientResponse -> Mono.error(new RuntimeException("custom exception")))
Another option is to use ExchangeFilterFunction and apply it to your webclient
ExchangeFilterFunction responseErrorHandler =
ExchangeFilterFunction.ofResponseProcessor(clientResponse -> {
clientResponse.statusCode(); // handle any status manually
return Mono.just(clientResponse);
});
WebClient webClient = WebClient.builder()
.filter(responseErrorHandler)

Spring WebClient Retry Logic with new Headers

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.

How do I log out the body of a failed response to a Spring WebFlux WebClient request while returning the response to the caller?

I'm very new to reactive programming and I have a REST service that takes a request and then calls to another API using the WebFlux WebClient. When the API responds with a 4xx or 5xx response, I want to log the response body in my service, and then pass on the response to the caller. I've found a number of ways to handle logging the response, but they generally return Mono.error to the caller, which is not what I want to do. I have this almost working, but when I make the request to my service, while I get back the 4xx code that the API returned, my client just hangs waiting for the body of the response, and the service never seems to complete processing the stream. I'm using Spring Boot version 2.2.4.RELEASE.
Here's what I've got:
Controller:
#PostMapping(path = "create-order")
public Mono<ResponseEntity<OrderResponse>> createOrder(#Valid #RequestBody CreateOrderRequest createOrderRequest) {
return orderService.createOrder(createOrderRequest);
}
Service:
public Mono<ResponseEntity<OrderResponse>> createOrder(CreateOrderRequest createOrderRequest) {
return this.webClient
.mutate()
.filter(OrderService.errorHandlingFilter(ORDERS_URI, createOrderRequest))
.build()
.post()
.uri(ORDERS_URI)
.contentType(MediaType.APPLICATION_JSON)
.bodyValue(createOrderRequest)
.exchange()
.flatMap(response -> response.toEntity(OrderResponse.class));
}
public static ExchangeFilterFunction errorHandlingFilter(String uri, CreateOrderRequest request) {
return ExchangeFilterFunction.ofResponseProcessor(clientResponse -> {
if (clientResponse.statusCode() != null && (clientResponse.statusCode().is5xxServerError() || clientResponse.statusCode().is4xxClientError())) {
return clientResponse.bodyToMono(String.class)
.flatMap(errorBody -> OrderService.logResponseError(clientResponse, uri, request, errorBody));
} else {
return Mono.just(clientResponse);
}
});
}
static Mono<ClientResponse> logResponseError(ClientResponse response, String attemptedUri, CreateOrderRequest orderRequest, String responseBody) {
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
try {
log.error("Response code {} received when attempting to hit {}, request:{}, response:{}",
response.rawStatusCode(), attemptedUri, objectMapper.writeValueAsString(orderRequest),
responseBody);
} catch (JsonProcessingException e) {
log.error("Error attempting to serialize request object when reporting on error for request to {}, with code:{} and response:{}",
attemptedUri, response.rawStatusCode(), responseBody);
}
return Mono.just(response);
}
As you can see, I'm simply trying to return a Mono of the original response from the logResponseError method. For my testing, I'm submitting a body with a bad element which results in a 422 Unprocessable Entity response from the ORDERS_URI endpoint in the API I'm calling. But for some reason, while the client that called the create-order endpoint receives the 422, it never receives the body. If I change the return in the logResponseError method to be
return Mono.error(new Exception("Some error"));
I receive a 500 at the client, and the request completes. If anyone knows why it won't complete when I try to send back the response itself, I would love to know what I'm doing wrong.
Can't have your cake and eat it too!
The issue here is that you are trying to consume the body of the response twice, which is not allowed. Normally you would get an error for doing so.
Once in
return clientResponse.bodyToMono(String.class)
but also in
response.toEntity(OrderResponse.class)
which actually runs
#Override
public <T> Mono<ResponseEntity<T>> toEntity(Class<T> bodyType) {
return WebClientUtils.toEntity(this, bodyToMono(bodyType));
}
So one solution would be to process the ResponseEntity instead of the ClientResponse as follows since you don't actually want to do any reactive stuff with the body
public Mono<ResponseEntity<OrderResponse>> createOrder(CreateOrderRequest createOrderRequest) {
return this.webClient
//no need for mutate unless you already have things specified in
//base webclient?
.post()
.uri(ORDERS_URI)
.contentType(MediaType.APPLICATION_JSON)
.bodyValue(createOrderRequest)
.exchange()
//Here you map the response to an entity first
.flatMap(response -> response.toEntity(OrderResponse.class))
//Then run the errorHandler to do whatever
//Use doOnNext since there isn't any reason to return anything
.doOnNext(response ->
errorHandler(ORDERS_URI,createOrderRequest,response));
}
//Void doesn't need to return
public static void errorHandler(String uri, CreateOrderRequest request,ResponseEntity<?> response) {
if( response.getStatusCode().is5xxServerError()
|| response.getStatusCode().is4xxClientError())
//run log method if 500 or 400
OrderService.logResponseError(response, uri, request);
}
//No need for redundant final param as already in response
static void logResponseError(ResponseEntity<?> response, String attemptedUri, CreateOrderRequest orderRequest) {
//Do the log stuff
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
try {
log.error("Response code {} received when attempting to hit {}, request:{}, response:{}",
response.getStatusCodeValue(), attemptedUri, objectMapper.writeValueAsString(orderRequest),
response.getBody());
} catch (JsonProcessingException e) {
log.error("Error attempting to serialize request object when reporting on error for request to {}, with code:{} and response:{}",
attemptedUri, response.getStatusCodeValue(), response.getBody());
}
}
Note that there isn't really a reason to use the ExchangeFilter since you aren't actually doing any filtering, just performing an action based off the response

Get http response code and all available body

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

Replacing RestTemplate with WebClient

I have a controller that uses RestTemplate to get data from several rest endpoints. Since RestTemplate is blocking, my web page is taking long time to load. In order to increase the performance, I am planning to replace all my usages of RestTemplate with WebClient. One of the methods I currently have that uses RestTemplate is as below.
public List<MyObject> getMyObject(String input){
URI uri = UriComponentsBuilder.fromUriString("/someurl")
.path("123456")
.build()
.toUri();
RequestEntity<?> request = RequestEntity.get(uri).build();
ParameterizedTypeReference<List<MyObject>> responseType = new ParameterizedTypeReference<List<MyObject>>() {};
ResponseEntity<List<MyObject>> responseEntity = restTemplate.exchange(request, responseType);
MyObject obj = responseEntity.getBody();
}
Now I want to replace my above method to use WebClient but I am new to WebClient and not sure where to start. Any direction and help is appreciated.
To help you I am giving you example how we can replace restTemple with webClient. I hope you have already setup your pom.xml
Created a Configuration class.
#Slf4j
#Configuration
public class ApplicationConfig {
/**
* Web client web client.
*
* #return the web client
*/
#Bean
WebClient webClient() {
return WebClient.builder()
.filter(this.logRequest())
.filter(this.logResponse())
.build();
}
private ExchangeFilterFunction logRequest() {
return ExchangeFilterFunction.ofRequestProcessor(clientRequest -> {
log.info("WebClient request: {} {} {}", clientRequest.method(), clientRequest.url(), clientRequest.body());
clientRequest.headers().forEach((name, values) -> values.forEach(value -> log.info("{}={}", name, value)));
return Mono.just(clientRequest);
});
}
private ExchangeFilterFunction logResponse() {
return ExchangeFilterFunction.ofResponseProcessor(clientResponse -> {
log.info("WebClient response status: {}", clientResponse.statusCode());
return Mono.just(clientResponse);
});
}
}
Plus a service class calling WebClient
#Component
#RequiredArgsConstructor
public class MyObjectService {
private final WebClient webClient;
public Mono<List<Object>> getMyObject(String input) {
URI uri = UriComponentsBuilder.fromUriString("/someurl")
.path("123456")
.build()
.toUri();
ParameterizedTypeReference<List<MyObject>> responseType = new ParameterizedTypeReference<List<MyObject>>() {
};
return this.webClient
.get()
.uri(uri)
.exchange()
.flatMap(response -> response.bodyToMono(responseType));
}
}
This will give you a non blocking Mono of List<MyObject>, you can also extract body to flux by using response.bodyToFlux(responseType)
I hope this will give you a base to explore more.

Categories