Reputation: 37034
Spring documentation states that we have to switch from RestTemplate
to WebClient
even if we want to execute Synchronous HTTP call.
For now I have following code:
Mono<ResponseEntity<PdResponseDto>> responseEntityMono = webClient.post()
.bodyValue(myDto)
.retrieve()
.toEntity(MyDto.class);
responseEntityMono.subscribe(resp -> log.info("Response is {}", resp));
//I have to return response here
// return resp;
Sure I could use CountdownLatch here but it looks like API misusing.
How could I execute synchronous request ?
Upvotes: 17
Views: 26783
Reputation: 127
The solutions described in other post for a webclient that call a service with basic auth to get a token and then use that token as bearer in other webclient only in webflux not work. I lost a long time the one solution that I want in next time to use is in the article published in medium site. If it works fine but normally there the code works fine. For .block/.blockFirst/.blockLast methods not work in real webflux. If you have only spring-boot-starter-webflux in pom.xml you can't use .block/.blockFirst/.blockLast (not reactive - not pure webflux). If you need to use .block/.blockFirst/.blockLast you need to add in pom.xml spring-boot-starter-web not only spring-boot-starter-webflux. In this hybrid scenario Spring try to use not reactive mode and allow use .block/.blockFirst/.blockLast methods
I found this solution on Mar 29, 2024 that may be working as described in pure webflux, it use CompletableFuture logic to prevent to call next webclient without bearer token returned that will get in future.
I don't test it but in medium site normally examples work fine. If somebody know a solution to tie 2 pure asyncronos WebClient call in syncronous using pure webflux please explain here is something as async, Promise and await(() => ) in nodeJS
Upvotes: 0
Reputation: 5934
TL;DR
WebClient
in a non-reactive application is to use block()
. For example, as a replacement for RestTemplate
. block()
is a blocking operation in reactive terms but there is no issue to use it in a non-reactive flow.WebClient
in the reactive application. It will block one of the few threads and could cause serious issues..toFuture().get()
in the reactive application because it could block the thread forever and application could hang.Mono<ResponseEntity<PdResponseDto>> responseEntityMono = webClient.post()
.bodyValue(myDto)
.retrieve()
.toEntity(PdResponseDto.class)
.block();
Here is more detailed explanation of possible scenarios and possible issues like error java.lang.IllegalStateException: block()/blockFirst()/blockLast() are blocking, which is not supported in thread reactor-http-nio-X
.
WebClient
in a Spring MVC (SERVLET) applicationConsider simple `RestController as
@GetMapping(path = "test")
public String test() {
log.info("START");
var res = webClient.get()
.uri("http://www.google.com")
.retrieve()
.bodyToMono(String.class)
.block();
log.info("END");
return res;
}
Looking at logs we will see that execution starts in http-nio-auto-1-exec-1
thread, then WebClient
switches to internal reactive thread pool reactor-http-nio-3
and block
returns execution to http-nio-auto-1-exec-1
00:17:26.637 [http-nio-auto-1-exec-1] INFO [c.e.d.TestController] - START
00:17:26.647 [http-nio-auto-1-exec-1] INFO [c.e.d.TestController] - Request: GET http://www.google.com
00:17:26.831 [reactor-http-nio-3] INFO [c.e.d.TestController] - Response Status: 200 OK
00:17:26.856 [http-nio-auto-1-exec-1] INFO [c.e.d.TestController] - END
As explained in the Spring Reference Documentation about Web Environments adding both
org.springframework.boot:spring-boot-starter-web
org.springframework.boot:spring-boot-starter-webflux
dependencies will configure Spring MVC application and initialize WebApplicationType
to SERVLET
.
WebClient
in a Spring WebFlux (REACTIVE) applicationWebClient
should not be blocked in a reactive application. The only reason I could think about is a period when application is migrated to reactive stack and not all code was refactored.
If we remove org.springframework.boot:spring-boot-starter-web
dependency or set WebApplicationType
to REACTIVE
, Spring WebFlux is initialized.
The previous code will start throwing exception
java.lang.IllegalStateException: block()/blockFirst()/blockLast() are blocking, which is not supported in thread reactor-http-nio-X
It happens because Spring WebFlux uses small number of threads to handle all requests and blocking these threads could cause serious performance issues. Therefor block()
has an internal logic to warn us about this issue.
Looking at logs we will see that request starts in [reactor-http-nio-4] that is part of the non-blocking thread pool (Scheduler).
00:45:38.857 [reactor-http-nio-4] INFO [c.e.d.TestController] - START
00:45:38.863 [reactor-http-nio-4] INFO [c.e.d.TestController] - Request: GET http://www.google.com
00:45:38.881 [reactor-http-nio-4] ERROR [o.s.b.a.w.r.e.AbstractErrorWebExceptionHandler] - [fbe6f35d-1] 500 Server Error for HTTP GET "/get"
Some people suggested toFuture().get()
but it is technically the same as block()
. It will not result in exception block()/blockFirst()/blockLast() are blocking, which is not supported in thread ...
because it's out of Reactor API control but problem is still the same because this operation is blocking and you just hide the issue. In some cases it could even block the app.
What is the right way to solve this problem?
Option 1 (preferred)
Refactor code to reactive and return Mono
or Flux
from controller
@GetMapping(path = "get")
public Mono<String> get() {
return webClient.get()
.uri("http://www.google.com")
.retrieve()
.bodyToMono(String.class)
.doOnSubscribe(s -> log.info("START"))
.doOnNext(res -> log.info("END"));
}
In this case the whole flow is reactive and running on non-blocking reactor-http-nio-4
01:21:39.275 [reactor-http-nio-4] INFO [c.e.d.TestController] - Request: GET http://www.google.com
01:21:39.277 [reactor-http-nio-4] INFO [c.e.d.TestController] - START
01:21:39.431 [reactor-http-nio-4] INFO [c.e.d.TestController] - Response Status: 200 OK
01:21:39.454 [reactor-http-nio-4] INFO [c.e.d.TestController] - END
Option 2 (temporary)
As a temporary solution we can consider wrapping blocking code into Runnable
or Callable
and schedule on a separate Scheduler
. Check How Do I Wrap a Synchronous, Blocking Call? for details.
@GetMapping(path = "get")
public Mono<String> get() {
return Mono.fromCallable(() -> {
var res = webClient.get()
.uri("http://www.google.com")
.retrieve()
.bodyToMono(String.class)
.block();
log.info("END");
return res;
})
.subscribeOn(Schedulers.boundedElastic())
.doOnSubscribe(s -> log.info("START"));
}
Request starts on non-blocking reactor-http-nio-4
but then switched to boundedElastic-1
that allows to execute blocking code.
01:17:48.930 [reactor-http-nio-4] INFO [c.e.d.TestController] - START
01:17:48.941 [boundedElastic-1] INFO [c.e.d.TestController] - Request: GET http://www.google.com
01:17:49.102 [reactor-http-nio-5] INFO [c.e.d.TestController] - Response Status: 200 OK
01:17:49.125 [boundedElastic-1] INFO [c.e.d.TestController] - END
Upvotes: -2
Reputation: 37034
In a new library version please use:
webClient.post()
.bodyValue(myDto)
.retrieve()
.toEntity(MyDto.class)
.toFuture()
.get();
It works:
webClient.post()
.bodyValue(myDto)
.retrieve()
.toEntity(MyDto.class)
.block(); // <-- This line makes trick
Upvotes: 29