13

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 ?

Manuel Jordan
  • 15,253
  • 21
  • 95
  • 158
gstackoverflow
  • 36,709
  • 117
  • 359
  • 710
  • do you have a link where it's stated: "switch from RestTemplate to WebClient"? – Ivan Lymar Nov 07 '19 at 15:02
  • 3
    @Ivan Lymar, sure: https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/web/client/RestTemplate.html – gstackoverflow Nov 07 '19 at 15:28
  • "As of 5.0 this class is in maintenance mode, with only minor requests for changes and bugs to be accepted going forward. Please, consider using the `org.springframework.web.reactive.client.WebClient` which has a more modern API and supports sync, async, and streaming scenarios." – M. Justin Dec 17 '22 at 06:24

2 Answers2

27

UPDATE

In a new library version please use:

webClient.post()
         .bodyValue(myDto)
         .retrieve()
         .toEntity(MyDto.class)
         .toFuture()
         .get();

Old answer (for old version of library)

It works:

webClient.post()
         .bodyValue(myDto)
         .retrieve()
         .toEntity(MyDto.class)
         .block(); // <-- This line makes trick
gstackoverflow
  • 36,709
  • 117
  • 359
  • 710
-2

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
Alex
  • 4,987
  • 1
  • 8
  • 26