Spring WebClient perform https call - spring-boot

Does anyone know how to configure WebClient in order to make an HTTPS endpoint?
My config looks like that:
#Bean
#NonNull
public WebClient webClient() throws SSLException {
final SslContext context = SslContextBuilder.forClient()
.trustManager(InsecureTrustManagerFactory.INSTANCE)
.build();
final HttpClient httpClient = HttpClient.create().secure(t -> t.sslContext(context));
return WebClient
.builder()
.exchangeStrategies(ExchangeStrategies.builder()
.codecs(configurer -> configurer.defaultCodecs().maxInMemorySize(16 * 1024 * 1024))
.build())
.defaultHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
.clientConnector(new ReactorClientHttpConnector(httpClient))
.build();
}
here is the method witch hits HTTPS endpoint
#Nullable
public AccessToken getAccessToken() {
return webClient
.post()
.uri(uriBuilder -> uriBuilder.path(authUrl)
.queryParam("username", username)
.queryParam("password", password)
.queryParam("client_id", clientId)
.queryParam("client_secret", clientSecret)
.queryParam("grant_type", "password")
.build())
.header("Content-Type", MediaType.APPLICATION_JSON_VALUE)
.exchange()
.flatMap(response -> {
//Error handling
if (response.statusCode().isError()) {
logger.error("error occured while authentication: {}", response.statusCode());
return response.createException().flatMap(Mono::error);
}
return response.bodyToMono(AccessToken.class);
})
.subscribeOn(Schedulers.elastic())
.block();
}
and that's my reponse, unfortunately I'm not allowed to show all the details cause there are secured data.
So I've checked everything like URL, parameters, all looks fine. Also if do the same with restTemaple it works.
Caused by: java.net.UnknownHostException: https:
at java.base/java.net.InetAddress$CachedAddresses.get(InetAddress.java:798)
Suppressed: reactor.core.publisher.FluxOnAssembly$OnAssemblyException:
Error has been observed at the following site(s):
|_ checkpoint ⇢ Request to POST https:/<here goes secured endpoint with query parameters>

Related

Handling errors from Spring WebClient in another method

In a Spring Boot application, I'm using WebClient to invoke a POST request to a remote application. The method currently looks like this:
// Class A
public void sendNotification(String notification) {
final WebClient webClient = WebClient.builder()
.defaultHeader(CONTENT_TYPE, APPLICATION_JSON_VALUE)
.build();
webClient.post()
.uri("http://localhost:9000/api")
.body(BodyInserters.fromValue(notification))
.retrieve()
.onStatus(HttpStatus::isError, clientResponse -> Mono.error(NotificationException::new))
.toBodilessEntity()
.block();
log.info("Notification delivered successfully");
}
// Class B
public void someOtherMethod() {
sendNotification("test");
}
The use case is: A method in another class calls sendNotification and should handle any error, i.e. any non 2xx status or if the request couldn't even be sent.
But I'm struggling with the concept of handling errors in the WebClient. As far as I understood, the following line would catch any HTTP status other than 2xx/3xx and then return a Mono.error with the NotificationException (a custom exception extending Exception).
onStatus(HttpStatus::isError, clientResponse -> Mono.error(NotificationException::new))
But how could someOtherMethod() handle this error scenario? How could it process this Mono.error? Or how does it actually catch the NotificationException if sendNotification doesn't even throw it in the signature?
Well, there are many ways to handle errors, it really depends on what you want to do in case of an error.
In your current setup, the solution is straightforward: first, NotificationException should extend RuntimeException, thus, in case of an HTTP error, .block() will throw a NotificationException. It is a good practice to add it in the signature of the method, accompanied with a Javadoc entry.
In another method, you just need to catch the exception and do what you want with it.
/**
* #param notification
* #throws NotificationException in case of a HTTP error
*/
public void sendNotification(String notification) throws NotificationException {
final WebClient webClient = WebClient.builder()
.defaultHeader(CONTENT_TYPE, APPLICATION_JSON_VALUE)
.build();
webClient.post()
.uri("http://localhost:9000/api")
.body(BodyInserters.fromValue(notification))
.retrieve()
.onStatus(HttpStatus::isError, clientResponse -> Mono.error(NotificationException::new))
.toBodilessEntity()
.block();
log.info("Notification delivered successfully");
}
public void someOtherMethod() {
try {
sendNotification("test");
} catch (NotificationException e) {
// Treat exception
}
}
In a more reactive style, you could return a Mono and use onErrorResume().
public Mono<Void> sendNotification(String notification) {
final WebClient webClient = WebClient.builder()
.defaultHeader(CONTENT_TYPE, APPLICATION_JSON_VALUE)
.build();
return webClient.post()
.uri("http://localhost:9000/api")
.body(BodyInserters.fromValue(notification))
.retrieve()
.onStatus(HttpStatus::isError, clientResponse -> Mono.error(NotificationException::new))
.bodyToMono(Void.class);
}
public void someOtherMethod() {
sendNotification("test")
.onErrorResume(NotificationException.class, ex -> {
log.error(ex.getMessage());
return Mono.empty();
})
.doOnSuccess(unused -> log.info("Notification delivered successfully"))
.block();
}
Using imperative/blocking style you can surround it with a try-catch:
try {
webClient.post()
.uri("http://localhost:9000/api")
.body(BodyInserters.fromValue(notification))
.retrieve()
.onStatus(HttpStatus::isError, clientResponse -> Mono.error(NotificationException::new))
.toBodilessEntity()
.block();
} catch(NotificationException e) {...}
A reactive solution would be to use the onErrorResume operator like this:
webClient.post()
.uri("http://localhost:9000/api")
.body(BodyInserters.fromValue(notification))
.retrieve()
.onErrorResume(e -> someOtherMethod())
.toBodilessEntity();
Here, the reactive method someOtherMethod() will be executed in case of any error.

Calling micro service from spring cloud gateway

In spring cloud gateway, added a filter that check for the authentication and authorization for further processing of request. I am calling authentication service using feign client, but I am getting the below error while invoking my service through spring cloud gateway.
java.lang.IllegalStateException: block()/blockFirst()/blockLast() are blocking, which is not supported in thread reactor-http-epoll-3\n\tat reactor.core.publisher.BlockingSingleSubscriber.blockingGet(BlockingSingleSubscriber.java:83)\n\tSuppressed: reactor.core.publisher.FluxOnAssembly$OnAssemblyException: \nError has been observed at the following site(s):\n\t|_ checkpoint ⇢ org.springframework.cloud.gateway.filter.WeightCalculatorWebFilter ....."
I would like to know is it wrong architecture I am using. How to proceed? I am stuck at this error.
#Autowired
private AuthenticationService authService;
// route validator
#Autowired
private RouterValidator routerValidator;
#Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
ServerHttpRequest request = exchange.getRequest();
if (routerValidator.isSecured.test(request)) {
log.info("Accessing the restricted path");
if (this.isAuthMissing(request))
return this.onError(exchange, "Authorization header is missing in request", HttpStatus.UNAUTHORIZED);
final String token = this.getAuthHeader(request);
log.info("before authservice call");
AuthenticationResponse user = authService.isTokenValid(token);
log.info("after authservice call");
if (!user.isValid())
return this.onError(exchange, "Authorization header is invalid", HttpStatus.UNAUTHORIZED);
log.info("before calling populatedRequest");
this.populateRequestWithHeaders(exchange, user);
}
return chain.filter(exchange);
}
private Mono<Void> onError(ServerWebExchange exchange, String err, HttpStatus httpStatus) {
ServerHttpResponse response = exchange.getResponse();
response.setStatusCode(httpStatus);
return response.setComplete();
}
private String getAuthHeader(ServerHttpRequest request) {
return request.getHeaders().getOrEmpty("Authorization").get(0);
}
private boolean isAuthMissing(ServerHttpRequest request) {
log.info("inside auth missing");
return !request.getHeaders().containsKey("Authorization");
}
private void populateRequestWithHeaders(ServerWebExchange exchange, AuthenticationResponse authRes) {
log.info("About to mutate the request->{}",exchange);
exchange.getRequest().mutate()
.header("id",Integer.toString(authRes.getUserId()))
.build();
}
Feign interface
#Autowired
private AuthenticationFeign auth;
public AuthenticationResponse isTokenValid(String token) {
return auth.getValidity(token);
}
I couldn't clearly read it. But problem is that: you can not make blocking call in filter pipeline. Current reactive impl. is like that. if you want, u can use .then() method of WebClient. U should use webclient. because it's reactive.
this link may help you:
https://github.com/spring-cloud/spring-cloud-gateway/issues/980
There was a long time, but i want to give answer. I hope, this help u, please response back, it works or not.

Spring Boot WebClient with OAuth2 and use InsecureTrustManagerFactory

I have successfully implemented WebClient with oAuth2. Facing problem with oAuth2 when the Authentication Server (Keycloak) is having SSL (https). Though I am passing InsecureTrustManagerFactory while defining WebClient, this oAuth is called before the builder is complete as it is there in the filter, it uses default implementation of WebClient and throws certification error.
Is there a way we can configure oAuth2 client also to use InsecureTrustManagerFactory?
pom.xml part
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-client</artifactId>
</dependency>
Bean Configuration
#Bean
public ReactiveOAuth2AuthorizedClientManager authorizedClientManager(
final ReactiveClientRegistrationRepository clientRegistrationRepository,
final ReactiveOAuth2AuthorizedClientService authorizedClientService) {
logger.info("ReactiveOAuth2AuthorizedClientManager Bean Method");
ReactiveOAuth2AuthorizedClientProvider authorizedClientProvider = ReactiveOAuth2AuthorizedClientProviderBuilder
.builder().password().build();
AuthorizedClientServiceReactiveOAuth2AuthorizedClientManager authorizedClientManager = new AuthorizedClientServiceReactiveOAuth2AuthorizedClientManager(
clientRegistrationRepository, authorizedClientService);
authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider);
authorizedClientManager.setContextAttributesMapper(oAuth2AuthorizeRequest -> Mono
.just(Map.of(OAuth2AuthorizationContext.USERNAME_ATTRIBUTE_NAME, System.getProperty("user"),
OAuth2AuthorizationContext.PASSWORD_ATTRIBUTE_NAME, System.getProperty("pass"))));
return authorizedClientManager;
}
/**
* The Oauth2 based WebClient bean for the web service
*
* #throws SSLException
*/
#Bean
public WebClient webClient(ReactiveOAuth2AuthorizedClientManager authorizedClientManager) throws SSLException {
String registrationId = "bael";
SslContext sslContext = SslContextBuilder.forClient().trustManager(InsecureTrustManagerFactory.INSTANCE)
.build();
SslProvider sslProvider = SslProvider.builder().sslContext(sslContext).build();
HttpClient httpClient = HttpClient.create().secure(sslProvider)
.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 5000).responseTimeout(Duration.ofMillis(5000))
.doOnConnected(conn -> conn.addHandlerLast(new ReadTimeoutHandler(5000, TimeUnit.MILLISECONDS))
.addHandlerLast(new WriteTimeoutHandler(5000, TimeUnit.MILLISECONDS)));
ServerOAuth2AuthorizedClientExchangeFilterFunction oauth = new ServerOAuth2AuthorizedClientExchangeFilterFunction(
authorizedClientManager);
oauth.setDefaultClientRegistrationId(registrationId);
logger.info("WebClient Bean Method");
return WebClient.builder()
// base path of the client, this way we need to set the complete url again
.baseUrl("BASE_URL")
.clientConnector(new ReactorClientHttpConnector(httpClient))
.defaultHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE).filter(logRequest())
.filter(oauth).filter(logResponse()).build();
}
So you have to make new WebClient for OAuth2 too.
In your authorizedClientManager definition add some strings(It's better to have HttpClient bean, so you won't define it all the time)
SslContext sslContext = SslContextBuilder.forClient().trustManager(InsecureTrustManagerFactory.INSTANCE)
.build();
SslProvider sslProvider = SslProvider.builder().sslContext(sslContext).build();
HttpClient httpClient = HttpClient.create().secure(sslProvider)
.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 5000).responseTimeout(Duration.ofMillis(5000))
.doOnConnected(conn -> conn.addHandlerLast(new ReadTimeoutHandler(5000, TimeUnit.MILLISECONDS))
.addHandlerLast(new WriteTimeoutHandler(5000, TimeUnit.MILLISECONDS)));
WebClient webClient = WebClient.builder()
.clientConnector(new ReactorClientHttpConnector(httpClient))
.build();
WebClientReactiveClientCredentialsTokenResponseClient clientCredentialsTokenResponseClient =
new WebClientReactiveClientCredentialsTokenResponseClient();
clientCredentialsTokenResponseClient.setWebClient(webClient);
and add in your authorizedClientProvider ->
ReactiveOAuth2AuthorizedClientProvider authorizedClientProvider = ReactiveOAuth2AuthorizedClientProviderBuilder
.builder().password(builder -> builder.accessTokenResponseClient(clientCredentialsTokenResponseClient)).build();

Spring WebClient: SSLEngine closed already

We are using Spring boot version 2.3.1 also we use WebClient
My WebClient configuration:
private val client: WebClient
init {
val sslCtx = SslContextBuilder
.forClient()
.trustManager(InsecureTrustManagerFactory.INSTANCE)
.build()
val httpClient = HttpClient.create().secure { it.sslContext(sslCtx) }
val connector = ReactorClientHttpConnector(httpClient)
client = WebClient.builder()
.clientConnector(connector)
.baseUrl(URL)
.build()
}
private fun post(formData: MultiValueMap<String, String>, response: Class<out Response>, enableLog:
Boolean = true): Response? {
val inserts = BodyInserters.fromFormData(formData)
return try {
client
.post()
.body(inserts)
.retrieve()
.bodyToMono(response)
.block()
} catch (e: Exception) {
if (enableLog) {
log.error("Failed execute request: $formData", e)
}
throw e
}
}
And when I try to debug my application I have this exception:
javax.net.ssl.SSLException: SSLEngine closed already
at io.netty.handler.ssl.SslHandler.wrap(SslHandler.java:848) ~[netty-handler-4.1.50.Final.jar:4.1.50.Final]
Suppressed: reactor.core.publisher.FluxOnAssembly$OnAssemblyException:
Error has been observed at the following site(s):
|_ checkpoint ⇢ Request to POST null [DefaultWebClient]

How to make reactive webclient follow 3XX-redirects?

I have created a basic REST controller which makes requests using the reactive Webclient in Spring-boot 2 using netty.
#RestController
#RequestMapping("/test")
#Log4j2
public class TestController {
private WebClient client;
#PostConstruct
public void setup() {
client = WebClient.builder()
.baseUrl("http://www.google.com/")
.exchangeStrategies(ExchangeStrategies.withDefaults())
.build();
}
#GetMapping
public Mono<String> hello() throws URISyntaxException {
return client.get().retrieve().bodyToMono(String.class);
}
}
When I get a 3XX response code back I want the webclient to follow the redirect using the Location in the response and call that URI recursively until I get a non 3XX response.
The actual result I get is the 3XX response.
You need to configure the client per the docs
WebClient.builder()
.clientConnector(new ReactorClientHttpConnector(
HttpClient.create().followRedirect(true)
))
You could make the URL parameter of your function, and recursively call it while you're getting 3XX responses. Something like this (in real implementation you would probably want to limit the number of redirects):
public Mono<String> hello(String uri) throws URISyntaxException {
return client.get()
.uri(uri)
.exchange()
.flatMap(response -> {
if (response.statusCode().is3xxRedirection()) {
String redirectUrl = response.headers().header("Location").get(0);
return response.bodyToMono(Void.class).then(hello(redirectUrl));
}
return response.bodyToMono(String.class);
}

Resources