How to handle OAuth webclient exceptions - java

I am pretty new to Spring and have been trying to catch unauthorized exceptions when authentication to a server by OAUTH. I don't understand why the method handleResponseError() doesn't catch the exception.
The stacktrace I get is:
org.springframework.security.oauth2.client.ClientAuthorizationException: [invalid_client] Client authentication failed
at org.springframework.security.oauth2.client.ClientCredentialsReactiveOAuth2AuthorizedClientProvider.lambda$authorize$0(ClientCredentialsReactiveOAuth2AuthorizedClientProvider.java:82)
Suppressed: reactor.core.publisher.FluxOnAssembly$OnAssemblyException:
Error has been observed at the following site(s):
|_ checkpoint ⇢ Request to GET https://localhost:8181/catalog/NL/brands?language_code=nl [DefaultWebClient]
Stack trace:
at org.springframework.security.oauth2.client.ClientCredentialsReactiveOAuth2AuthorizedClientProvider.lambda$authorize$0(ClientCredentialsReactiveOAuth2AuthorizedClientProvider.java:82)
at reactor.core.publisher.Mono.lambda$onErrorMap$29(Mono.java:3272)
at reactor.core.publisher.Mono.lambda$onErrorResume$31(Mono.java:3362)
at reactor.core.publisher.FluxOnErrorResume$ResumeSubscriber.onError(FluxOnErrorResume.java:88)
at reactor.core.publisher.FluxHide$SuppressFuseableSubscriber.onError(FluxHide.java:132)
at reactor.core.publisher.MonoFlatMap$FlatMapMain.secondError(MonoFlatMap.java:185)
at reactor.core.publisher.MonoFlatMap$FlatMapInner.onError(MonoFlatMap.java:251)
at reactor.core.publisher.FluxMapFuseable$MapFuseableSubscriber.onError(FluxMapFuseable.java:134)
at reactor.core.publisher.FluxMapFuseable$MapFuseableSubscriber.onError(FluxMapFuseable.java:134)
at reactor.core.publisher.MonoFlatMap$FlatMapMain.onNext(MonoFlatMap.java:135)
at reactor.core.publisher.FluxMap$MapSubscriber.onNext(FluxMap.java:114)
at reactor.core.publisher.FluxSwitchIfEmpty$SwitchIfEmptySubscriber.onNext(FluxSwitchIfEmpty.java:67)
at reactor.core.publisher.FluxOnErrorResume$ResumeSubscriber.onNext(FluxOnErrorResume.java:73)
at reactor.core.publisher.Operators$MonoSubscriber.complete(Operators.java:1782)
at reactor.core.publisher.MonoFlatMap$FlatMapMain.onNext(MonoFlatMap.java:144)
The code to authenticate to the server:
#Bean
public WebClient myClient() {
InMemoryReactiveClientRegistrationRepository clientRegistryRepo = new InMemoryReactiveClientRegistrationRepository(getClientRegistration());
InMemoryReactiveOAuth2AuthorizedClientService clientService = new InMemoryReactiveOAuth2AuthorizedClientService(clientRegistryRepo);
AuthorizedClientServiceReactiveOAuth2AuthorizedClientManager authorizedClientManager = new AuthorizedClientServiceReactiveOAuth2AuthorizedClientManager(clientRegistryRepo, clientService);
ServerOAuth2AuthorizedClientExchangeFilterFunction oauthFilter = new ServerOAuth2AuthorizedClientExchangeFilterFunction(authorizedClientManager);
oauthFilter.setDefaultClientRegistrationId(OAUTH_PROVIDER_NAME);
return WebClient.builder()
.clientConnector(new JettyClientHttpConnector(createHttpClient()))
.exchangeStrategies(getMaxMessageInMemorySize(maxInMemorySize))
.baseUrl(baseURL)
.filter(oauthFilter)
.filter(handleResponseError())
.build();
}
private static ExchangeFilterFunction handleResponseError() {
return ExchangeFilterFunction.ofResponseProcessor(
response -> response.statusCode().isError() ?
response.bodyToMono(String.class)
.flatMap(errorBody -> Mono.error(new MyUnAuthorizedRequestException(response.statusCode().name(), errorBody, ""))) :
Mono.just(response));
}
I have looked at various examples:
How to set the access token once during the instanciation of the webClient in spring webflux?
https://www.baeldung.com/spring-webclient-oauth2
I catch all the other exceptions using #ControllerAdvice. Is that the correct way to handle this?

I found the solution:
.... omitted
ServerOAuth2AuthorizedClientExchangeFilterFunction oauthFilter = new
ServerOAuth2AuthorizedClientExchangeFilterFunction(authorizedClientManager);
oauthFilter.setDefaultClientRegistrationId(OAUTH_PROVIDER_NAME);
return WebClient.builder()
.clientConnector(new JettyClientHttpConnector(createHttpClient()))
.exchangeStrategies(getMaxMessageInMemorySize(maxInMemorySize))
.baseUrl(baseURL)
.filter(oauthFilter)
.build();
}
private ReactiveOAuth2AuthorizationFailureHandler getReactiveOAuth2AuthorizationFailureHandler() {
final ReactiveOAuth2AuthorizationFailureHandler reactiveOAuth2AuthorizationFailureHandler = (authorizationException, principal, attributes) -> {
if (authorizationException instanceof ClientAuthorizationException) {
ClientAuthorizationException clientAuthorizationException = (ClientAuthorizationException)authorizationException;
return Mono.error(new MyUnAuthorizedRequestException("401","Could not authorize client", clientAuthorizationException.getMessage()));
} else {
return Mono.empty();
}
};
return reactiveOAuth2AuthorizationFailureHandler;
}

Related

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

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

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 Boot WebClient Connection and Read Timeout

Currently my post and get requests are handled through WebClients which has a common connection and read timeout in Spring Boot. I have 5 different classes each requiring its own set of connection and read timeout. I don't want to create 5 different WebClients, rather use the same Webclient but while sending a post or a get request from a particular class, specify the required connection and read timeout. Is there any way to implement this?
My current WebClient:
#Bean
public WebClient getWebClient(WebClient.Builder builder){
HttpClient httpClient = HttpClient.newConnection()
.tcpConfiguration(tcpClient -> {
tcpClient = tcpClient.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, connectionTimeout*1000);
tcpClient = tcpClient.doOnConnected(conn -> conn
.addHandlerLast(new ReadTimeoutHandler(readTimeout, TimeUnit.SECONDS)));
return tcpClient;
}).wiretap(true);
ClientHttpConnector connector = new ReactorClientHttpConnector(httpClient);
return builder.clientConnector(connector).build();
}
A post request I'm using:
public WebClientResponse httpPost(String endpoint, String requestData, Map<String, Object> requestHeader) {
ClientResponse res = webClient.post().uri(endpoint)
.body(BodyInserters.fromObject(requestData))
.headers(x -> {
if(requestHeader != null && !requestHeader.isEmpty()) {
for (String s : requestHeader.keySet()) {
x.set(s, String.valueOf(requestHeader.get(s)));
}
}
})
.exchange()
.doOnSuccess(x -> log.info("response code = " + x.statusCode()))
.block();
return convertWebClientResponse(res);
}
You can configure request-level timeout in WebClient.
webClient.get()
.uri("https://baeldung.com/path")
.httpRequest(httpRequest -> {
HttpClientRequest reactorRequest = httpRequest.getNativeRequest();
reactorRequest.responseTimeout(Duration.ofSeconds(2));
});
Now what you can do is that based on the request you can add those values either from the properties file or you can hard code them.
Reference:- https://www.baeldung.com/spring-webflux-timeout
You can try timeout with webClient like below,
webClient.post()
.uri(..)
.body(..)
.retrieve()
.
.
.
.timeout(Duration.ofMillis(30);
30 is just example.
#Bean
public WebClient getWebClient() {
HttpClient httpClient = HttpClient.create()
.tcpConfiguration(client ->
client.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 4000)
.doOnConnected(conn -> conn
.addHandlerLast(new ReadTimeoutHandler(4))
.addHandlerLast(new WriteTimeoutHandler(4))));
ClientHttpConnector connector = new ReactorClientHttpConnector(httpClient.wiretap(true));
return WebClient.builder()
.clientConnector(connector)
.defaultHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE) // if you need a default header
.build();
}
or you can use what #Amol has suggested

Spring Reactive Web - Exceptions are always wrapped in 500

With a very simple project, error handling is unclear.
public class WebfluxApplication {
public static void main(String[] args) {
SpringApplication.run(WebfluxApplication.class, args);
}
}
public class EndpointRouter
{
#Bean
public WebClient webClient() {
return WebClient.builder().build();
}
#Bean
public RouterFunction<ServerResponse> goodRoute(Handler handler, ConfigProperties configProperties) {
log.info(configProperties.toString());
return
RouterFunctions.route(RequestPredicates.GET("/api/v1/integration/ok"),
handler::goodEndpoint)
.and(
RouterFunctions.route(RequestPredicates.GET("/api/v1/integration/notfound") {
handler::badEndpoint));
}
public Mono<ServerResponse> goodEndpoint(ServerRequest r) {
return ServerResponse.ok().build();
}
public Mono<ServerResponse> badEndpoint(ServerRequest r) {
var result = service.badEndpoint();
return ServerResponse
.ok()
.body(result, String.class);
}
public class Service
{
private final WebClient webClient;
private final ConfigProperties configProperties;
public Service(WebClient webClient, ConfigProperties configurationProperties) {
this.webClient = webClient;
this.configProperties = configurationProperties;
}
public Mono<String> badEndpoint() {
return webClient
.get()
.uri(configProperties.getNotfound())
.retrieve()
.onStatus(HttpStatus::is4xxClientError, clientResponse -> {
if(clientResponse.statusCode().equals(HttpStatus.NOT_FOUND)){
return Mono.error(new HttpClientErrorException(HttpStatus.NOT_FOUND,
"Entity not found."));
} else {
return Mono.error(new HttpClientErrorException(HttpStatus.INTERNAL_SERVER_ERROR));
}
})
.bodyToMono(String.class);
}
From reading the docs, I shouldn't need to set up a Global error handler for the entire project, I should be able to handle the 404, and return a 404 back to the original caller.
This is the output
2020-08-29 16:52:46.301 ERROR 25020 --- [ctor-http-nio-4] a.w.r.e.AbstractErrorWebExceptionHandler : [b339763e-1] 500 Server Error for HTTP GET "/api/v1/integration/notfound"
org.springframework.web.client.HttpClientErrorException: 404 Entity not found.
at com.stevenpg.restperformance.webflux.Service.lambda$badEndpoint$0(Service.java:30) ~[main/:na]
Suppressed: reactor.core.publisher.FluxOnAssembly$OnAssemblyException:
Error has been observed at the following site(s):
|_ checkpoint ⇢ 404 from GET https://httpbin.org/status/404 [DefaultWebClient]
|_ checkpoint ⇢ Handler com.stevenpg.restperformance.webflux.EndpointRouter$$Lambda$445/0x00000008003ae040#7799b58 [DispatcherHandler]
|_ checkpoint ⇢ HTTP GET "/api/v1/integration/notfound" [ExceptionHandlingWebHandler]
I've also tried using onErrorResume on my Mono from the service, but it never works correct and requires a return of Mono rather than Mono.
The documentation and Stack Overflow don't have many/any examples of making a WebClient call inside a RouterFunction and handling different types of responses.
Just adding onErrorResume solves your problem. Otherwise error is handled in AbstractErrorWebExceptionHandler.
return webClient
.get()
.uri(configProperties.getNotfound())
.retrieve()
.onStatus(HttpStatus::is4xxClientError, clientResponse -> {
if(clientResponse.statusCode().equals(HttpStatus.NOT_FOUND)){
return Mono.error(new HttpClientErrorException(HttpStatus.NOT_FOUND,
"Entity not found."));
} else {
return Mono.error(new HttpClientErrorException(HttpStatus.INTERNAL_SERVER_ERROR));
}
})
.bodyToMono(String.class)
.onErrorResume( e -> Mono.just(e.getMessage()) );

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