I am going to use the web client to call api from other microservice.However, the web client cannot regconzied my url and give the error.
2023-01-23T17:10:17.261+08:00 ERROR 114017 --- [nio-8080-exec-1] o.a.c.c.C.[.[.[/].[dispatcherServlet] : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed: org.springframework.web.reactive.function.client.WebClientResponseException$BadRequest: 400 Bad Request from UNKNOWN ] with root cause
org.springframework.web.reactive.function.client.WebClientResponseException$BadRequest: 400 Bad Request from UNKNOWN
at org.springframework.web.reactive.function.client.WebClientResponseException.create(WebClientResponseException.java:309) ~[spring-webflux-6.0.3.jar:6.0.3]
Suppressed: reactor.core.publisher.FluxOnAssembly$OnAssemblyException:
Error has been observed at the following site(s):
*__checkpoint ⇢ 400 BAD_REQUEST from GET http:/localhost:8082/Checkuser/252 [DefaultWebClient]
Original Stack Trace:
at org.springframework.web.reactive.function.client.WebClientResponseException.create(WebClientResponseException.java:309) ~[spring-webflux-6.0.3.jar:6.0.3]
at org.springframework.web.reactive.function.client.DefaultClientResponse.lambda$createException$1(DefaultClientResponse.java:213) ~[spring-webflux-6.0.3.jar:6.0.3]
at reactor.core.publisher.FluxMap$MapSubscriber.onNext(FluxMap.java:106) ~[reactor-core-3.5.1.jar:3.5.1]
at reactor.core.publisher.FluxOnErrorReturn$ReturnSubscriber.onNext(FluxOnErrorReturn.java:162) ~[reactor-core-3.5.1.jar:3.5.1]
at reactor.core.publisher.FluxDefaultIfEmpty$DefaultIfEmptySubscriber.onNext(FluxDefaultIfEmpty.java:122) ~[reactor-core-3.5.1.jar:3.5.1]
at reactor.core.publisher.FluxMapFuseable$MapFuseableSubscriber.onNext(FluxMapFuseable.java:129) ~[reactor-core-3.5.1.jar:3.5.1]
at reactor.core.publisher.FluxContextWrite$ContextWriteSubscriber.onNext(FluxContextWrite.java:107) ~[reactor-core-3.5.1.jar:3.5.1]
at reactor.core.publisher.FluxMapFuseable$MapFuseableConditionalSubscriber.onNext(FluxMapFuseable.java:299) ~[reactor-core-3.5.1.jar:3.5.1]
at reactor.core.publisher.FluxFilterFuseable$FilterFuseableConditionalSubscriber.onNext(FluxFilterFuseable.java:337) ~[reactor-core-3.5.1.jar:3.5.1]
at reactor.core.publisher.Operators$BaseFluxToMonoOperator.completePossiblyEmpty(Operators.java:2071) ~[reactor-core-3.5.1.jar:3.5.1]
at reactor.core.publisher.MonoCollect$CollectSubscriber.onComplete(MonoCollect.java:145) ~[reactor-core-3.5.1.jar:3.5.1]
at
My code:
Boolean result=webClientBuilder.build().get()
.uri(uriBuilder -> uriBuilder
.path("http://localhost:8082/Checkuser/{id}")//"http://localhost:8082/Checkuser/{id}")
.build(252))
.retrieve()
.bodyToMono(Boolean.class)
.block(); //make syn request
if(Boolean.FALSE.equals(result)){
throw new IllegalAccessException("User score to low");
My config file:
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.reactive.function.client.WebClient;
import org.springframework.cloud.client.loadbalancer.LoadBalanced;
#Configuration
public class webclientconfig {
#Bean
public WebClient webClient(){
return WebClient.builder().build();
}
#Bean
#LoadBalanced
public WebClient.Builder loadBalancedWebClientBuilder(){
return WebClient.builder();
}
}
The webclient automatically ignore my "/" after http which i have typed
Related
I am new to reactive and web client and i just doing a project in springboot microsevice.While when setting the ReactiveUserDetail Service which require the use of mono.I find that my synchronous request by webclient give an error.I suspect this is due to the "block" ,but it seems that i cannot eliminate it.
Error:
java.lang.IllegalStateException: block()/blockFirst()/blockLast() are blocking, which is not supported in thread reactor-http-epoll-3
at reactor.core.publisher.BlockingSingleSubscriber.blockingGet(BlockingSingleSubscriber.java:83) ~[reactor-core-3.5.1.jar:3.5.1]
Suppressed: reactor.core.publisher.FluxOnAssembly$OnAssemblyException:
Error has been observed at the following site(s):
*__checkpoint ⇢ org.springframework.security.web.server.authentication.AuthenticationWebFilter [DefaultWebFilterChain]
*__checkpoint ⇢ org.springframework.security.web.server.context.ReactorContextWebFilter [DefaultWebFilterChain]
*__checkpoint ⇢ org.springframework.security.web.server.header.HttpHeaderWriterWebFilter [DefaultWebFilterChain]
*__checkpoint ⇢ org.springframework.security.config.web.server.ServerHttpSecurity$ServerWebExchangeReactorContextWebFilter [DefaultWebFilterChain]
*__checkpoint ⇢ org.springframework.security.web.server.WebFilterChainProxy [DefaultWebFilterChain]
*__checkpoint ⇢ HTTP POST "/signin" [ExceptionHandlingWebHandler]
Original Stack Trace:
at reactor.core.publisher.BlockingSingleSubscriber.blockingGet(BlockingSingleSubscriber.java:83) ~[reactor-core-3.5.1.jar:3.5.1]
at reactor.core.publisher.Mono.block(Mono.java:1710) ~[reactor-core-3.5.1.jar:3.5.1]
My code:
#Service
public class SecurityUserService implements ReactiveUserDetailsService {
#Autowired
private WebClient.Builder webClientBuilder;
#Override
public Mono<UserDetails> findByUsername(String username) {
UserAuthdto result= webClientBuilder.baseUrl("http://USER").build().get() //make syn request
.uri(uriBuilder -> uriBuilder
.path("/User/AuthUser/{username}")
.build(username))
.retrieve()
.bodyToMono(UserAuthdto.class)
.block()
;
//map the userresult into securityuser
if(result!=null){
return Mono.just(new SecurityUser(result));
}
//no userfound
return null;
}
}
Entry point from the other microservice:
#GetMapping("/AuthUser/{username}")
public UserAuthdto getSecurityUser(#PathVariable String username){
return userCoreservice.getSecurityUser(username);
}
Web client bean:
#Bean
public WebClient webClient(){
return WebClient.builder().build();
}
Please help
I am trying to use WebClient to consume an endpoint which provides a token.
Using Postman it works as expected. Exported curl from postman is:
curl --location --request POST 'https://mycomp.url/api/oauth/token' \
--header 'Content-Type: application/x-www-form-urlencoded' \
--data-urlencode 'client_id=xxx' \
--data-urlencode 'client_secret=yyy' \
--data-urlencode 'grant_type=client_credentials'
I am configuring webclient call based on same curl above.
Here is my WebClient config:
#Configuration
class ClientConfiguration {
#Bean
fun webClient(): WebClient = WebClient.builder()
.clientConnector(
ReactorClientHttpConnector(
HttpClient.from(
TcpClient
.create()
.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 10000)
.doOnConnected { connection: Connection ->
connection.addHandlerLast(ReadTimeoutHandler(10000, TimeUnit.MILLISECONDS))
connection.addHandlerLast(WriteTimeoutHandler(10000, TimeUnit.MILLISECONDS))
}))
)
.build()
}
Here is the webclient post in order to recieve a token:
#Service
class TokenService(private val webClient: WebClient) {
fun postAsynchronous(): Mono<TokenResponse> = webClient
.post()
.uri(UriComponentsBuilder
.fromHttpUrl("https://mycomp.url")
.path("/api/oauth/token")
.build()
.toUri())
.header("grant_type","client_credentials")
.header("client_id","xxx")
.header("client_secret","yyy")
.header(HttpHeaders.CONTENT_TYPE, "application/x-www-form-urlencoded")
.retrieve()
.onStatus(HttpStatus::is4xxClientError) { Mono.error(RuntimeException("4XX Error ${it.statusCode()}")) }
.onStatus(HttpStatus::is5xxServerError) { Mono.error(RuntimeException("5XX Error ${it.statusCode()}")) }
.bodyToMono(TokenResponse::class.java)
}
Here is my build.gradle.kts (the relevant part):
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
plugins {
id("org.jetbrains.kotlin.jvm") version "1.4.10"
id("org.jetbrains.kotlin.kapt") version "1.4.10"
kotlin("plugin.spring") version "1.5.20"
id("org.springframework.boot") version "2.4.7"
//kotlin("jvm") version "1.5.30"
id("io.spring.dependency-management") version "1.0.10.RELEASE"
}
val kotlinVersion: String by project
val springVersion: String by project
val projectGroupId: String by project
val projectVersion: String by project
group = projectGroupId
version = projectVersion
repositories {
mavenLocal()
... some internal artifactories
mavenCentral()
}
// add dependencies
dependencies {
kapt(kotlin("stdlib", kotlinVersion))
implementation(kotlin("stdlib-jdk8"))
implementation(kotlin("reflect", kotlinVersion))
implementation("org.springframework.boot:spring-boot-dependencies:2.4.7")
implementation("org.springframework.boot:spring-boot-starter:2.4.7")
implementation("org.springframework.boot:spring-boot-starter-web:2.4.7")
implementation("org.springframework.boot:spring-boot-starter-webflux")
implementation("org.springframework.cloud:spring-cloud-starter-openfeign:3.0.3")
implementation("io.github.openfeign:feign-okhttp:10.2.0")
implementation("org.jetbrains.kotlin:kotlin-reflect")
implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8")
implementation("com.fasterxml.jackson.module:jackson-module-kotlin:2.11.2")
}
The whole exception is:
2021/09/23 17:33:53.123 [http-nio-8080-exec-2] INFO o.a.c.c.C.[Tomcat].[localhost].[/] - Initializing Spring DispatcherServlet 'dispatcherServlet'
2021/09/23 17:33:53.123 [http-nio-8080-exec-2] INFO o.s.web.servlet.DispatcherServlet - Initializing Servlet 'dispatcherServlet'
2021/09/23 17:33:53.124 [http-nio-8080-exec-2] INFO o.s.web.servlet.DispatcherServlet - Completed initialization in 1 ms
2021/09/23 17:33:54.396 [http-nio-8080-exec-2] ERROR o.a.c.c.C.[.[.[.[dispatcherServlet] - Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed; nested exception is java.lang.RuntimeException: 4XX Error 401 UNAUTHORIZED] with root cause
java.lang.RuntimeException: 4XX Error 401 UNAUTHORIZED
at com.mycomp.security.TokenService$postAsynchronous$2.apply(TokenService.kt:32)
Suppressed: reactor.core.publisher.FluxOnAssembly$OnAssemblyException:
Error has been observed at the following site(s):
|_ checkpoint ⇢ 401 from POST https://mycomp-url/api/oauth/token [DefaultWebClient]
Stack trace:
at com.mycomp.security.TokenService$postAsynchronous$2.apply(TokenService.kt:32)
at com.mycomp.security.TokenService$postAsynchronous$2.apply(TokenService.kt:15)
at org.springframework.web.reactive.function.client.DefaultWebClient$DefaultResponseSpec$StatusHandler.apply(DefaultWebClient.java:693)
at org.springframework.web.reactive.function.client.DefaultWebClient$DefaultResponseSpec.applyStatusHandlers(DefaultWebClient.java:652)
at org.springframework.web.reactive.function.client.DefaultWebClient$DefaultResponseSpec.handleBodyMono(DefaultWebClient.java:621)
at org.springframework.web.reactive.function.client.DefaultWebClient$DefaultResponseSpec.lambda$bodyToMono$2(DefaultWebClient.java:541)
at reactor.core.publisher.MonoFlatMap$FlatMapMain.onNext(MonoFlatMap.java:125)
I tried also other approach just in case.
I keep webclient as it is and I just change how I send the credentials.
Firstly I created a simple class containing all three parameters:
data class TokenRequest(
var grantType: String,
var clientId: String,
var clientSecret: String
)
And then I modified the webclient.post to
fun postAsynchronous(): Mono<TokenResponse> = webClient
.post()
.uri(UriComponentsBuilder
.fromHttpUrl("https://mycomp-url")
.path("/api/oauth/token")
.build()
.toUri())
.body(BodyInserters.fromValue(TokenRequest("client_credentials","xxx", "yyy")))
.header(HttpHeaders.CONTENT_TYPE, "application/json")
.retrieve()
.onStatus(HttpStatus::is4xxClientError) { Mono.error(RuntimeException("4XX Error ${it.statusCode()}")) }
.onStatus(HttpStatus::is5xxServerError) { Mono.error(RuntimeException("5XX Error ${it.statusCode()}")) }
.bodyToMono(TokenResponse::class.java)
And I got exact same issue:
2021/09/23 18:01:55.994 [http-nio-8080-exec-1] ERROR o.a.c.c.C.[.[.[.[dispatcherServlet] - Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed; nested exception is java.lang.RuntimeException: 4XX Error 401 UNAUTHORIZED] with root cause
java.lang.RuntimeException: 4XX Error 401 UNAUTHORIZED
at com.mycomp.security.TokenService$postAsynchronous$2.apply(TokenService.kt:32)
Suppressed: reactor.core.publisher.FluxOnAssembly$OnAssemblyException:
Error has been observed at the following site(s):
|_ checkpoint ⇢ 401 from POST https://mycomp.url/api/oauth/token [DefaultWebClient]
Stack trace:
at com.mycomp.security.TokenService$postAsynchronous$2.apply(TokenService.kt:32)
at com.mycomp.security.TokenService$postAsynchronous$2.apply(TokenService.kt:15)
at org.springframework.web.reactive.function.client.DefaultWebClient$DefaultResponseSpec$StatusHandler.apply(DefaultWebClient.java:693)
*** Edited in Oct 7th 2021
With Aniket Singla proposal I reached this new issue:
[reactor-tcp-nio-2] WARN r.n.http.client.HttpClientConnect - [id:9270e5dc-1, L:/10.92.12.165:58268 - R:mycomp-url/x.x.x.x:443] The connection observed an error
org.springframework.web.reactive.function.UnsupportedMediaTypeException: Content type 'application/x-www-form-urlencoded' not supported for bodyType=com.mycomp.application.models.token.TokenRequest
at org.springframework.web.reactive.function.BodyInserters.unsupportedError(BodyInserters.java:391)
...
[http-nio-8080-exec-1] ERROR o.a.c.c.C.[.[.[.[dispatcherServlet] - Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed; nested exception is org.springframework.web.reactive.function.client.WebClientRequestException: Content type 'application/x-www-form-urlencoded' not supported for bodyType=com.mycomp.application.models.token.TokenRequest; nested exception is org.springframework.web.reactive.function.UnsupportedMediaTypeException: Content type 'application/x-www-form-urlencoded' not supported for bodyType=com.mycomp.application.models.token.TokenRequest] with root cause
org.springframework.web.reactive.function.UnsupportedMediaTypeException: Content type 'application/x-www-form-urlencoded' not supported for bodyType=com.mycomp.application.models.token.TokenRequest
With Maciej Dobrowolski proposal I got this new exception:
2021/10/07 17:36:29.098 [http-nio-8080-exec-2] ERROR o.a.c.c.C.[.[.[.[dispatcherServlet] - Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed; nested exception is org.springframework.core.codec.DecodingException: JSON decoding error: Instantiation of [simple type, class com.mycomp.application.models.token.TokenResponse] value failed for JSON property result due to missing (therefore NULL) value for creator parameter result which is a non-nullable type; nested exception is com.fasterxml.jackson.module.kotlin.MissingKotlinParameterException: Instantiation of [simple type, class com.mycomp.application.models.token.TokenResponse] value failed for JSON property result due to missing (therefore NULL) value for creator parameter result which is a non-nullable type
at [Source: (io.netty.buffer.ByteBufInputStream); line: 8, column: 1] (through reference chain: com.mycomp.application.models.token.TokenResponse["result"])] with root cause
com.fasterxml.jackson.module.kotlin.MissingKotlinParameterException: Instantiation of [simple type, class com.mycomp.application.models.token.TokenResponse] value failed for JSON property result due to missing (therefore NULL) value for creator parameter result which is a non-nullable type
at [Source: (io.netty.buffer.ByteBufInputStream); line: 8, column: 1] (through reference chain: com.mycomp.application.models.token.TokenResponse["result"])
at com.fasterxml.jackson.module.kotlin.KotlinValueInstantiator.createFromObjectWith(KotlinValueInstantiator.kt:112)
*** Edited
data class TokenResponse (
val result: String
)
Using --data-urlencode curl option, you are adding a parameter to the request's body. In your Kotlin code, you are not passing the same data in the request's body, but in the headers.
What you should do (to mimic postman behavior) is to pass grant_type, client_id, client_secret in the request body by using BodyInserters, like this:
webClient
.post()
.uri(UriComponentsBuilder
.fromHttpUrl("https://mycomp.url")
.path("/api/oauth/token")
.build()
.toUri())
.body(BodyInserters.fromFormData("grant_type", "client_credentials")
.with("client_id", "xxx")
.with("client_secret", "yyy"))
.header(HttpHeaders.CONTENT_TYPE, "application/x-www-form-urlencoded")
.retrieve()
// ...
Supplying url encoded data in headers wont work , you just need to tell in headers that you are going to use "application/x-www-form-urlencoded" as content type, else will be taken care by webclient to convert the body into url encoded form. Made some changes to your postAsynchronous method, should solve your problem.
fun postAsynchronous(): Mono<TokenResponse> = webClient
.post()
.uri(UriComponentsBuilder
.fromHttpUrl("https://des-sts-int.mbi.cloud.ihf")
.path("/api/oauth/token")
.build()
.toUri())
.header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_FORM_URLENCODED_VALUE)
.body(BodyInserters.fromFormData("grant_type", "client_credentials")
.with("client_id", "xxx")
.with("client_secret", "yyy")) )
.retrieve()
.onStatus(HttpStatus::is4xxClientError) { Mono.error(RuntimeException("4XX Error ${it.statusCode()}")) }
.onStatus(HttpStatus::is5xxServerError) { Mono.error(RuntimeException("5XX Error ${it.statusCode()}")) }
.bodyToMono(TokenResponse::class.java)
Spring boot-2.3.10, Spring Cloud Gcp: 1.2.8
I'm trying to access specific image pattern **(/resources/images/specific_folder/****) from GC Storage. For that I wrote the resource handler as shown below:
#Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
log.info("Setting the resource location {}", gcStorageLocation);
registry.addResourceHandler("/resources/images/specific_folder/**").addResourceLocations("gs:bucket_name/storage/images/specific_folder/").setCachePeriod(3600).resourceChain(true)
.addResolver(new GcStorageResolver());
}
GcStorageResolver.java extends AbstractResourceResolver.java
#Override
protected Resource resolveResourceInternal(#Nullable HttpServletRequest request, String requestPath, List<? extends Resource> locations,
ResourceResolverChain chain) {
log.info("resolveResourceInternal called for request: {}, requestPath: {}", request.getRequestURL(), requestPath);
return getResource(requestPath, request, locations);
}
I verified that a valid GoogleStorageResource is being returned along with credential. But somewhere in the spring chain, I'm getting the below error:
2021-06-25 15:40:23.366 ERROR 4676 --- [nio-8080-exec-1]
o.a.c.c.C.[.[.[.[dispatcherServlet] 175 : Servlet.service() for
servlet [dispatcherServlet] in context with path [] threw exception
[Request processing failed; nested exception is
com.google.cloud.storage.StorageException: Error parsing token refresh
response. Expected value access_token not found.] with root cause
java.io.IOException: Error parsing token refresh response. Expected
value access_token not found. at
com.google.auth.oauth2.OAuth2Utils.validateString(OAuth2Utils.java:113)
~[google-auth-library-oauth2-http-0.22.1.jar:?] at
com.google.auth.oauth2.ServiceAccountCredentials.refreshAccessToken(ServiceAccountCredentials.java:449)
~[google-auth-library-oauth2-http-0.22.1.jar:?] at
com.google.auth.oauth2.OAuth2Credentials.refresh(OAuth2Credentials.java:157)
~[google-auth-library-oauth2-http-0.22.1.jar:?] at
com.google.auth.oauth2.OAuth2Credentials.getRequestMetadata(OAuth2Credentials.java:145)
~[google-auth-library-oauth2-http-0.22.1.jar:?] at
com.google.auth.oauth2.ServiceAccountCredentials.getRequestMetadata(ServiceAccountCredentials.java:603)
~[google-auth-library-oauth2-http-0.22.1.jar:?] at
com.google.auth.http.HttpCredentialsAdapter.initialize(HttpCredentialsAdapter.java:91)
~[google-auth-library-oauth2-http-0.22.1.jar:?] at
com.google.cloud.http.HttpTransportOptions$1.initialize(HttpTransportOptions.java:159)
~[google-cloud-core-http-1.94.0.jar:1.94.0] at
com.google.cloud.http.CensusHttpModule$CensusHttpRequestInitializer.initialize(CensusHttpModule.java:109)
~[google-cloud-core-http-1.94.0.jar:1.94.0] at
com.google.api.client.http.HttpRequestFactory.buildRequest(HttpRequestFactory.java:88)
~[google-http-client-1.38.0.jar:1.38.0]
Not sure what's going on here. Any pointers?
Related to https://github.com/reactor/reactor-netty/issues/1431.
Spring cloud gateway server starts correctly but requests via the gateway fail with the below exception:
java.net.UnknownHostException: failed to resolve 'myserver' after 4 queries
at io.netty.resolver.dns.DnsResolveContext.finishResolve(DnsResolveContext.java:1013)
Suppressed: reactor.core.publisher.FluxOnAssembly$OnAssemblyException:
Error has been observed at the following site(s):
|_ checkpoint ? org.springframework.cloud.gateway.filter.WeightCalculatorWebFilter [DefaultWebFilterChain]
|_ checkpoint ? org.springframework.cloud.sleuth.instrument.web.TraceWebFilter [DefaultWebFilterChain]
|_ checkpoint ? org.springframework.boot.actuate.metrics.web.reactive.server.MetricsWebFilter [DefaultWebFilterChain]
|_ checkpoint ? HTTP GET "/service/api/applications/123" [ExceptionHandlingWebHandler]
Stack trace:
at io.netty.resolver.dns.DnsResolveContext.finishResolve(DnsResolveContext.java:1013)
at io.netty.resolver.dns.DnsResolveContext.tryToFinishResolve(DnsResolveContext.java:966)
at io.netty.resolver.dns.DnsResolveContext.query(DnsResolveContext.java:414)
at io.netty.resolver.dns.DnsResolveContext.onResponse(DnsResolveContext.java:601)
at io.netty.resolver.dns.DnsResolveContext.access$400(DnsResolveContext.java:63)
at io.netty.resolver.dns.DnsResolveContext$2.operationComplete(DnsResolveContext.java:458)
.....
Works correctly when downgrading spring boot.
Fixed by adding the below bean definition:
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import io.netty.resolver.DefaultAddressResolverGroup;
import reactor.netty.http.client.HttpClient;
#Configuration
public class TempFixConfig {
#Bean
public HttpClient webClient() {
return HttpClient.create().resolver(DefaultAddressResolverGroup.INSTANCE);
}
}
If you don't want to change the logic of how the HttpClient bean is created, you can add a HttpClientCustomizer:
import org.springframework.cloud.gateway.config.HttpClientCustomizer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import io.netty.resolver.DefaultAddressResolverGroup;
import reactor.netty.http.client.HttpClient;
#Configuration
public class HttpClientResolverFixConfig {
#Bean
public HttpClientCustomizer httpClientResolverCustomizer() {
return new HttpClientCustomizer() {
#Override
public HttpClient customize(HttpClient httpClient) {
return httpClient.resolver(DefaultAddressResolverGroup.INSTANCE);
}
};
}
}
I am using WebClient in a Spring Boot MVC 2.1 project and found that the first request made by the client takes up to 6 seconds. Subsequent requests are way faster (~30ms).
There's a closed issue in Spring's JIRA that advices using Jetty as the WebClient Http connector. I have tried that approach, improving the figures, with a ~800ms first request. This time is an improvement but it's still far from RestTemplate which usally takes <200ms.
Netty approach (5s first request):
Conf:
#Bean
public WebClient webClient() {
return WebClient.create();
}
Usage:
private final WebClient webClient;
#GetMapping(value="/wc", produces = APPLICATION_JSON_UTF8_VALUE)
public Mono<String> findWc() throws URISyntaxException {
URI uri = new URI("http://xxx");
final Mono<String> response = webClient.get().uri(uri).retrieve().bodyToMono(String.class);
return response;
}
Jetty approach (800ms first request):
Conf:
#Bean
public JettyResourceFactory resourceFactory() {
return new JettyResourceFactory();
}
#Bean
public WebClient webClient() {
ClientHttpConnector connector = new JettyClientHttpConnector(resourceFactory(), null);
return WebClient.builder().clientConnector(connector).build();
}
Usage: same as before.
There's another "problem" with the Jetty approach. On server shutdown it always produces the following exception:
27-Dec-2018 11:24:20.463 INFO [jetty-http#74305db9-65] org.apache.catalina.loader.WebappClassLoaderBase.checkStateForResourceLoading Illegal access: this web application instance has been stopped already. Could not load [org.eclipse.jetty.io.ManagedSelector$StopSelector]. The following stack trace is thrown for debugging purposes as well as to attempt to terminate the thread which caused the illegal access.
java.lang.IllegalStateException: Illegal access: this web application instance has been stopped already. Could not load [org.eclipse.jetty.io.ManagedSelector$StopSelector]. The following stack trace is thrown for debugging purposes as well as to attempt to terminate the thread which caused the illegal access.
at org.apache.catalina.loader.WebappClassLoaderBase.checkStateForResourceLoading(WebappClassLoaderBase.java:1348)
at org.apache.catalina.loader.WebappClassLoaderBase.checkStateForClassLoading(WebappClassLoaderBase.java:1336)
at org.apache.catalina.loader.WebappClassLoaderBase.loadClass(WebappClassLoaderBase.java:1195)
at org.apache.catalina.loader.WebappClassLoaderBase.loadClass(WebappClassLoaderBase.java:1157)
at java.lang.Class.getDeclaringClass0(Native Method)
at java.lang.Class.getDeclaringClass(Class.java:1235)
at java.lang.Class.getEnclosingClass(Class.java:1277)
at java.lang.Class.getSimpleBinaryName(Class.java:1443)
at java.lang.Class.getSimpleName(Class.java:1309)
at org.eclipse.jetty.io.ManagedSelector$SelectorProducer.toString(ManagedSelector.java:534)
at java.lang.String.valueOf(String.java:2994)
at java.lang.StringBuilder.append(StringBuilder.java:131)
at org.eclipse.jetty.util.thread.strategy.EatWhatYouKill.getString(EatWhatYouKill.java:458)
at org.eclipse.jetty.util.thread.strategy.EatWhatYouKill.toStringLocked(EatWhatYouKill.java:447)
at org.eclipse.jetty.util.thread.strategy.EatWhatYouKill.toString(EatWhatYouKill.java:440)
at org.slf4j.helpers.MessageFormatter.safeObjectAppend(MessageFormatter.java:299)
at org.slf4j.helpers.MessageFormatter.deeplyAppendParameter(MessageFormatter.java:271)
at org.slf4j.helpers.MessageFormatter.arrayFormat(MessageFormatter.java:233)
at org.slf4j.helpers.MessageFormatter.arrayFormat(MessageFormatter.java:173)
at org.eclipse.jetty.util.log.JettyAwareLogger.log(JettyAwareLogger.java:680)
at org.eclipse.jetty.util.log.JettyAwareLogger.debug(JettyAwareLogger.java:224)
at org.eclipse.jetty.util.log.Slf4jLog.debug(Slf4jLog.java:97)
at org.eclipse.jetty.util.thread.strategy.EatWhatYouKill.doProduce(EatWhatYouKill.java:288)
at org.eclipse.jetty.util.thread.strategy.EatWhatYouKill.tryProduce(EatWhatYouKill.java:168)
at org.eclipse.jetty.util.thread.strategy.EatWhatYouKill.run(EatWhatYouKill.java:126)
at org.eclipse.jetty.util.thread.ReservedThreadExecutor$ReservedThread.run(ReservedThreadExecutor.java:366)
at org.eclipse.jetty.util.thread.QueuedThreadPool.runJob(QueuedThreadPool.java:765)
at org.eclipse.jetty.util.thread.QueuedThreadPool$2.run(QueuedThreadPool.java:683)
at java.lang.Thread.run(Thread.java:748)
SLF4J: Failed toString() invocation on an object of type [org.eclipse.jetty.util.thread.strategy.EatWhatYouKill]
Reported exception:
java.lang.NoClassDefFoundError: org/eclipse/jetty/io/ManagedSelector$StopSelector
at java.lang.Class.getDeclaringClass0(Native Method)
at java.lang.Class.getDeclaringClass(Class.java:1235)
at java.lang.Class.getEnclosingClass(Class.java:1277)
at java.lang.Class.getSimpleBinaryName(Class.java:1443)
at java.lang.Class.getSimpleName(Class.java:1309)
at org.eclipse.jetty.io.ManagedSelector$SelectorProducer.toString(ManagedSelector.java:534)
at java.lang.String.valueOf(String.java:2994)
at java.lang.StringBuilder.append(StringBuilder.java:131)
at org.eclipse.jetty.util.thread.strategy.EatWhatYouKill.getString(EatWhatYouKill.java:458)
at org.eclipse.jetty.util.thread.strategy.EatWhatYouKill.toStringLocked(EatWhatYouKill.java:447)
at org.eclipse.jetty.util.thread.strategy.EatWhatYouKill.toString(EatWhatYouKill.java:440)
at org.slf4j.helpers.MessageFormatter.safeObjectAppend(MessageFormatter.java:299)
at org.slf4j.helpers.MessageFormatter.deeplyAppendParameter(MessageFormatter.java:271)
at org.slf4j.helpers.MessageFormatter.arrayFormat(MessageFormatter.java:233)
at org.slf4j.helpers.MessageFormatter.arrayFormat(MessageFormatter.java:173)
at org.eclipse.jetty.util.log.JettyAwareLogger.log(JettyAwareLogger.java:680)
at org.eclipse.jetty.util.log.JettyAwareLogger.debug(JettyAwareLogger.java:224)
at org.eclipse.jetty.util.log.Slf4jLog.debug(Slf4jLog.java:97)
at org.eclipse.jetty.util.thread.strategy.EatWhatYouKill.doProduce(EatWhatYouKill.java:288)
at org.eclipse.jetty.util.thread.strategy.EatWhatYouKill.tryProduce(EatWhatYouKill.java:168)
at org.eclipse.jetty.util.thread.strategy.EatWhatYouKill.run(EatWhatYouKill.java:126)
at org.eclipse.jetty.util.thread.ReservedThreadExecutor$ReservedThread.run(ReservedThreadExecutor.java:366)
at org.eclipse.jetty.util.thread.QueuedThreadPool.runJob(QueuedThreadPool.java:765)
at org.eclipse.jetty.util.thread.QueuedThreadPool$2.run(QueuedThreadPool.java:683)
at java.lang.Thread.run(Thread.java:748)
Caused by: java.lang.ClassNotFoundException: Illegal access: this web application instance has been stopped already. Could not load [org.eclipse.jetty.io.ManagedSelector$StopSelector]. The following stack trace is thrown for debugging purposes as well as to attempt to terminate the thread which caused the illegal access.
at org.apache.catalina.loader.WebappClassLoaderBase.checkStateForClassLoading(WebappClassLoaderBase.java:1338)
at org.apache.catalina.loader.WebappClassLoaderBase.loadClass(WebappClassLoaderBase.java:1195)
at org.apache.catalina.loader.WebappClassLoaderBase.loadClass(WebappClassLoaderBase.java:1157)
... 25 more
Caused by: java.lang.IllegalStateException: Illegal access: this web application instance has been stopped already. Could not load [org.eclipse.jetty.io.ManagedSelector$StopSelector]. The following stack trace is thrown for debugging purposes as well as to attempt to terminate the thread which caused the illegal access.
at org.apache.catalina.loader.WebappClassLoaderBase.checkStateForResourceLoading(WebappClassLoaderBase.java:1348)
at org.apache.catalina.loader.WebappClassLoaderBase.checkStateForClassLoading(WebappClassLoaderBase.java:1336)
... 27 more
27-Dec-2018 11:24:20.467 INFO [jetty-http#74305db9-65] org.apache.catalina.loader.WebappClassLoaderBase.checkStateForResourceLoading Illegal access: this web application instance has been stopped already. Could not load [ch.qos.logback.classic.spi.ThrowableProxy]. The following stack trace is thrown for debugging purposes as well as to attempt to terminate the thread which caused the illegal access.
java.lang.IllegalStateException: Illegal access: this web application instance has been stopped already. Could not load [ch.qos.logback.classic.spi.ThrowableProxy]. The following stack trace is thrown for debugging purposes as well as to attempt to terminate the thread which caused the illegal access.
at org.apache.catalina.loader.WebappClassLoaderBase.checkStateForResourceLoading(WebappClassLoaderBase.java:1348)
at org.apache.catalina.loader.WebappClassLoaderBase.checkStateForClassLoading(WebappClassLoaderBase.java:1336)
at org.apache.catalina.loader.WebappClassLoaderBase.loadClass(WebappClassLoaderBase.java:1195)
at org.apache.catalina.loader.WebappClassLoaderBase.loadClass(WebappClassLoaderBase.java:1157)
at ch.qos.logback.classic.spi.LoggingEvent.<init>(LoggingEvent.java:119)
at ch.qos.logback.classic.Logger.buildLoggingEventAndAppend(Logger.java:419)
at ch.qos.logback.classic.Logger.filterAndLog_0_Or3Plus(Logger.java:383)
at ch.qos.logback.classic.Logger.log(Logger.java:765)
at org.eclipse.jetty.util.log.JettyAwareLogger.log(JettyAwareLogger.java:668)
at org.eclipse.jetty.util.log.JettyAwareLogger.warn(JettyAwareLogger.java:474)
at org.eclipse.jetty.util.log.Slf4jLog.warn(Slf4jLog.java:73)
at org.eclipse.jetty.util.log.Slf4jLog.warn(Slf4jLog.java:67)
at org.eclipse.jetty.util.thread.strategy.EatWhatYouKill.execute(EatWhatYouKill.java:375)
at org.eclipse.jetty.util.thread.strategy.EatWhatYouKill.doProduce(EatWhatYouKill.java:305)
at org.eclipse.jetty.util.thread.strategy.EatWhatYouKill.tryProduce(EatWhatYouKill.java:168)
at org.eclipse.jetty.util.thread.strategy.EatWhatYouKill.run(EatWhatYouKill.java:126)
at org.eclipse.jetty.util.thread.ReservedThreadExecutor$ReservedThread.run(ReservedThreadExecutor.java:366)
at org.eclipse.jetty.util.thread.QueuedThreadPool.runJob(QueuedThreadPool.java:765)
at org.eclipse.jetty.util.thread.QueuedThreadPool$2.run(QueuedThreadPool.java:683)
at java.lang.Thread.run(Thread.java:748)
Exception in thread "jetty-http#74305db9-65" java.lang.NoClassDefFoundError: ch/qos/logback/classic/spi/ThrowableProxy
at ch.qos.logback.classic.spi.LoggingEvent.<init>(LoggingEvent.java:119)
at ch.qos.logback.classic.Logger.buildLoggingEventAndAppend(Logger.java:419)
at ch.qos.logback.classic.Logger.filterAndLog_0_Or3Plus(Logger.java:383)
at ch.qos.logback.classic.Logger.log(Logger.java:765)
at org.eclipse.jetty.util.log.JettyAwareLogger.log(JettyAwareLogger.java:668)
at org.eclipse.jetty.util.log.JettyAwareLogger.warn(JettyAwareLogger.java:474)
at org.eclipse.jetty.util.log.Slf4jLog.warn(Slf4jLog.java:73)
at org.eclipse.jetty.util.log.Slf4jLog.warn(Slf4jLog.java:67)
at org.eclipse.jetty.util.thread.QueuedThreadPool$2.run(QueuedThreadPool.java:740)
at java.lang.Thread.run(Thread.java:748)
How can I avoid this exception?
Is there any other way we can use to improve the WebClient first request slowness?
I went through the same problem and managed to solve it by changing the connector used by the WebClient.
Below the configuration file with the appropriate imports
import org.eclipse.jetty.client.HttpClient;
import org.eclipse.jetty.util.ssl.SslContextFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.client.reactive.ClientHttpConnector;
import org.springframework.http.client.reactive.JettyClientHttpConnector;
import org.springframework.web.reactive.function.client.WebClient;
The class
SslContextFactory.Client sslContextFactory = new SslContextFactory.Client();
HttpClient httpClient = new HttpClient(sslContextFactory);
httpClient.setFollowRedirects(false);
httpClient.setConnectTimeout(TIMEOUT);
httpClient.start();
final ClientHttpConnector connector = new JettyClientHttpConnector(httpClient);
final WebClient webClient = WebClient.builder()
.clientConnector(connector)
.baseUrl(BASE_URL)
.build();
It is important to know how to add the right libs to your project, so below is how I managed to import using gradle:
implementation 'org.eclipse.jetty:jetty-client'
implementation 'org.eclipse.jetty:jetty-reactive-httpclient'
We upgraded Spring Boot to 2.4.2 with reactor-netty 1.0.3 but still encounter this issue.
Here is our upgraded configuration:
#Bean
public WebClient createWebClient(WebClient.Builder webClientBuilder) {
log.info("Initializing WebClient Bean");
final int timeoutInMillis = Long.valueOf(TimeUnit.SECONDS.toMillis(timeout)).intValue();
final HttpClient httpClient = HttpClient.create()
.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, timeoutInMillis)
.responseTimeout(Duration.ofMillis(timeoutInMillis))
.doOnConnected(conn ->
conn.addHandlerLast(new ReadTimeoutHandler(timeoutInMillis, TimeUnit.MILLISECONDS))
.addHandlerLast(new WriteTimeoutHandler(timeoutInMillis, TimeUnit.MILLISECONDS)));
final ClientHttpConnector connector = new ReactorClientHttpConnector(httpClient);
final WebClient webClient = webClientBuilder
.clientConnector(connector)
.defaultHeader("x-clientname", clientname)
.build();
httpClient.warmup().block();
log.info("WebClient initialized");
return webClient;
}
Our call with WebClient:
ResoponseObject doCall() {
return this.webClient
.get()
.uri("http://***.de/api/rest/***")
.accept(MediaType.APPLICATION_JSON)
.retrieve()
.bodyToMono(ResponseObject.class)
.block();
}
With debugging enabled via application.yaml:
logging.level.reactor.netty: debug
We now see this during application startup:
2021-02-10 17:02:31,922 INFO d.t.e.b.c.c.WebClientAutoConfiguration - Initializing WebClient Bean
2021-02-10 17:02:31,959 DEBUG r.n.r.DefaultLoopIOUring - Default io_uring support : false
2021-02-10 17:02:31,967 DEBUG r.n.r.DefaultLoopEpoll - Default Epoll support : true
2021-02-10 17:02:31,997 INFO d.t.e.b.c.c.WebClientAutoConfiguration - WebClient initialized
This should be an indication that the warmup works as expected?
But on first request this happens:
2021-02-10 17:05:16,045 DEBUG o.s.w.r.f.c.ExchangeFunctions - [73d400c8] HTTP GET http://***.de/api/rest/***
2021-02-10 17:05:16,050 DEBUG r.n.r.PooledConnectionProvider - Creating a new [http] client pool [PoolFactory{evictionInterval=PT0S, leasingStrategy=fifo, maxConnections=500, maxIdleTime=-1, maxLifeTime=-1, metricsEnabled=false, pendingAcquireMaxCount=1000, pendingAcquireTimeout=45000}] for [***.de/<unresolved>:80]
2021-02-10 17:05:29,619 DEBUG r.n.r.DefaultPooledConnectionProvider - [id: 0x71b840f4] Created a new pooled channel, now 1 active connections and 0 inactive connections
2021-02-10 17:05:29,635 DEBUG r.n.t.TransportConfig - [id: 0x71b840f4] Initialized pipeline DefaultChannelPipeline{(reactor.left.httpCodec = io.netty.handler.codec.http.HttpClientCodec), (reactor.right.reactiveBridge = reactor.netty.channel.ChannelOperationsHandler)}
...
In our case, creating the client pool is the problem. On a decent machine, it takes about 13 seconds.
Can you give us any comment on that? This is very this is very frustrating for us.
Thanks a lot!