How to use WebClient to execute synchronous request?
Asked Answered
B

2

17

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 ?

Bedard answered 7/11, 2019 at 14:30 Comment(3)
do you have a link where it's stated: "switch from RestTemplate to WebClient"?Perusse
@Ivan Lymar, sure: docs.spring.io/spring-framework/docs/current/javadoc-api/org/…Bedard
"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."Tie
B
29

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
Bedard answered 7/11, 2019 at 14:39 Comment(6)
synchronous request means blocking so as you've metionned, you add juste block()Chenoweth
@Bedard how did you get your resp value, after calling the block() method?Contradistinction
@A M .block() resturns the responseBedard
How about to make sync calls and also get the response code?Palliative
@Bedard block() method throws an exception in the newer spring boot versions: block()/blockFirst()/blockLast() are blocking, which is not supported in thread reactor-http-nio-5. Any idea how to solve this issu?Decorous
@Decorous https://mcmap.net/q/337700/-block-blockfirst-blocklast-are-blocking-error-when-calling-bodytomono-after-exchangeBedard
J
-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
Jesicajeske answered 17/2, 2023 at 15:43 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.