Spring WebFlux contains a client for making HTTP requests. WebClient has a functional, fluid API based on Reactor that allows you to declaratively compose asynchronous logic without the need to deal with threads or concurrency. It is completely non-blocking, supports streaming, and is based on the same codecs used to encode and decode the content of requests and responses on the server side.

WebClient requires an HTTP client library to run requests. There is built-in support for:

Configuration

The easiest way to create WebClient is to use one of the static factory methods:

  • WebClient.create()

  • WebClient.create(String baseUrl)

You can also use WebClient.builder() with additional parameters:

  • uriBuilderFactory: Configured UriBuilderFactory to be used as the base URL.

  • defaultUriVariables: Default values to use when expanding URI templates.

  • defaultHeader: Headers for each request.

  • defaultCookie: Cookies for each request.

  • defaultRequest: Consumer to configure each request.

  • filter: Client filter for each request.

  • exchangeStrategies: Settings for reading/writing HTTP messages.

  • clientConnector: Client HTTP library settings.

For example

Java

WebClient client = WebClient.builder()
.codecs(configurer -> ... )
.build();
Kotlin

val webClient = WebClient.builder()
.codecs { configurer -> ... }
.build()

Once created, the WebClient is immutable. However, you can clone it and create a modified copy as follows:

Java

WebClient client1 = WebClient.builder()
.filter(filterA).filter(filterB).build();
WebClient client2 = client1.mutate()
.filter(filterC).filter(filterD).build();
// client1 имеет filterA, filterB
// client2 имеет filterA, filterB, filterC, filterD
Kotlin

val client1 = WebClient.builder()
.filter(filterA).filter(filterB).build()
val client2 = client1.mutate()
.filter(filterC).filter(filterD).build()
// client1 имеет filterA, filterB
// client2 имеет filterA, filterB, filterC, filterD

MaxInMemorySize

Codecs have limitations on buffering data in memory to avoid application memory problems. By default they are set to 256 KB. If this is not enough, the following error will be received:

org.springframework.core.io.buffer.DataBufferLimitException: Exceeded limit on max bytes to buffer

To change the default limit for codecs, do the following:

Java

WebClient webClient = WebClient.builder()
.codecs(configurer -> configurer.defaultCodecs().maxInMemorySize(2 * 1024 * 1024))
.build();
Kotlin

val webClient = WebClient.builder()
.codecs { configurer -> configurer.defaultCodecs().maxInMemorySize(2 * 1024 * 1024) }
.build()

Reactor Netty

To configure Reactor Netty parameters, provide a pre-configured HttpClient:

Java

HttpClient httpClient = HttpClient.create().secure(sslSpec -> ...);
WebClient webClient = WebClient.builder()
.clientConnector(new ReactorClientHttpConnector(httpClient))
.build();
Kotlin

val httpClient = HttpClient.create().secure { ... }
val webClient = WebClient.builder()
.clientConnector(ReactorClientHttpConnector(httpClient))
.build()

Resources

By default, HttpClient participates in the use of global Reactor Netty resources stored in reactor.netty.http.HttpResources, including event loop threads and pool connections. This mode is recommended because for the purpose of concurrency in event wait loops, it is preferable to use fixed, shared resources. In this mode, global resources remain active until the process terminates.

If the server is synchronized with the process, there is usually no need to terminate the process explicitly. However, if the server can be started or stopped in-process (as is the case with a Spring MVC application deployed as a WAR file), then you can declare a Spring managed bean of type ReactorResourceFactory with the parameter globalResources=true (default) so that the use of Reactor Netty global resources is guaranteed to end when the ApplicationContext is closed from Spring, as shown in the following example:

Java

@Bean
public ReactorResourceFactory reactorResourceFactory() {
return new ReactorResourceFactory();
}
Kotlin

@Bean
fun reactorResourceFactory() = ReactorResourceFactory()

You can also avoid using Reactor Netty global resources. However, in this mode, you are responsible for ensuring that all instances of the Reactor Netty client and server share resources, as shown in the following example:

Java

@Bean
public ReactorResourceFactory resourceFactory() {
ReactorResourceFactory factory = new ReactorResourceFactory();
factory.setUseGlobalResources(false); 
return factory;
}
@Bean
public WebClient webClient() {
Function<HttpClient, HttpClient> mapper = client -> {
// Further configuration...
};
ClientHttpConnector connector =
    new ReactorClientHttpConnector(resourceFactory(), mapper); 
return WebClient.builder().clientConnector(connector).build(); 
}
        
  1. We create resources independent of global.
  2. Use the ReactorClientHttpConnector constructor with the resource factory.
  3. Connect the connector to WebClient.Builder.
Kotlin

@Bean
fun resourceFactory() = ReactorResourceFactory().apply {
isUseGlobalResources = false 
}
@Bean
fun webClient(): WebClient {
val mapper: (HttpClient) -> HttpClient = {
// Further configuration...
}
val connector = ReactorClientHttpConnector(resourceFactory(), mapper) 
return WebClient.builder().clientConnector(connector).build() 
}
  1. We create resources independent of global.
  2. Use the ReactorClientHttpConnector constructor with the resource factory.
  3. Connect the connector to WebClient.Builder.

Timeout

Configuring connection timeout values:

Java

import io.netty.channel.ChannelOption;
HttpClient httpClient = HttpClient.create()
.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 10000);
WebClient webClient = WebClient.builder()
.clientConnector(new ReactorClientHttpConnector(httpClient))
.build();
Kotlin

import io.netty.channel.ChannelOption
val httpClient = HttpClient.create()
.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 10000);
val webClient = WebClient.builder()
.clientConnector(new ReactorClientHttpConnector(httpClient))
.build();

Setting the read and write timeout value:

Java

import io.netty.handler.timeout.ReadTimeoutHandler;
import io.netty.handler.timeout.WriteTimeoutHandler;
HttpClient httpClient = HttpClient.create()
.doOnConnected(conn -> conn
        .addHandlerLast(new ReadTimeoutHandler(10))
        .addHandlerLast(new WriteTimeoutHandler(10)));
// Create a WebClient...
Kotlin

import io.netty.handler.timeout.ReadTimeoutHandler
import io.netty.handler.timeout.WriteTimeoutHandler
val httpClient = HttpClient.create()
.doOnConnected { conn -> conn
        .addHandlerLast(new ReadTimeoutHandler(10))
        .addHandlerLast(new WriteTimeoutHandler(10))
}// Create a WebClient...

Setting the response timeout for a specific request:

Java

HttpClient httpClient = HttpClient.create()
.responseTimeout(Duration.ofSeconds(2));
// Create a WebClient...
Kotlin

val httpClient = HttpClient.create()
.responseTimeout(Duration.ofSeconds(2));
// Create a WebClient...

The following example shows how to configure the HttpClient from Jetty:

Java

WebClient.create().get()
.uri("https://example.org/path")
.httpRequest(httpRequest -> {
    HttpClientRequest reactorRequest = httpRequest.getNativeRequest();
    reactorRequest.responseTimeout(Duration.ofSeconds(2));
})
.retrieve()
.bodyToMono(String.class);
Kotlin

WebClient.create().get()
.uri("https://example.org/path")
.httpRequest { httpRequest: ClientHttpRequest ->
    val reactorRequest = httpRequest.getNativeRequest<HttpClientRequest>()
    reactorRequest.responseTimeout(Duration.ofSeconds(2))
}
.retrieve()
.bodyToMono(String::class.java)

Jetty

The following example shows how to configure HttpClient settings from Jetty:

Java

HttpClient httpClient = new HttpClient();
httpClient.setCookieStore(...);
WebClient webClient = WebClient.builder()
.clientConnector(new JettyClientHttpConnector(httpClient))
.build();
Kotlin

 val httpClient = HttpClient()
httpClient.cookieStore = ...
val webClient = WebClient.builder()
.clientConnector(new JettyClientHttpConnector(httpClient))
.build();

By default, HttpClient creates its own resources (Executor, ByteBufferPool, Scheduler) that remain active until the process completes execution or calling the stop() function.

It is possible to share resources among multiple instances of the Jetty client (and server) and ensure that resource use ends when the ApplicationContext is closed from Spring, by declaring a Spring managed bean of type JettyResourceFactory, as shown in the following example:

Java

@Bean
public JettyResourceFactory resourceFactory() {
return new JettyResourceFactory();
}
@Bean
public WebClient webClient() {
HttpClient httpClient = new HttpClient();
// Further configuration...
ClientHttpConnector connector =
    new JettyClientHttpConnector(httpClient, resourceFactory()); 
return WebClient.builder().clientConnector(connector).build(); 
}
  1. Use the constructor JettyClientHttpConnector with a resource factory.
  2. Connect the connector to WebClient.Builder.
Kotlin

@Bean
fun resourceFactory() = JettyResourceFactory()
@Bean
fun webClient(): WebClient {
val httpClient = HttpClient()
// Further configuration...
val connector = JettyClientHttpConnector(httpClient, resourceFactory()) 
return WebClient.builder().clientConnector(connector).build() 
}
  1. Use the JettyClientHttpConnector constructor with a resource factory.
  2. Connect the connector to WebClient.Builder.

HttpComponents

The following example shows how to configure HttpClient parameters from Apache HttpComponents:

Java

HttpAsyncClientBuilder clientBuilder = HttpAsyncClients.custom();
clientBuilder.setDefaultRequestConfig(...);
CloseableHttpAsyncClient client = clientBuilder.build();
ClientHttpConnector connector = new HttpComponentsClientHttpConnector(client);
WebClient webClient = WebClient.builder().clientConnector(connector).build();
Kotlin

val client = HttpAsyncClients.custom().apply {
setDefaultRequestConfig(...)
}.build()
val connector = HttpComponentsClientHttpConnector(client)
val webClient = WebClient.builder().clientConnector(connector).build()

retrieve()

The retrieve() method can be used to declare how to retrieve the response. For example:

Java

WebClient client = WebClient.create("https://example.org");
Mono<ResponseEntity<Person>> result = client.get()
.uri("/persons/{id}", id).accept(MediaType.APPLICATION_JSON)
.retrieve()
.toEntity(Person.class);
Kotlin

val client = WebClient.create("https://example.org")
val result = client.get()
.uri("/persons/{id}", id).accept(MediaType.APPLICATION_JSON)
.retrieve()
.toEntity<Person>().awaitSingle()

Or we get only the body:

Java

WebClient client = WebClient.create("https://example.org");
Mono<Person> result = client.get()
.uri("/persons/{id}", id).accept(MediaType.APPLICATION_JSON)
.retrieve()
.bodyToMono(Person.class);
Kotlin

val client = WebClient.create("https://example.org")
val result = client.get()
.uri("/persons/{id}", id).accept(MediaType.APPLICATION_JSON)
.retrieve()
.awaitBody<Person>()

Getting a stream of decoded objects:

Java

Flux<Quote> result = client.get()
.uri("/quotes").accept(MediaType.TEXT_EVENT_STREAM)
.retrieve()
.bodyToFlux(Quote.class);
Kotlin

val result = client.get()
.uri("/quotes").accept(MediaType.TEXT_EVENT_STREAM)
.retrieve()
.bodyToFlow<Quote>()

By default, 4xx or 5xx responses result in a WebClientResponseException, including subclasses for certain HTTP status codes. To configure how error messages are handled, use onStatus handlers as follows:

Java

Mono<Person> result = client.get()
.uri("/persons/{id}", id).accept(MediaType.APPLICATION_JSON)
.retrieve()
.onStatus(HttpStatus::is4xxClientError, response -> ...)
.onStatus(HttpStatus::is5xxServerError, response -> ...)
.bodyToMono(Person.class);
Kotlin

val result = client.get()
.uri("/persons/{id}", id).accept(MediaType.APPLICATION_JSON)
.retrieve()
.onStatus(HttpStatus::is4xxClientError) { ... }
.onStatus(HttpStatus::is5xxServerError) { ... }
.awaitBody<Person>()

Exchange

Methods exchangeToMono() and exchangeToFlux() (or awaitExchange { } and exchangeToFlow { } in Kotlin) useful for more complex cases that require more control, such as decoding a response differently depending on the status of the response:

Java

Mono<Person> entityMono = client.get()
.uri("/persons/1")
.accept(MediaType.APPLICATION_JSON)
.exchangeToMono(response -> {
    if (response.statusCode().equals(HttpStatus.OK)) {
        return response.bodyToMono(Person.class);
    }
    else {
        // Address the error
        return response.createException().flatMap(Mono::error);
    }
});
Kotlin

val entity = client.get()
.uri("/persons/1")
.accept(MediaType.APPLICATION_JSON)
.awaitExchange {
if (response.statusCode() == HttpStatus.OK) {
     return response.awaitBody<Person>()
}
else {
     throw response.createExceptionAndAwait()
}
}

When using the above code, after completion of the returned Mono or Flux, the response body is checked and, if not used, freed to prevent memory and connection leaks. Therefore, the response cannot be decoded further downstream. The provided function should determine how to decode the response if necessary.

Request Body

The request body can be encoded from any asynchronous type handled by ReactiveAdapterRegistry, such as Mono or Deferred from Kotlin coroutines, as shown in the following example:

Java

Mono<Person> personMono = ... ;
Mono<Void> result = client.post()
.uri("/persons/{id}", id)
.contentType(MediaType.APPLICATION_JSON)
.body(personMono, Person.class)
.retrieve()
.bodyToMono(Void.class);
Kotlin

val personDeferred: Deferred<Person> = ...
client.post()
.uri("/persons/{id}", id)
.contentType(MediaType.APPLICATION_JSON)
.body<Person>(personDeferred)
.retrieve()
.awaitBody<Unit>()

Can also be coded stream of objects, as shown in the following example:

Java

Flux<Person> personFlux = ... ;
Mono<Void> result = client.post()
.uri("/persons/{id}", id)
.contentType(MediaType.APPLICATION_STREAM_JSON)
.body(personFlux, Person.class)
.retrieve()
.bodyToMono(Void.class);
Kotlin

val people: Flow<Person> = ...
client.post()
.uri("/persons/{id}", id)
.contentType(MediaType.APPLICATION_JSON)
.body(people)
.retrieve()
.awaitBody<Unit>()

In addition, if there is an actual value, then you can use the bodyValue shortcut method, as shown in the following example:

Java

Person person = ... ;
Mono<Void> result = client.post()
.uri("/persons/{id}", id)
.contentType(MediaType.APPLICATION_JSON)
.bodyValue(person)
.retrieve()
.bodyToMono(Void.class);
Kotlin

val person: Person = ...
client.post()
.uri("/persons/{id}", id)
.contentType(MediaType.APPLICATION_JSON)
.bodyValue(person)
.retrieve()
.awaitBody<Unit>()

Form Data

To submit form data, you can specify MultiValueMap<String, String> as the body. Note that the content is automatically set to application/x-www-form-urlencoded using FormHttpMessageWriter. The following example shows how to use MultiValueMap<String, String>:

Java

MultiValueMap<String, String> formData = ... ;
Mono<Void> result = client.post()
.uri("/path", id)
.bodyValue(formData)
.retrieve()
.bodyToMono(Void.class);
Kotlin

val formData: MultiValueMap<String, String> = ...
client.post()
.uri("/path", id)
.bodyValue(formData)
.retrieve()
.awaitBody<Unit>()

You can also add form data inline using BodyInserters as shown in the following example:

Java

import static org.springframework.web.reactive.function.BodyInserters.*;
Mono<Void> result = client.post()
.uri("/path", id)
.body(fromFormData("k1", "v1").with("k2", "v2"))
.retrieve()
.bodyToMono(Void.class);
Kotlin

import org.springframework.web.reactive.function.BodyInserters.*
client.post()
.uri("/path", id)
.body(fromFormData("k1", "v1").with("k2", "v2"))
.retrieve()
.awaitBody<Unit>()

Multicomponent data

To send multicomponent data, you must specify a MultiValueMap<String, ?> whose values are either Object instances representing the content of the component or HttpEntity instances representing the content and component headers. MultipartBodyBuilder provides a convenient API for preparing a multipart request. The following example shows how to create a MultiValueMap<String, ?>:

Java

MultipartBodyBuilder builder = new MultipartBodyBuilder();
builder.part("fieldPart", "fieldValue");
builder.part("filePart1", new FileSystemResource("...logo.png"));
builder.part("jsonPart", new Person("Jason"));
builder.part("myPart", part); // Part from a server request
MultiValueMap<String, HttpEntity<?>> parts = builder.build();
Kotlin

val builder = MultipartBodyBuilder().apply {
part("fieldPart", "fieldValue")
part("filePart1", new FileSystemResource("...logo.png"))
part("jsonPart", new Person("Jason"))
part("myPart", part) // Part from a server request
}
val parts = builder.build()

In most cases it is not necessary to set Content-Type for each component. The content type is determined automatically based on the HttpMessageWriter selected for serialization, or, in the case of Resource, based on the file extension. If necessary, you can explicitly set MediaType for each component through one of the build tool's part overloads.

After preparing MultiValueMap The easiest way to pass it to the WebClient is through the body method, as shown in the following example:

Java

MultipartBodyBuilder builder = ...;
Mono<Void> result = client.post()
.uri("/path", id)
.body(builder.build())
.retrieve()
.bodyToMono(Void.class);
Kotlin

val builder: MultipartBodyBuilder = ...
client.post()
.uri("/path", id)
.body(builder.build())
.retrieve()
.awaitBody<Unit>()

If MultiValueMap contains at least a single non-String value that can also represent regular form data (i.e. application/x-www-form-urlencoded), no need to set Content-Type in multipart/form-data. This always happens when using MultipartBodyBuilder, which provides a HttpEntity wrapper function.

As an alternative to MultipartBodyBuilder, you can also provide inline-style multi-part content using inline BodyInserters, as shown in the following example:

Java

import static org.springframework.web.reactive.function.BodyInserters.*;
Mono<Void> result = client.post()
.uri("/path", id)
.body(fromMultipartData("fieldPart", "value").with("filePart", resource))
.retrieve()
.bodyToMono(Void.class);
Kotlin

import org.springframework.web.reactive.function.BodyInserters.*
client.post()
.uri("/path", id)
.body(fromMultipartData("fieldPart", "value").with("filePart", resource))
.retrieve()
.awaitBody<Unit>()

Filters

You can register a client filter ExchangeFilterFunction) through WebClient.Builder to intercept and modify requests, as shown in the following example :

Java

WebClient client = WebClient.builder()
.filter((request, next) -> {
    ClientRequest filtered = ClientRequest.from(request)
            .header("foo", "bar")
            .build();
    return next.exchange(filtered);
})
.build();
Kotlin

val client = WebClient.builder()
.filter { request, next ->
    val filtered = ClientRequest.from(request)
            .header("foo", "bar")
            .build()
    next.exchange(filtered)
}
.build()

This can be used to end-to-end functionality such as authentication. The following example uses a filter for basic authentication through a static factory method:

Java

import static org.springframework.web.reactive.function.client.ExchangeFilterFunctions.basicAuthentication;
WebClient client = WebClient.builder()
.filter(basicAuthentication("user", "password"))
.build();
Kotlin

import org.springframework.web.reactive.function.client.ExchangeFilterFunctions.basicAuthentication
val client = WebClient.builder()
.filter(basicAuthentication("user", "password"))
.build()

Filters can be added or removed by modifying an existing WebClient instance, which creates a new WebClient instance without affecting the original one. For example:

Java

import static org.springframework.web.reactive.function.client.ExchangeFilterFunctions.basicAuthentication;
WebClient client = webClient.mutate()
.filters(filterList -> {
    filterList.add(0, basicAuthentication("user", "password"));
})
.build();
Kotlin

val client = webClient.mutate()
.filters { it.add(0, basicAuthentication("user", "password")) }
.build()

WebClient is a thin interface above the filter chain, accompanied by an ExchangeFunction. It provides a workflow for making requests, encoding to and from higher-level objects, and helps ensure that the response content is always consumed. If filters process the response in any way, care must be taken to ensure that its contents are always consumed or otherwise propagated downstream to the WebClient, which will provide the same. Below is a filter that handles the UNAUTHORIZED status code but ensures that any response content, whether expected or not, is returned:

Java
 
public ExchangeFilterFunction renewTokenFilter() {
return (request, next) -> next.exchange(request).flatMap(response -> {
if (response.statusCode().value() == HttpStatus.UNAUTHORIZED.value()) {
    return response.releaseBody()
            .then(renewToken())
            .flatMap(token -> {
                ClientRequest newRequest = ClientRequest.from(request).build();
                return next.exchange(newRequest);
            });
} else {
    return Mono.just(response);
}
});
}
Kotlin

fun renewTokenFilter(): ExchangeFilterFunction? {
return ExchangeFilterFunction { request: ClientRequest?, next: ExchangeFunction ->
next.exchange(request!!).flatMap { response: ClientResponse ->
    if (response.statusCode().value() == HttpStatus.UNAUTHORIZED.value()) {
        return@flatMap response.releaseBody()
                .then(renewToken())
                .flatMap { token: String? ->
                    val newRequest = ClientRequest.from(request).build()
                    next.exchange(newRequest)
                }
    } else {
        return@flatMap Mono.just(response)
    }
}
}
}

Attributes

Attributes can be added to the request. This is convenient if you need to pass information along a chain of filters and influence the logic of the filters within a given request. For example:

Java

WebClient client = WebClient.builder()
.filter((request, next) -> {
    Optional<Object> usr = request.attribute("myAttribute");
    // ...
})
.build();
client.get().uri("https://example.org/")
.attribute("myAttribute", "...")
.retrieve()
.bodyToMono(Void.class);
}
Kotlin
val client = WebClient.builder()
.filter { request, _ ->
    val usr = request.attributes()["myAttribute"];
    // ...
}
.build()
client.get().uri("https://example.org/")
    .attribute("myAttribute", "...")
    .retrieve()
    .awaitBody<Unit>()

Note that you can globally configure a defaultRequest callback at the WebClient.Builder level, which allows attributes to be inserted into all requests, which can be used, for example, in an application on Spring MVC to populate request attributes based on ThreadLocal data.

Context

Attributes provide convenient transmission of information to the filter chain, but only affect the current request. If you need to pass information that extends to additional requests that are nested, for example, via flatMap, or executed after, for example, via concatMap, then you need to use Context from Reactor.

The Context from the Reactor project must be filled in at the end of the reactive chain so that it applies to all operations. For example:

Java
WebClient client = WebClient.builder()
.filter((request, next) ->
        Mono.deferContextual(contextView -> {
            String value = contextView.get("foo");
            // ...
        }))
.build();
client.get().uri("https://example.org/")
.retrieve()
.bodyToMono(String.class)
.flatMap(body -> {
        // execute a nested query (the context is propagated automatically)...
})
.contextWrite(context -> context.put("foo", ...));

Synchronous use

WebClient can be used in a synchronous style, blocking at the end to obtain the result:

Java
Person person = client.get().uri("/person/{id}", i).retrieve()
.bodyToMono(Person.class)
.block();
List<Person> persons = client.get().uri("/persons").retrieve()
.bodyToFlux(Person.class)
.collectList()
.block();
Kotlin
val person = runBlocking {
client.get().uri("/person/{id}", i).retrieve( )
    .awaitBody<Person>()
}
val persons = runBlocking {
client.get().uri("/persons").retrieve()
    .bodyToFlow<Person>()
    .toList()
}

However, if you need to make several calls, it is more efficient not to block each response individually, but to wait for the combined result:

Java
Mono<Person> personMono = client.get().uri("/person/{id}", personId)
.retrieve().bodyToMono(Person.class);
Mono<List<Hobby>> hobbiesMono = client.get().uri("/person/{id}/hobbies", personId)
.retrieve().bodyToFlux(Hobby.class).collectList();
Map<String, Object> data = Mono.zip(personMono, hobbiesMono, (person, hobbies) -> {
    Map<String, String> map = new LinkedHashMap<>();
    map.put("person", person);
    map.put("hobbies", hobbies);
    return map;
})
.block();
Kotlin
val data = runBlocking {
val personDeferred = async {
    client.get().uri("/person/{id}", personId)
            .retrieve().awaitBody<Person>()
}
val hobbiesDeferred = async {
    client.get().uri("/person/{id}/hobbies", personId)
            .retrieve().bodyToFlow<Hobby>().toList()
}
mapOf("person" to personDeferred.await(), "hobbies" to hobbiesDeferred.await())
}

The above is just one example. There are many other patterns and operators for creating a reactive pipeline that makes many remote calls, potentially multiple nested, interdependent, without blocking until the very end.

When using Flux or Mono you won't have to block the Spring MVC or Spring WebFlux controller at all. It will simply be possible to return the resulting reactive type from the controller method. The same principle applies to Kotlin and Spring WebFlux coroutines - just use a pause function or return Flow in the controller method.

Testing

To test code that uses WebClient, you can use a mock web server object, for example OkHttp MockWebServer. For an example of its use, see WebClientIntegrationTests in the Spring Framework test suite or example static server in the OkHttp repository.