Skip to content

Commit 79f79e9

Browse files
committed
WebClient method to populate the Reactor Context
The alternative is to use a filter but this makes it a little easier and also guarantees that it will be downstream from all filters regardless of their order, and therefore the Context will be visible to all of them. Closes gh-25710
1 parent bd2640a commit 79f79e9

File tree

4 files changed

+127
-29
lines changed

4 files changed

+127
-29
lines changed

spring-webflux/src/main/java/org/springframework/web/reactive/function/client/DefaultWebClient.java

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
import org.reactivestreams.Publisher;
3434
import reactor.core.publisher.Flux;
3535
import reactor.core.publisher.Mono;
36+
import reactor.util.context.Context;
3637

3738
import org.springframework.core.ParameterizedTypeReference;
3839
import org.springframework.http.HttpHeaders;
@@ -173,6 +174,9 @@ private class DefaultRequestBodyUriSpec implements RequestBodyUriSpec {
173174

174175
private final Map<String, Object> attributes = new LinkedHashMap<>(4);
175176

177+
@Nullable
178+
private Function<Context, Context> contextModifier;
179+
176180
@Nullable
177181
private Consumer<ClientHttpRequest> httpRequestConsumer;
178182

@@ -298,6 +302,13 @@ public DefaultRequestBodyUriSpec ifNoneMatch(String... ifNoneMatches) {
298302
return this;
299303
}
300304

305+
@Override
306+
public RequestBodySpec context(Function<Context, Context> contextModifier) {
307+
this.contextModifier = (this.contextModifier != null ?
308+
this.contextModifier.andThen(contextModifier) : contextModifier);
309+
return this;
310+
}
311+
301312
@Override
302313
public RequestBodySpec httpRequest(Consumer<ClientHttpRequest> requestConsumer) {
303314
this.httpRequestConsumer = (this.httpRequestConsumer != null ?
@@ -412,9 +423,15 @@ public Mono<ClientResponse> exchange() {
412423
ClientRequest request = (this.inserter != null ?
413424
initRequestBuilder().body(this.inserter).build() :
414425
initRequestBuilder().build());
415-
return Mono.defer(() -> exchangeFunction.exchange(request)
416-
.checkpoint("Request to " + this.httpMethod.name() + " " + this.uri + " [DefaultWebClient]")
417-
.switchIfEmpty(NO_HTTP_CLIENT_RESPONSE_ERROR));
426+
return Mono.defer(() -> {
427+
Mono<ClientResponse> responseMono = exchangeFunction.exchange(request)
428+
.checkpoint("Request to " + this.httpMethod.name() + " " + this.uri + " [DefaultWebClient]")
429+
.switchIfEmpty(NO_HTTP_CLIENT_RESPONSE_ERROR);
430+
if (this.contextModifier != null) {
431+
responseMono = responseMono.contextWrite(this.contextModifier);
432+
}
433+
return responseMono;
434+
});
418435
}
419436

420437
private ClientRequest.Builder initRequestBuilder() {

spring-webflux/src/main/java/org/springframework/web/reactive/function/client/WebClient.java

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
import org.reactivestreams.Publisher;
3030
import reactor.core.publisher.Flux;
3131
import reactor.core.publisher.Mono;
32+
import reactor.util.context.Context;
3233

3334
import org.springframework.core.ParameterizedTypeReference;
3435
import org.springframework.core.ReactiveAdapterRegistry;
@@ -470,6 +471,17 @@ interface RequestHeadersSpec<S extends RequestHeadersSpec<S>> {
470471
*/
471472
S attributes(Consumer<Map<String, Object>> attributesConsumer);
472473

474+
/**
475+
* Provide a function to populate the Reactor {@code Context}. In contrast
476+
* to {@link #attribute(String, Object) attributes} which apply only to
477+
* the current request, the Reactor {@code Context} transparently propagates
478+
* to the downstream processing chain which may include other nested or
479+
* successive calls over HTTP or via other reactive clients.
480+
* @param contextModifier the function to modify the context with
481+
* @since 5.3.1
482+
*/
483+
S context(Function<Context, Context> contextModifier);
484+
473485
/**
474486
* Callback for access to the {@link ClientHttpRequest} that in turn
475487
* provides access to the native request of the underlying HTTP library.

spring-webflux/src/test/java/org/springframework/web/reactive/function/client/DefaultWebClientTests.java

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,34 @@ public void requestHeaderAndCookie() {
129129
assertThat(request.cookies().getFirst("id")).isEqualTo("123");
130130
}
131131

132+
@Test
133+
public void contextFromThreadLocal() {
134+
WebClient client = this.builder
135+
.filter((request, next) ->
136+
// Async, continue on different thread
137+
Mono.delay(Duration.ofMillis(10)).then(next.exchange(request)))
138+
.filter((request, next) ->
139+
Mono.deferContextual(contextView -> {
140+
String fooValue = contextView.get("foo");
141+
return next.exchange(ClientRequest.from(request).header("foo", fooValue).build());
142+
}))
143+
.build();
144+
145+
ThreadLocal<String> fooHolder = new ThreadLocal<>();
146+
fooHolder.set("bar");
147+
try {
148+
client.get().uri("/path")
149+
.context(context -> context.put("foo", fooHolder.get()))
150+
.retrieve().bodyToMono(Void.class).block(Duration.ofSeconds(10));
151+
}
152+
finally {
153+
fooHolder.remove();
154+
}
155+
156+
ClientRequest request = verifyAndGetRequest();
157+
assertThat(request.headers().getFirst("foo")).isEqualTo("bar");
158+
}
159+
132160
@Test
133161
public void httpRequest() {
134162
this.builder.build().get().uri("/path")
@@ -196,8 +224,6 @@ public void defaultHeaderAndCookieCopies() {
196224
request = verifyAndGetRequest();
197225
assertThat(request.headers().getFirst("Accept")).isEqualTo("application/xml");
198226
assertThat(request.cookies().getFirst("id")).isEqualTo("456");
199-
200-
201227
}
202228

203229
@Test

src/docs/asciidoc/web/webflux-webclient.adoc

Lines changed: 67 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -831,7 +831,7 @@ inline-style, through the built-in `BodyInserters`, as the following example sho
831831

832832

833833
[[webflux-client-filter]]
834-
== Client Filters
834+
== Filters
835835

836836
You can register a client filter (`ExchangeFilterFunction`) through the `WebClient.Builder`
837837
in order to intercept and modify requests, as the following example shows:
@@ -887,9 +887,36 @@ a filter for basic authentication through a static factory method:
887887
.build()
888888
----
889889

890-
Filters apply globally to every request. To change a filter's behavior for a specific
891-
request, you can add request attributes to the `ClientRequest` that can then be accessed
892-
by all filters in the chain, as the following example shows:
890+
You can create a new `WebClient` instance by using another as a starting point. This allows
891+
insert or removing filters without affecting the original `WebClient`. Below is an example
892+
that inserts a basic authentication filter at index 0:
893+
894+
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
895+
.Java
896+
----
897+
import static org.springframework.web.reactive.function.client.ExchangeFilterFunctions.basicAuthentication;
898+
899+
WebClient client = webClient.mutate()
900+
.filters(filterList -> {
901+
filterList.add(0, basicAuthentication("user", "password"));
902+
})
903+
.build();
904+
----
905+
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
906+
.Kotlin
907+
----
908+
val client = webClient.mutate()
909+
.filters { it.add(0, basicAuthentication("user", "password")) }
910+
.build()
911+
----
912+
913+
914+
[[webflux-client-attributes]]
915+
== Attributes
916+
917+
You can add attributes to a request. This is convenient if you want to pass information
918+
through the filter chain and influence the behavior of filters for a given request.
919+
For example:
893920

894921
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
895922
.Java
@@ -912,40 +939,56 @@ by all filters in the chain, as the following example shows:
912939
.Kotlin
913940
----
914941
val client = WebClient.builder()
915-
.filter { request, _ ->
916-
val usr = request.attributes()["myAttribute"];
917-
// ...
918-
}.build()
942+
.filter { request, _ ->
943+
val usr = request.attributes()["myAttribute"];
944+
// ...
945+
}
946+
.build()
919947
920948
client.get().uri("https://example.org/")
921949
.attribute("myAttribute", "...")
922950
.retrieve()
923951
.awaitBody<Unit>()
924952
----
925953

926-
You can also replicate an existing `WebClient`, insert new filters, or remove already
927-
registered filters. The following example, inserts a basic authentication filter at
928-
index 0:
954+
955+
[[webflux-client-context]]
956+
== Context
957+
958+
<<webflux-client-attributes>> provide a convenient way to pass information to the filter
959+
chain but they only influence the current request. If you want to pass information that
960+
propagates to additional requests that are nested, e.g. via `flatMap`, or executed after,
961+
e.g. via `concatMap`, then you'll need to use the Reactor `Context`.
962+
963+
`WebClient` exposes a method to populate the Reactor `Context` for a given request.
964+
This information is available to filters for the current request and it also propagates
965+
to subsequent requests or other reactive clients participating in the downstream
966+
processing chain. For example:
929967

930968
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
931969
.Java
932970
----
933-
import static org.springframework.web.reactive.function.client.ExchangeFilterFunctions.basicAuthentication;
934-
935-
WebClient client = webClient.mutate()
936-
.filters(filterList -> {
937-
filterList.add(0, basicAuthentication("user", "password"));
938-
})
971+
WebClient client = WebClient.builder()
972+
.filter((request, next) ->
973+
Mono.deferContextual(contextView -> {
974+
String value = contextView.get("foo");
975+
// ...
976+
}))
939977
.build();
940-
----
941-
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
942-
.Kotlin
943-
----
944-
val client = webClient.mutate()
945-
.filters { it.add(0, basicAuthentication("user", "password")) }
946-
.build()
978+
979+
client.get().uri("https://example.org/")
980+
.context(context -> context.put("foo", ...))
981+
.retrieve()
982+
.bodyToMono(String.class)
983+
.flatMap(body -> {
984+
// perform nested request (context propagates automatically)...
985+
});
947986
----
948987

988+
Note that you can also specify how to populate the context through the `defaultRequest`
989+
method at the level of the `WebClient.Builder` and that applies to all requests.
990+
This could be used for to example to pass information from `ThreadLocal` storage onto
991+
a Reactor processing chain in a Spring MVC application.
949992

950993

951994
[[webflux-client-synchronous]]

0 commit comments

Comments
 (0)