TL;DR
- The correct way to use
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.
- You should never block
WebClient
in the reactive application. It will block one of the few threads and could cause serious issues.
- Don't use
.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
.
Using WebClient
in a Spring MVC (SERVLET) application
Consider 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
.
Using WebClient
in a Spring WebFlux (REACTIVE) application
WebClient
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
org.springframework.web.reactive.client.WebClient
which has a more modern API and supports sync, async, and streaming scenarios." – Tie