Skip to content

Commit d463598

Browse files
committed
Defer ExchangeFilterFunction to subscription time
Prior to this commit, the `ExchangeFilterFunction` instances configured on a `WebClient` instance would be executed as soon as the `exchange` method would be called. This behavior is not consistent with the server side and can confuse filter developers as they'd need to manually `Mono.defer()` their implementations if they want to record metrics. This commit defers all `ExchangeFilterFunction` processing at subscription time. Fixes gh-22375
1 parent 4e47006 commit d463598

File tree

3 files changed

+46
-22
lines changed

3 files changed

+46
-22
lines changed

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

+3-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2018 the original author or authors.
2+
* Copyright 2002-2019 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -316,7 +316,8 @@ public Mono<ClientResponse> exchange() {
316316
ClientRequest request = (this.inserter != null ?
317317
initRequestBuilder().body(this.inserter).build() :
318318
initRequestBuilder().build());
319-
return exchangeFunction.exchange(request).switchIfEmpty(NO_HTTP_CLIENT_RESPONSE_ERROR);
319+
return Mono.defer(() -> exchangeFunction.exchange(request))
320+
.switchIfEmpty(NO_HTTP_CLIENT_RESPONSE_ERROR);
320321
}
321322

322323
private ClientRequest.Builder initRequestBuilder() {

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

+4-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2018 the original author or authors.
2+
* Copyright 2002-2019 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -23,7 +23,9 @@
2323
import org.springframework.util.Assert;
2424

2525
/**
26-
* Represents a function that filters an{@linkplain ExchangeFunction exchange function}.
26+
* Represents a function that filters an {@linkplain ExchangeFunction exchange function}.
27+
* <p>The filter is executed when a {@code Subscriber} subscribes to the
28+
* {@code Publisher} returned by the {@code WebClient}.
2729
*
2830
* @author Arjen Poutsma
2931
* @since 5.0

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

+39-18
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2018 the original author or authors.
2+
* Copyright 2002-2019 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -41,6 +41,7 @@
4141
* Unit tests for {@link DefaultWebClient}.
4242
*
4343
* @author Rossen Stoyanchev
44+
* @author Brian Clozel
4445
*/
4546
public class DefaultWebClientTests {
4647

@@ -56,14 +57,16 @@ public class DefaultWebClientTests {
5657
public void setup() {
5758
MockitoAnnotations.initMocks(this);
5859
this.exchangeFunction = mock(ExchangeFunction.class);
59-
when(this.exchangeFunction.exchange(this.captor.capture())).thenReturn(Mono.empty());
60+
ClientResponse mockResponse = mock(ClientResponse.class);
61+
when(this.exchangeFunction.exchange(this.captor.capture())).thenReturn(Mono.just(mockResponse));
6062
this.builder = WebClient.builder().baseUrl("/base").exchangeFunction(this.exchangeFunction);
6163
}
6264

6365

6466
@Test
6567
public void basic() {
66-
this.builder.build().get().uri("/path").exchange();
68+
this.builder.build().get().uri("/path")
69+
.exchange().block(Duration.ofSeconds(10));
6770

6871
ClientRequest request = verifyAndGetRequest();
6972
assertEquals("/base/path", request.url().toString());
@@ -75,34 +78,31 @@ public void basic() {
7578
public void uriBuilder() {
7679
this.builder.build().get()
7780
.uri(builder -> builder.path("/path").queryParam("q", "12").build())
78-
.exchange();
81+
.exchange().block(Duration.ofSeconds(10));
7982

8083
ClientRequest request = verifyAndGetRequest();
8184
assertEquals("/base/path?q=12", request.url().toString());
82-
verifyNoMoreInteractions(this.exchangeFunction);
8385
}
8486

8587
@Test
8688
public void uriBuilderWithPathOverride() {
8789
this.builder.build().get()
8890
.uri(builder -> builder.replacePath("/path").build())
89-
.exchange();
91+
.exchange().block(Duration.ofSeconds(10));
9092

9193
ClientRequest request = verifyAndGetRequest();
9294
assertEquals("/path", request.url().toString());
93-
verifyNoMoreInteractions(this.exchangeFunction);
9495
}
9596

9697
@Test
9798
public void requestHeaderAndCookie() {
9899
this.builder.build().get().uri("/path").accept(MediaType.APPLICATION_JSON)
99100
.cookies(cookies -> cookies.add("id", "123")) // SPR-16178
100-
.exchange();
101+
.exchange().block(Duration.ofSeconds(10));
101102

102103
ClientRequest request = verifyAndGetRequest();
103104
assertEquals("application/json", request.headers().getFirst("Accept"));
104105
assertEquals("123", request.cookies().getFirst("id"));
105-
verifyNoMoreInteractions(this.exchangeFunction);
106106
}
107107

108108
@Test
@@ -111,12 +111,11 @@ public void defaultHeaderAndCookie() {
111111
.defaultHeader("Accept", "application/json").defaultCookie("id", "123")
112112
.build();
113113

114-
client.get().uri("/path").exchange();
114+
client.get().uri("/path").exchange().block(Duration.ofSeconds(10));
115115

116116
ClientRequest request = verifyAndGetRequest();
117117
assertEquals("application/json", request.headers().getFirst("Accept"));
118118
assertEquals("123", request.cookies().getFirst("id"));
119-
verifyNoMoreInteractions(this.exchangeFunction);
120119
}
121120

122121
@Test
@@ -126,12 +125,14 @@ public void defaultHeaderAndCookieOverrides() {
126125
.defaultCookie("id", "123")
127126
.build();
128127

129-
client.get().uri("/path").header("Accept", "application/xml").cookie("id", "456").exchange();
128+
client.get().uri("/path")
129+
.header("Accept", "application/xml")
130+
.cookie("id", "456")
131+
.exchange().block(Duration.ofSeconds(10));
130132

131133
ClientRequest request = verifyAndGetRequest();
132134
assertEquals("application/xml", request.headers().getFirst("Accept"));
133135
assertEquals("456", request.cookies().getFirst("id"));
134-
verifyNoMoreInteractions(this.exchangeFunction);
135136
}
136137

137138
@Test
@@ -151,7 +152,8 @@ public void defaultRequest() {
151152

152153
try {
153154
context.set("bar");
154-
client.get().uri("/path").attribute("foo", "bar").exchange();
155+
client.get().uri("/path").attribute("foo", "bar")
156+
.exchange().block(Duration.ofSeconds(10));
155157
}
156158
finally {
157159
context.remove();
@@ -219,7 +221,7 @@ public void withStringAttribute() {
219221
this.builder.filter(filter).build()
220222
.get().uri("/path")
221223
.attribute("foo", "bar")
222-
.exchange();
224+
.exchange().block(Duration.ofSeconds(10));
223225

224226
assertEquals("bar", actual.get("foo"));
225227

@@ -238,7 +240,7 @@ public void withNullAttribute() {
238240
this.builder.filter(filter).build()
239241
.get().uri("/path")
240242
.attribute("foo", null)
241-
.exchange();
243+
.exchange().block(Duration.ofSeconds(10));
242244

243245
assertNull(actual.get("foo"));
244246

@@ -254,21 +256,40 @@ public void apply() {
254256
.defaultCookie("id", "123"))
255257
.build();
256258

257-
client.get().uri("/path").exchange();
259+
client.get().uri("/path").exchange().block(Duration.ofSeconds(10));
258260

259261
ClientRequest request = verifyAndGetRequest();
260262
assertEquals("application/json", request.headers().getFirst("Accept"));
261263
assertEquals("123", request.cookies().getFirst("id"));
262-
verifyNoMoreInteractions(this.exchangeFunction);
263264
}
264265

265266
@Test
266267
public void switchToErrorOnEmptyClientResponseMono() {
268+
ExchangeFunction exchangeFunction = mock(ExchangeFunction.class);
269+
when(exchangeFunction.exchange(any())).thenReturn(Mono.empty());
270+
WebClient.Builder builder = WebClient.builder().baseUrl("/base").exchangeFunction(exchangeFunction);
267271
StepVerifier.create(builder.build().get().uri("/path").exchange())
268272
.expectErrorMessage("The underlying HTTP client completed without emitting a response.")
269273
.verify(Duration.ofSeconds(5));
270274
}
271275

276+
@Test
277+
public void shouldApplyFiltersAtSubscription() {
278+
WebClient client = this.builder
279+
.filter((request, next) -> {
280+
return next.exchange(ClientRequest
281+
.from(request)
282+
.header("Custom", "value")
283+
.build());
284+
})
285+
.build();
286+
Mono<ClientResponse> exchange = client.get().uri("/path").exchange();
287+
verifyZeroInteractions(this.exchangeFunction);
288+
exchange.block(Duration.ofSeconds(10));
289+
ClientRequest request = verifyAndGetRequest();
290+
assertEquals("value", request.headers().getFirst("Custom"));
291+
}
292+
272293
private ClientRequest verifyAndGetRequest() {
273294
ClientRequest request = this.captor.getValue();
274295
Mockito.verify(this.exchangeFunction).exchange(request);

0 commit comments

Comments
 (0)