gstackoverflow
gstackoverflow

Reputation: 37034

How to use WebClient to execute synchronous request?

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

Answers (3)

SkyBlackHawk
SkyBlackHawk

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

Alex
Alex

Reputation: 5934

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

Upvotes: -2

gstackoverflow
gstackoverflow

Reputation: 37034

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

Upvotes: 29

Related Questions