Post

Getting Started with Spring Boot’s WebClient

An introduction to using Spring Boot’s WebClient—the modern, reactive replacement for RestTemplate.

Getting Started with Spring Boot’s WebClient

WebClient is Spring’s modern, reactive HTTP client. Introduced in Spring 5 as part of WebFlux, it is the successor to RestTemplate.
Unlike RestTemplate, WebClient is non-blocking and works seamlessly in both reactive and traditional applications.


Why WebClient?

  • Reactive, non-blocking API
  • Can be used in both Spring MVC and Spring WebFlux
  • More flexible and powerful than RestTemplate

Adding the Dependency

Add the WebFlux starter to your project:

Maven

1
2
3
4
<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-webflux</artifactId>
</dependency>

Gradle

1
implementation 'org.springframework.boot:spring-boot-starter-webflux'

Creating a WebClient

You can create WebClient in multiple ways:

  1. Default instance:

    1
    
    WebClient client = WebClient.create();
    
  2. With base URL:

    1
    
    WebClient client = WebClient.create("http://localhost:8080");
    
  3. Custom builder:

    1
    2
    3
    4
    5
    
    WebClient client = WebClient.builder()
        .baseUrl("https://api.example.com")
        .defaultHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
        .defaultCookie("sessionId", "abc123")
        .build();
    

Making Requests

GET Example

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Service
public class MyService {

    private final WebClient webClient;

    public MyService(WebClient.Builder webClientBuilder) {
        this.webClient = webClientBuilder
            .baseUrl("http://example.org")
            .build();
    }

    public Mono<Details> getDetails(String name) {
        return webClient.get()
            .uri("/{name}/details", name)
            .retrieve()
            .bodyToMono(Details.class);
    }
}

.retrieve() is preferred over the older .exchange().

POST Example (With Body)

1
2
3
4
5
6
Mono<UserResponse> response = webClient.post()
    .uri("/users")
    .contentType(MediaType.APPLICATION_JSON)
    .bodyValue(new UserRequest("John", "Doe"))
    .retrieve()
    .bodyToMono(UserResponse.class);

PUT Example (With Body)

1
2
3
4
5
6
Mono<Product> updated = webClient.put()
    .uri("/products/{id}", 42)
    .contentType(MediaType.APPLICATION_JSON)
    .bodyValue(new Product("Laptop", 799.99))
    .retrieve()
    .bodyToMono(Product.class);

Working with Parameters and URIs

1
2
3
4
5
6
7
8
9
webClient.get()
  .uri(uriBuilder -> uriBuilder
      .path("/products/{id}")
      .queryParam("category", "widgets")
      .queryParam("tags", "blue", "large")
      .build(productId))
  .retrieve()
  .bodyToMono(Product.class)
  .block(); // avoid blocking in reactive flows

Error Handling

WebClient provides flexible error handling. Instead of always relying on .retrieve(), you can use .exchangeToMono() to handle different status codes explicitly:

1
2
3
4
5
6
7
8
9
10
11
12
Mono<String> response = webClient.get()
    .uri("/api/data")
    .exchangeToMono(res -> {
        if (res.statusCode().equals(HttpStatus.OK)) {
            return res.bodyToMono(String.class);
        } else if (res.statusCode().is4xxClientError()) {
            return Mono.just("Client error occurred");
        } else {
            return res.createException()
                .flatMap(Mono::error);
        }
    });

This approach allows you to:

  • Return different results depending on status code
  • Gracefully handle 4xx errors
  • Convert other errors into exceptions with createException()

Logging Requests and Responses

Option 1: Using Filters

1
2
3
4
WebClient.builder()
  .filter(logRequest())
  .filter(logResponse())
  .build();
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
private ExchangeFilterFunction logRequest() {
    return ExchangeFilterFunction.ofRequestProcessor(req -> {
        log.info("Request: {} {}", req.method(), req.url());
        req.headers().forEach((n, v) -> v.forEach(val -> log.info("{}={}", n, val)));
        return Mono.just(req);
    });
}

private ExchangeFilterFunction logResponse() {
    return ExchangeFilterFunction.ofResponseProcessor(res -> {
        log.info("Response status: {}", res.statusCode());
        res.headers().asHttpHeaders().forEach((n, v) -> v.forEach(val -> log.info("{}={}", n, val)));
        return Mono.just(res);
    });
}

Option 2: Netty Wiretap

With spring-boot-starter-webflux, WebClient runs on Reactor Netty.
Netty’s Wiretap logs detailed request/response information including headers and bodies (for text-based payloads).


Enabling Wiretap

1
2
3
4
5
6
7
8
9
10
11
import org.springframework.web.reactive.function.client.WebClient;
import reactor.netty.http.client.HttpClient;
import org.springframework.http.client.reactive.ReactorClientHttpConnector;

WebClient webClient = WebClient.builder()
    .clientConnector(new ReactorClientHttpConnector(
        HttpClient.create()
            .wiretap("reactor.netty.http.client.HttpClient",
                LogLevel.DEBUG, AdvancedByteBufFormat.TEXTUAL)
    ))
    .build();

Log Output Example

1
2
3
4
5
6
[reactor.netty.http.client.HttpClient] [id: 0x1a2b3c4d] REGISTERED
[reactor.netty.http.client.HttpClient] [id: 0x1a2b3c4d] CONNECT: localhost/127.0.0.1:8080
[reactor.netty.http.client.HttpClient] [id: 0x1a2b3c4d] WRITE: 123B POST /users HTTP/1.1
[reactor.netty.http.client.HttpClient] CONTENT: {"firstName":"John","lastName":"Doe"}
[reactor.netty.http.client.HttpClient] [id: 0x1a2b3c4d] READ: 456B HTTP/1.1 200 OK
[reactor.netty.http.client.HttpClient] CONTENT: {"id":1,"firstName":"John","lastName":"Doe"}

Configuring Logging Verbosity

Enable DEBUG logs for Reactor Netty in application.yml:

1
2
3
4
logging:
  level:
    reactor.netty: DEBUG
    reactor.netty.http.client: DEBUG

When to Use Netty Wiretap?

  • During development to inspect requests/responses
  • For debugging API integration issues
  • To analyze connection lifecycle events

Conclusion

WebClient is the modern alternative to RestTemplate, offering non-blocking, reactive capabilities while staying flexible for traditional applications.
With built-in support for custom error handling and detailed logging (via filters or Wiretap), it is the recommended HTTP client for new Spring projects.


This post is licensed under CC BY 4.0 by the author.