Webclient

When to Use WebClient

  • When building reactive applications (e.g., using Spring WebFlux).

  • When you need non-blocking I/O for better scalability and performance.

  • When working with streaming APIs or handling large datasets.

  • When you want to use functional programming for HTTP requests.

Dependency

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-webflux</artifactId>
</dependency>

Sample Usages

// get example
public <T> Mono<T> get(String url, Class<T> responseType,String token) {
        return webClient.get()
                .uri(getUriBuilderURIFunction(url))
                .header(MessageConst.AUTHORIZATION, MessageConst.BEARER +token)
                .retrieve()
                .bodyToMono(responseType)
                .timeout(timeout)
                .doOnSuccess(response -> log.info("Received successful response from GET {}", url))
                .doOnError(error -> log.error("Error in GET request to {}: {}", url, error.getMessage()))
                .retryWhen(
                        Retry.backoff(retry, Duration.ofSeconds(1)).filter(e->!e.getMessage().equals("401"))
                                .doBeforeRetry(retrySignal -> log.warn("Retrying GET request to {}", url))
                )
                .onErrorResume(this::handleError);
    }
    

// post example
 public <T> Mono<T> post(String url, Object body, Class<T> responseType,String token) {
        return webClient.post().uri(getUriBuilderURIFunction(url))
                .header(MessageConst.AUTHORIZATION, MessageConst.BEARER +token)
                .bodyValue(body)
                .retrieve()
                .bodyToMono(responseType)
                .timeout(timeout)
                .doOnSuccess(response -> log.info("Received successful response from POST {}", url))
                .doOnError(error -> log.error("Error in POST request to {}: {}", url, error.getMessage()))
                .onErrorResume(this::handleError);
    }

Exception handling

We can use reactive extreame to do the handling . We have certain methods like doOnError, doOnStatus, onErrorResume etc, using them we can define how we want to handle it.

// in web client 
.onStatus(
        HttpStatus::is4xxClientError,
        clientResponse -> Mono.error(new RuntimeException("Client error occurred"))
    )
 .onStatus(
        HttpStatus::is5xxServerError,
        clientResponse -> Mono.error(new RuntimeException("Server error occurred"))
 )
    
// or
.onErrorResume(this::handleError)

// custom handler method
  private <T> Mono<T> handleError(Throwable error) {
        if (error instanceof WebClientResponseException wcre) {
            HttpStatusCode status = wcre.getStatusCode();
            String body = wcre.getResponseBodyAsString();

            if (status.is4xxClientError()) {
                log.error("Client error: {} {}", status, body);
                return Mono.error(new ClientException("Client error: " + status + ", body: " + body));
            } else if (status.is5xxServerError()) {
                log.error("Server error: {} {}", status, body);
                return Mono.error(new ServerException("Server error: " + status + ", body: " + body));
            }
        } else if (error instanceof java.util.concurrent.TimeoutException) {
            log.error("Request timed out");
            return Mono.error(new TimeoutException("Request timed out after " + timeout.getSeconds() + " seconds"));
        } else if(Exceptions.isRetryExhausted(error)){
            log.error("Retry exhausted ");
            return Mono.error(new TimeoutException("Retry exhausted after " + retry + " attempts"));
        }
        log.error("Unexpected error", error);
        return Mono.error(new GenericApiException("An unexpected error occurred", error));
    }

Log request and response of requests

  • To log request and reponse in webclient we can utilize the filters.

  • We can use ExchangeFilterFunction that have methods like ofRequestProcessor and ofResponseProcessor .

  • Create log filters for request and response and attach in configuration.

private WebClient getBuilder(String applicationProperties) {
    return WebClient.builder()
           .baseUrl(applicationProperties)
           .defaultHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
           .filter(logRequest())
           .filter(logResponse())
           .build();
}

private ExchangeFilterFunction logRequest() {
    return ExchangeFilterFunction.ofRequestProcessor(request -> {
        log.info("Request: {} {}", request.method(), request.url());
        return Mono.just(request);
    });
}

private ExchangeFilterFunction logResponse() {
    return ExchangeFilterFunction.ofResponseProcessor(response -> {
        log.info("Response: {}", response.statusCode());
        return Mono.just(response);
    });
}

Multiple web clients bean

Yes we can have multiple webclients bean present in the system. We need to create multiple beans with different bean names , then whenever required can use @Qualifier to inject the bean required.

// in configuratrion 
    @Bean
    public WebClient userWebClient(){
        return getBuilder(applicationProperties.getUserServiceUrl());
    }

    @Bean
    public WebClient authWebClient(){
        return getBuilder(applicationProperties.getAuthServiceUrl());
    }

    @Bean
    public WebClient vendorWebClient(){
        return getBuilder(applicationProperties.getVendorServiceUrl());
    }

// usages
@Qualifier("vendorWebClient") WebClient webClient

Custom Headers and Query Parameters

Add custom headers and query parameters dynamically.

public Mono<String> fetchDataWithCustomHeadersAndParams(String paramValue) {
    return webClient.get()
        .uri(uriBuilder -> uriBuilder
            .path("/data")
            .queryParam("param", paramValue)
            .build())
        .header("Custom-Header", "value")
        .retrieve()
        .bodyToMono(String.class);
}

Parallel requests

We can utilize zip method from reactive streams for this .

public Mono<String> fetchParallelData() {
    Mono<String> data1 = webClient.get()
        .uri("/data1")
        .retrieve()
        .bodyToMono(String.class);

    Mono<String> data2 = webClient.get()
        .uri("/data2")
        .retrieve()
        .bodyToMono(String.class);

    return Mono.zip(data1, data2, (d1, d2) -> d1 + " " + d2);
}

Streaming Data

Can be used to stream data from an API (e.g., Server-Sent Events or large JSON arrays).

public Flux<String> streamData() {
    return webClient.get()
        .uri("/stream")
        .retrieve()
        .bodyToFlux(String.class); // Stream data as Flux
}

Caching

Implement caching for responses.

public Mono<String> fetchDataWithCache() {
    return webClient.get()
        .uri("/data")
        .retrieve()
        .bodyToMono(String.class)
        .cache(Duration.ofMinutes(10)); // Cache the response for 10 minutes
}

Reactive Caching with Redis

Use Redis for reactive caching of responses.

public Mono<String> fetchDataWithRedisCache() {
    return webClient.get()
        .uri("/data")
        .retrieve()
        .bodyToMono(String.class)
        .cache(Duration.ofMinutes(10))
        .flatMap(data -> redisTemplate.opsForValue().set("cacheKey", data).thenReturn(data));
}

File Upload

Upload a file using WebClient with multipart/form-data.

public Mono<String> uploadFile(File file) {
    MultipartBodyBuilder builder = new MultipartBodyBuilder();
    builder.part("file", new FileSystemResource(file));

    return webClient.post()
        .uri("/upload")
        .body(BodyInserters.fromMultipartData(builder.build()))
        .retrieve()
        .bodyToMono(String.class);
}

File Download

Download a file and save it to the local filesystem.

public Mono<Void> downloadFile(String url, Path savePath) {
    return webClient.get()
        .uri(url)
        .retrieve()
        .bodyToMono(Resource.class)
        .flatMap(resource -> {
            try {
                Files.copy(resource.getInputStream(), savePath, StandardCopyOption.REPLACE_EXISTING);
                return Mono.empty();
            } catch (IOException e) {
                return Mono.error(e);
            }
        });
}

Custom Serialization/Deserialisation

Use custom serialization/deserialisation for requests and responses.

public Mono<MyData> fetchDataWithCustomDeserialization() {
    return webClient.get()
        .uri("/data")
        .retrieve()
        .bodyToMono(new ParameterizedTypeReference<MyData>() {});
}

Custom SSL Configuration

Configure custom SSL settings .

public WebClient createWebClientWithCustomSSL() {
    SslContext sslContext = SslContextBuilder.forClient().build();
    HttpClient httpClient = HttpClient.create().secure(sslContextSpec -> sslContextSpec.sslContext(sslContext));

    return WebClient.builder()
        .clientConnector(new ReactorClientHttpConnector(httpClient))
        .build();
}

Custom Metrics

Add custom metrics for monitoring WebClient requests.

public Mono<String> fetchDataWithMetrics() {
    return webClient.get()
        .uri("/data")
        .retrieve()
        .bodyToMono(String.class)
        .metrics() // Add metrics
        .name("webclient.requests")
        .tag("uri", "/data")
        .register(registry); // attach to registery
}

Last updated