Skip to content

Commit 7adeb46

Browse files
committed
WebClient exposes API for access to native request
Closes gh-25115, gh-25493
1 parent 0f7ad1b commit 7adeb46

File tree

13 files changed

+148
-12
lines changed

13 files changed

+148
-12
lines changed

spring-test/src/main/java/org/springframework/mock/http/client/reactive/MockClientHttpRequest.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,12 @@ public DataBufferFactory bufferFactory() {
104104
return DefaultDataBufferFactory.sharedInstance;
105105
}
106106

107+
@Override
108+
@SuppressWarnings("unchecked")
109+
public <T> T getNativeRequest() {
110+
return (T) this;
111+
}
112+
107113
@Override
108114
protected void applyHeaders() {
109115
}

spring-web/src/main/java/org/springframework/http/client/reactive/ClientHttpRequest.java

Lines changed: 8 additions & 1 deletion
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-2020 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.
@@ -47,4 +47,11 @@ public interface ClientHttpRequest extends ReactiveHttpOutputMessage {
4747
*/
4848
MultiValueMap<String, HttpCookie> getCookies();
4949

50+
/**
51+
* Return the request from the underlying HTTP library.
52+
* @param <T> the expected type of the request to cast to
53+
* @since 5.3
54+
*/
55+
<T> T getNativeRequest();
56+
5057
}

spring-web/src/main/java/org/springframework/http/client/reactive/ClientHttpRequestDecorator.java

Lines changed: 6 additions & 1 deletion
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-2020 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.
@@ -80,6 +80,11 @@ public DataBufferFactory bufferFactory() {
8080
return this.delegate.bufferFactory();
8181
}
8282

83+
@Override
84+
public <T> T getNativeRequest() {
85+
return this.delegate.getNativeRequest();
86+
}
87+
8388
@Override
8489
public void beforeCommit(Supplier<? extends Mono<Void>> action) {
8590
this.delegate.beforeCommit(action);

spring-web/src/main/java/org/springframework/http/client/reactive/HttpComponentsClientHttpRequest.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,12 @@ public DataBufferFactory bufferFactory() {
9494
return this.dataBufferFactory;
9595
}
9696

97+
@Override
98+
@SuppressWarnings("unchecked")
99+
public <T> T getNativeRequest() {
100+
return (T) this.httpRequest;
101+
}
102+
97103
@Override
98104
public Mono<Void> writeWith(Publisher<? extends DataBuffer> body) {
99105
return doCommit(() -> {

spring-web/src/main/java/org/springframework/http/client/reactive/JettyClientHttpRequest.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,12 @@ public DataBufferFactory bufferFactory() {
8484
return this.bufferFactory;
8585
}
8686

87+
@Override
88+
@SuppressWarnings("unchecked")
89+
public <T> T getNativeRequest() {
90+
return (T) this.jettyRequest;
91+
}
92+
8793
@Override
8894
public Mono<Void> writeWith(Publisher<? extends DataBuffer> body) {
8995
return Mono.<Void>create(sink -> {

spring-web/src/main/java/org/springframework/http/client/reactive/ReactorClientHttpRequest.java

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -64,11 +64,6 @@ public ReactorClientHttpRequest(HttpMethod method, URI uri, HttpClientRequest re
6464
}
6565

6666

67-
@Override
68-
public DataBufferFactory bufferFactory() {
69-
return this.bufferFactory;
70-
}
71-
7267
@Override
7368
public HttpMethod getMethod() {
7469
return this.httpMethod;
@@ -79,6 +74,17 @@ public URI getURI() {
7974
return this.uri;
8075
}
8176

77+
@Override
78+
public DataBufferFactory bufferFactory() {
79+
return this.bufferFactory;
80+
}
81+
82+
@Override
83+
@SuppressWarnings("unchecked")
84+
public <T> T getNativeRequest() {
85+
return (T) this.request;
86+
}
87+
8288
@Override
8389
public Mono<Void> writeWith(Publisher<? extends DataBuffer> body) {
8490
return doCommit(() -> {

spring-web/src/testFixtures/java/org/springframework/web/testfixture/http/client/reactive/MockClientHttpRequest.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,12 @@ public DataBufferFactory bufferFactory() {
110110
return DefaultDataBufferFactory.sharedInstance;
111111
}
112112

113+
@Override
114+
@SuppressWarnings("unchecked")
115+
public <T> T getNativeRequest() {
116+
return (T) this;
117+
}
118+
113119
@Override
114120
protected void applyHeaders() {
115121
}

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

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
import org.springframework.http.HttpHeaders;
2929
import org.springframework.http.HttpMethod;
3030
import org.springframework.http.client.reactive.ClientHttpRequest;
31+
import org.springframework.lang.Nullable;
3132
import org.springframework.util.MultiValueMap;
3233
import org.springframework.web.reactive.function.BodyInserter;
3334

@@ -94,6 +95,14 @@ default Optional<Object> attribute(String name) {
9495
*/
9596
Map<String, Object> attributes();
9697

98+
/**
99+
* Return consumer(s) configured to access to the {@link ClientHttpRequest}.
100+
* @since 5.3
101+
*/
102+
@Nullable
103+
Consumer<ClientHttpRequest> httpRequest();
104+
105+
97106
/**
98107
* Return a log message prefix to use to correlate messages for this request.
99108
* The prefix is based on the value of the attribute {@link #LOG_ID_ATTRIBUTE
@@ -251,6 +260,18 @@ interface Builder {
251260
*/
252261
Builder attributes(Consumer<Map<String, Object>> attributesConsumer);
253262

263+
/**
264+
* Callback for access to the {@link ClientHttpRequest} that in turn
265+
* provides access to the native request of the underlying HTTP library.
266+
* This could be useful for setting advanced, per-request options that
267+
* exposed by the underlying library.
268+
* @param requestConsumer a consumer to access the
269+
* {@code ClientHttpRequest} with
270+
* @return this builder
271+
* @since 5.3
272+
*/
273+
Builder httpRequest(Consumer<ClientHttpRequest> requestConsumer);
274+
254275
/**
255276
* Build the request.
256277
*/

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

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
import org.springframework.http.client.reactive.ClientHttpRequest;
3636
import org.springframework.http.codec.HttpMessageWriter;
3737
import org.springframework.http.server.reactive.ServerHttpRequest;
38+
import org.springframework.lang.Nullable;
3839
import org.springframework.util.Assert;
3940
import org.springframework.util.CollectionUtils;
4041
import org.springframework.util.LinkedMultiValueMap;
@@ -63,6 +64,9 @@ final class DefaultClientRequestBuilder implements ClientRequest.Builder {
6364

6465
private BodyInserter<?, ? super ClientHttpRequest> body = BodyInserters.empty();
6566

67+
@Nullable
68+
private Consumer<ClientHttpRequest> httpRequestConsumer;
69+
6670

6771
public DefaultClientRequestBuilder(ClientRequest other) {
6872
Assert.notNull(other, "ClientRequest must not be null");
@@ -72,6 +76,7 @@ public DefaultClientRequestBuilder(ClientRequest other) {
7276
cookies(cookies -> cookies.addAll(other.cookies()));
7377
attributes(attributes -> attributes.putAll(other.attributes()));
7478
body(other.body());
79+
this.httpRequestConsumer = other.httpRequest();
7580
}
7681

7782
public DefaultClientRequestBuilder(HttpMethod method, URI url) {
@@ -150,6 +155,13 @@ public ClientRequest.Builder attributes(Consumer<Map<String, Object>> attributes
150155
return this;
151156
}
152157

158+
@Override
159+
public ClientRequest.Builder httpRequest(Consumer<ClientHttpRequest> requestConsumer) {
160+
this.httpRequestConsumer = (this.httpRequestConsumer != null ?
161+
this.httpRequestConsumer.andThen(requestConsumer) : requestConsumer);
162+
return this;
163+
}
164+
153165
@Override
154166
public ClientRequest.Builder body(BodyInserter<?, ? super ClientHttpRequest> inserter) {
155167
this.body = inserter;
@@ -158,7 +170,9 @@ public ClientRequest.Builder body(BodyInserter<?, ? super ClientHttpRequest> ins
158170

159171
@Override
160172
public ClientRequest build() {
161-
return new BodyInserterRequest(this.method, this.url, this.headers, this.cookies, this.body, this.attributes);
173+
return new BodyInserterRequest(
174+
this.method, this.url, this.headers, this.cookies, this.body,
175+
this.attributes, this.httpRequestConsumer);
162176
}
163177

164178

@@ -176,18 +190,22 @@ private static class BodyInserterRequest implements ClientRequest {
176190

177191
private final Map<String, Object> attributes;
178192

193+
@Nullable
194+
private final Consumer<ClientHttpRequest> httpRequestConsumer;
195+
179196
private final String logPrefix;
180197

181198
public BodyInserterRequest(HttpMethod method, URI url, HttpHeaders headers,
182199
MultiValueMap<String, String> cookies, BodyInserter<?, ? super ClientHttpRequest> body,
183-
Map<String, Object> attributes) {
200+
Map<String, Object> attributes, @Nullable Consumer<ClientHttpRequest> httpRequestConsumer) {
184201

185202
this.method = method;
186203
this.url = url;
187204
this.headers = HttpHeaders.readOnlyHttpHeaders(headers);
188205
this.cookies = CollectionUtils.unmodifiableMultiValueMap(cookies);
189206
this.body = body;
190207
this.attributes = Collections.unmodifiableMap(attributes);
208+
this.httpRequestConsumer = httpRequestConsumer;
191209

192210
Object id = attributes.computeIfAbsent(LOG_ID_ATTRIBUTE, name -> ObjectUtils.getIdentityHexString(this));
193211
this.logPrefix = "[" + id + "] ";
@@ -223,6 +241,11 @@ public Map<String, Object> attributes() {
223241
return this.attributes;
224242
}
225243

244+
@Override
245+
public Consumer<ClientHttpRequest> httpRequest() {
246+
return this.httpRequestConsumer;
247+
}
248+
226249
@Override
227250
public String logPrefix() {
228251
return this.logPrefix;
@@ -245,6 +268,9 @@ public Mono<Void> writeTo(ClientHttpRequest request, ExchangeStrategies strategi
245268
requestCookies.add(name, cookie);
246269
}));
247270
}
271+
if (this.httpRequestConsumer != null) {
272+
this.httpRequestConsumer.accept(request);
273+
}
248274

249275
return this.body.insert(request, new BodyInserter.Context() {
250276
@Override

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

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -166,6 +166,10 @@ private class DefaultRequestBodyUriSpec implements RequestBodyUriSpec {
166166

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

169+
@Nullable
170+
private Consumer<ClientHttpRequest> httpRequestConsumer;
171+
172+
169173
DefaultRequestBodyUriSpec(HttpMethod httpMethod) {
170174
this.httpMethod = httpMethod;
171175
}
@@ -239,6 +243,13 @@ public RequestBodySpec attributes(Consumer<Map<String, Object>> attributesConsum
239243
return this;
240244
}
241245

246+
@Override
247+
public RequestBodySpec httpRequest(Consumer<ClientHttpRequest> requestConsumer) {
248+
this.httpRequestConsumer = (this.httpRequestConsumer != null ?
249+
this.httpRequestConsumer.andThen(requestConsumer) : requestConsumer);
250+
return this;
251+
}
252+
242253
@Override
243254
public DefaultRequestBodyUriSpec accept(MediaType... acceptableMediaTypes) {
244255
getHeaders().setAccept(Arrays.asList(acceptableMediaTypes));
@@ -344,10 +355,14 @@ private ClientRequest.Builder initRequestBuilder() {
344355
if (defaultRequest != null) {
345356
defaultRequest.accept(this);
346357
}
347-
return ClientRequest.create(this.httpMethod, initUri())
358+
ClientRequest.Builder builder = ClientRequest.create(this.httpMethod, initUri())
348359
.headers(headers -> headers.addAll(initHeaders()))
349360
.cookies(cookies -> cookies.addAll(initCookies()))
350361
.attributes(attributes -> attributes.putAll(this.attributes));
362+
if (this.httpRequestConsumer != null) {
363+
builder.httpRequest(this.httpRequestConsumer);
364+
}
365+
return builder;
351366
}
352367

353368
private URI initUri() {

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
@@ -470,6 +470,18 @@ interface RequestHeadersSpec<S extends RequestHeadersSpec<S>> {
470470
*/
471471
S attributes(Consumer<Map<String, Object>> attributesConsumer);
472472

473+
/**
474+
* Callback for access to the {@link ClientHttpRequest} that in turn
475+
* provides access to the native request of the underlying HTTP library.
476+
* This could be useful for setting advanced, per-request options that
477+
* exposed by the underlying library.
478+
* @param requestConsumer a consumer to access the
479+
* {@code ClientHttpRequest} with
480+
* @return {@code ResponseSpec} to specify how to decode the body
481+
* @since 5.3
482+
*/
483+
S httpRequest(Consumer<ClientHttpRequest> requestConsumer);
484+
473485
/**
474486
* Perform the HTTP request and retrieve the response body:
475487
* <p><pre>

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

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@
4646
import static org.springframework.http.HttpMethod.POST;
4747

4848
/**
49+
* Unit tests for {@link DefaultClientRequestBuilder}.
4950
* @author Arjen Poutsma
5051
*/
5152
public class DefaultClientRequestBuilderTests {
@@ -54,17 +55,20 @@ public class DefaultClientRequestBuilderTests {
5455
public void from() throws URISyntaxException {
5556
ClientRequest other = ClientRequest.create(GET, URI.create("https://example.com"))
5657
.header("foo", "bar")
57-
.cookie("baz", "qux").build();
58+
.cookie("baz", "qux")
59+
.httpRequest(request -> {})
60+
.build();
5861
ClientRequest result = ClientRequest.from(other)
5962
.headers(httpHeaders -> httpHeaders.set("foo", "baar"))
6063
.cookies(cookies -> cookies.set("baz", "quux"))
61-
.build();
64+
.build();
6265
assertThat(result.url()).isEqualTo(new URI("https://example.com"));
6366
assertThat(result.method()).isEqualTo(GET);
6467
assertThat(result.headers().size()).isEqualTo(1);
6568
assertThat(result.headers().getFirst("foo")).isEqualTo("baar");
6669
assertThat(result.cookies().size()).isEqualTo(1);
6770
assertThat(result.cookies().getFirst("baz")).isEqualTo("quux");
71+
assertThat(result.httpRequest()).isNotNull();
6872
}
6973

7074
@Test
@@ -100,6 +104,10 @@ public void build() {
100104
ClientRequest result = ClientRequest.create(GET, URI.create("https://example.com"))
101105
.header("MyKey", "MyValue")
102106
.cookie("foo", "bar")
107+
.httpRequest(request -> {
108+
MockClientHttpRequest nativeRequest = (MockClientHttpRequest) request.getNativeRequest();
109+
nativeRequest.getHeaders().add("MyKey2", "MyValue2");
110+
})
103111
.build();
104112

105113
MockClientHttpRequest request = new MockClientHttpRequest(GET, "/");
@@ -108,7 +116,9 @@ public void build() {
108116
result.writeTo(request, strategies).block();
109117

110118
assertThat(request.getHeaders().getFirst("MyKey")).isEqualTo("MyValue");
119+
assertThat(request.getHeaders().getFirst("MyKey2")).isEqualTo("MyValue2");
111120
assertThat(request.getCookies().getFirst("foo").getValue()).isEqualTo("bar");
121+
112122
StepVerifier.create(request.getBody()).expectComplete().verify();
113123
}
114124

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

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,16 @@ public void requestHeaderAndCookie() {
127127
assertThat(request.cookies().getFirst("id")).isEqualTo("123");
128128
}
129129

130+
@Test
131+
public void httpRequest() {
132+
this.builder.build().get().uri("/path")
133+
.httpRequest(httpRequest -> {})
134+
.exchange().block(Duration.ofSeconds(10));
135+
136+
ClientRequest request = verifyAndGetRequest();
137+
assertThat(request.httpRequest()).isNotNull();
138+
}
139+
130140
@Test
131141
public void defaultHeaderAndCookie() {
132142
WebClient client = this.builder

0 commit comments

Comments
 (0)