diff --git a/spring-test/src/main/java/org/springframework/mock/http/client/reactive/MockClientHttpRequest.java b/spring-test/src/main/java/org/springframework/mock/http/client/reactive/MockClientHttpRequest.java index 3291de25fb7..170909f1d86 100644 --- a/spring-test/src/main/java/org/springframework/mock/http/client/reactive/MockClientHttpRequest.java +++ b/spring-test/src/main/java/org/springframework/mock/http/client/reactive/MockClientHttpRequest.java @@ -119,6 +119,10 @@ public class MockClientHttpRequest extends AbstractClientHttpRequest { .forEach(cookie -> getHeaders().add(HttpHeaders.COOKIE, cookie.toString())); } + @Override + protected void applyAttributes() { + } + @Override public Mono writeWith(Publisher body) { return doCommit(() -> Mono.defer(() -> this.writeHandler.apply(Flux.from(body)))); diff --git a/spring-test/src/main/java/org/springframework/test/web/reactive/server/DefaultWebTestClientBuilder.java b/spring-test/src/main/java/org/springframework/test/web/reactive/server/DefaultWebTestClientBuilder.java index 61a5e47f5a6..74b2bb51838 100644 --- a/spring-test/src/main/java/org/springframework/test/web/reactive/server/DefaultWebTestClientBuilder.java +++ b/spring-test/src/main/java/org/springframework/test/web/reactive/server/DefaultWebTestClientBuilder.java @@ -94,6 +94,8 @@ class DefaultWebTestClientBuilder implements WebTestClient.Builder { @Nullable private MultiValueMap defaultCookies; + private boolean applyAttributes; + @Nullable private List filters; @@ -155,6 +157,7 @@ class DefaultWebTestClientBuilder implements WebTestClient.Builder { } this.defaultCookies = (other.defaultCookies != null ? new LinkedMultiValueMap<>(other.defaultCookies) : null); + this.applyAttributes = other.applyAttributes; this.filters = (other.filters != null ? new ArrayList<>(other.filters) : null); this.entityResultConsumer = other.entityResultConsumer; this.strategies = other.strategies; @@ -213,6 +216,12 @@ class DefaultWebTestClientBuilder implements WebTestClient.Builder { return this.defaultCookies; } + @Override + public WebTestClient.Builder applyAttributes(boolean applyAttributes) { + this.applyAttributes = applyAttributes; + return this; + } + @Override public WebTestClient.Builder filter(ExchangeFilterFunction filter) { Assert.notNull(filter, "ExchangeFilterFunction is required"); @@ -312,22 +321,25 @@ class DefaultWebTestClientBuilder implements WebTestClient.Builder { this.entityResultConsumer, this.responseTimeout, new DefaultWebTestClientBuilder(this)); } - private static ClientHttpConnector initConnector() { + private ClientHttpConnector initConnector() { + final ClientHttpConnector connector; if (reactorNettyClientPresent) { - return new ReactorClientHttpConnector(); + connector = new ReactorClientHttpConnector(); } else if (reactorNetty2ClientPresent) { return new ReactorNetty2ClientHttpConnector(); } else if (jettyClientPresent) { - return new JettyClientHttpConnector(); + connector = new JettyClientHttpConnector(); } else if (httpComponentsClientPresent) { - return new HttpComponentsClientHttpConnector(); + connector = new HttpComponentsClientHttpConnector(); } else { - return new JdkClientHttpConnector(); + connector = new JdkClientHttpConnector(); } + connector.setApplyAttributes(this.applyAttributes); + return connector; } private ExchangeStrategies initExchangeStrategies() { diff --git a/spring-test/src/main/java/org/springframework/test/web/reactive/server/HttpHandlerConnector.java b/spring-test/src/main/java/org/springframework/test/web/reactive/server/HttpHandlerConnector.java index f0cfd1ef69c..06a44d3d89a 100644 --- a/spring-test/src/main/java/org/springframework/test/web/reactive/server/HttpHandlerConnector.java +++ b/spring-test/src/main/java/org/springframework/test/web/reactive/server/HttpHandlerConnector.java @@ -64,6 +64,8 @@ public class HttpHandlerConnector implements ClientHttpConnector { private final HttpHandler handler; + private boolean applyAttributes = true; + /** * Constructor with the {@link HttpHandler} to handle requests with. @@ -82,6 +84,16 @@ public class HttpHandlerConnector implements ClientHttpConnector { .subscribeOn(Schedulers.parallel()); } + @Override + public void setApplyAttributes(boolean applyAttributes) { + this.applyAttributes = applyAttributes; + } + + @Override + public boolean getApplyAttributes() { + return this.applyAttributes; + } + private Mono doConnect( HttpMethod httpMethod, URI uri, Function> requestCallback) { diff --git a/spring-test/src/main/java/org/springframework/test/web/reactive/server/WebTestClient.java b/spring-test/src/main/java/org/springframework/test/web/reactive/server/WebTestClient.java index 96938ac7b79..f8a14f06c1a 100644 --- a/spring-test/src/main/java/org/springframework/test/web/reactive/server/WebTestClient.java +++ b/spring-test/src/main/java/org/springframework/test/web/reactive/server/WebTestClient.java @@ -425,6 +425,13 @@ public interface WebTestClient { */ Builder defaultCookies(Consumer> cookiesConsumer); + /** + * Global option to specify whether or not attributes should be applied to every request, + * if the used {@link ClientHttpConnector} allows it. + * @param applyAttributes whether or not to apply attributes + */ + Builder applyAttributes(boolean applyAttributes); + /** * Add the given filter to the filter chain. * @param filter the filter to be added to the chain diff --git a/spring-test/src/main/java/org/springframework/test/web/reactive/server/WiretapConnector.java b/spring-test/src/main/java/org/springframework/test/web/reactive/server/WiretapConnector.java index 1c5d91caeab..b9124c52f24 100644 --- a/spring-test/src/main/java/org/springframework/test/web/reactive/server/WiretapConnector.java +++ b/spring-test/src/main/java/org/springframework/test/web/reactive/server/WiretapConnector.java @@ -55,6 +55,8 @@ class WiretapConnector implements ClientHttpConnector { private final Map exchanges = new ConcurrentHashMap<>(); + private boolean applyAttributes = true; + WiretapConnector(ClientHttpConnector delegate) { this.delegate = delegate; @@ -84,6 +86,16 @@ class WiretapConnector implements ClientHttpConnector { }); } + @Override + public void setApplyAttributes(boolean applyAttributes) { + this.applyAttributes = applyAttributes; + } + + @Override + public boolean getApplyAttributes() { + return this.applyAttributes; + } + /** * Create the {@link ExchangeResult} for the given "request-id" header value. */ diff --git a/spring-test/src/main/java/org/springframework/test/web/servlet/client/MockMvcHttpConnector.java b/spring-test/src/main/java/org/springframework/test/web/servlet/client/MockMvcHttpConnector.java index 36a49b58c2b..1628803d60f 100644 --- a/spring-test/src/main/java/org/springframework/test/web/servlet/client/MockMvcHttpConnector.java +++ b/spring-test/src/main/java/org/springframework/test/web/servlet/client/MockMvcHttpConnector.java @@ -86,6 +86,8 @@ public class MockMvcHttpConnector implements ClientHttpConnector { private final List requestPostProcessors; + private boolean applyAttributes = true; + public MockMvcHttpConnector(MockMvc mockMvc) { this(mockMvc, Collections.emptyList()); @@ -115,6 +117,16 @@ public class MockMvcHttpConnector implements ClientHttpConnector { } } + @Override + public void setApplyAttributes(boolean applyAttributes) { + this.applyAttributes = applyAttributes; + } + + @Override + public boolean getApplyAttributes() { + return this.applyAttributes; + } + private RequestBuilder adaptRequest( HttpMethod httpMethod, URI uri, Function> requestCallback) { diff --git a/spring-test/src/test/java/org/springframework/test/web/reactive/server/WiretapConnectorTests.java b/spring-test/src/test/java/org/springframework/test/web/reactive/server/WiretapConnectorTests.java index 01f6828946f..c1ed8bd96f7 100644 --- a/spring-test/src/test/java/org/springframework/test/web/reactive/server/WiretapConnectorTests.java +++ b/spring-test/src/test/java/org/springframework/test/web/reactive/server/WiretapConnectorTests.java @@ -18,6 +18,7 @@ package org.springframework.test.web.reactive.server; import java.net.URI; import java.time.Duration; +import java.util.function.Function; import org.junit.jupiter.api.Test; import reactor.core.publisher.Mono; @@ -48,7 +49,22 @@ public class WiretapConnectorTests { public void captureAndClaim() { ClientHttpRequest request = new MockClientHttpRequest(HttpMethod.GET, "/test"); ClientHttpResponse response = new MockClientHttpResponse(HttpStatus.OK); - ClientHttpConnector connector = (method, uri, fn) -> fn.apply(request).then(Mono.just(response)); + ClientHttpConnector connector = new ClientHttpConnector() { + @Override + public Mono connect(HttpMethod method, URI uri, Function> requestCallback) { + return requestCallback.apply(request).then(Mono.just(response)); + } + + @Override + public void setApplyAttributes(boolean applyAttributes) { + + } + + @Override + public boolean getApplyAttributes() { + return false; + } + }; ClientRequest clientRequest = ClientRequest.create(HttpMethod.GET, URI.create("/test")) .header(WebTestClient.WEBTESTCLIENT_REQUEST_ID, "1").build(); diff --git a/spring-web/src/main/java/org/springframework/http/client/reactive/AbstractClientHttpRequest.java b/spring-web/src/main/java/org/springframework/http/client/reactive/AbstractClientHttpRequest.java index ecc2faaaf81..147885bb604 100644 --- a/spring-web/src/main/java/org/springframework/http/client/reactive/AbstractClientHttpRequest.java +++ b/spring-web/src/main/java/org/springframework/http/client/reactive/AbstractClientHttpRequest.java @@ -17,7 +17,10 @@ package org.springframework.http.client.reactive; import java.util.ArrayList; +import java.util.Collections; +import java.util.LinkedHashMap; import java.util.List; +import java.util.Map; import java.util.concurrent.atomic.AtomicReference; import java.util.function.Supplier; @@ -55,6 +58,10 @@ public abstract class AbstractClientHttpRequest implements ClientHttpRequest { private final MultiValueMap cookies; + private final Map attributes; + + private final boolean applyAttributes; + private final AtomicReference state = new AtomicReference<>(State.NEW); private final List>> commitActions = new ArrayList<>(4); @@ -64,13 +71,19 @@ public abstract class AbstractClientHttpRequest implements ClientHttpRequest { public AbstractClientHttpRequest() { - this(new HttpHeaders()); + this(new HttpHeaders(), false); } - public AbstractClientHttpRequest(HttpHeaders headers) { + public AbstractClientHttpRequest(boolean applyAttributes) { + this(new HttpHeaders(), applyAttributes); + } + + public AbstractClientHttpRequest(HttpHeaders headers, boolean applyAttributes) { Assert.notNull(headers, "HttpHeaders must not be null"); this.headers = headers; this.cookies = new LinkedMultiValueMap<>(); + this.attributes = new LinkedHashMap<>(); + this.applyAttributes = applyAttributes; } @@ -106,6 +119,14 @@ public abstract class AbstractClientHttpRequest implements ClientHttpRequest { return this.cookies; } + @Override + public Map getAttributes() { + if (State.COMMITTED.equals(this.state.get())) { + return Collections.unmodifiableMap(this.attributes); + } + return this.attributes; + } + @Override public void beforeCommit(Supplier> action) { Assert.notNull(action, "Action must not be null"); @@ -140,6 +161,9 @@ public abstract class AbstractClientHttpRequest implements ClientHttpRequest { Mono.fromRunnable(() -> { applyHeaders(); applyCookies(); + if (this.applyAttributes) { + applyAttributes(); + } this.state.set(State.COMMITTED); })); @@ -168,4 +192,10 @@ public abstract class AbstractClientHttpRequest implements ClientHttpRequest { */ protected abstract void applyCookies(); + /** + * Add additional attributes from {@link #getAttributes()} to the underlying request. + * This method is called once only. + */ + protected abstract void applyAttributes(); + } diff --git a/spring-web/src/main/java/org/springframework/http/client/reactive/ClientHttpConnector.java b/spring-web/src/main/java/org/springframework/http/client/reactive/ClientHttpConnector.java index 8de59c9c260..d0cf568670e 100644 --- a/spring-web/src/main/java/org/springframework/http/client/reactive/ClientHttpConnector.java +++ b/spring-web/src/main/java/org/springframework/http/client/reactive/ClientHttpConnector.java @@ -48,4 +48,14 @@ public interface ClientHttpConnector { Mono connect(HttpMethod method, URI uri, Function> requestCallback); + /** + * Set whether or not attributes should be applied to the underlying http-client library request. + */ + void setApplyAttributes(boolean applyAttributes); + + /** + * Whether or not attributes should be applied to the underlying http-client library request. + */ + boolean getApplyAttributes(); + } diff --git a/spring-web/src/main/java/org/springframework/http/client/reactive/ClientHttpRequest.java b/spring-web/src/main/java/org/springframework/http/client/reactive/ClientHttpRequest.java index 7a0183b870a..770a5eaeb0b 100644 --- a/spring-web/src/main/java/org/springframework/http/client/reactive/ClientHttpRequest.java +++ b/spring-web/src/main/java/org/springframework/http/client/reactive/ClientHttpRequest.java @@ -17,6 +17,7 @@ package org.springframework.http.client.reactive; import java.net.URI; +import java.util.Map; import org.springframework.http.HttpCookie; import org.springframework.http.HttpMethod; @@ -54,4 +55,9 @@ public interface ClientHttpRequest extends ReactiveHttpOutputMessage { */ T getNativeRequest(); + /** + * Return a mutable map of the request attributes. + */ + Map getAttributes(); + } diff --git a/spring-web/src/main/java/org/springframework/http/client/reactive/ClientHttpRequestDecorator.java b/spring-web/src/main/java/org/springframework/http/client/reactive/ClientHttpRequestDecorator.java index cb2948ac7d5..ade5faa44a2 100644 --- a/spring-web/src/main/java/org/springframework/http/client/reactive/ClientHttpRequestDecorator.java +++ b/spring-web/src/main/java/org/springframework/http/client/reactive/ClientHttpRequestDecorator.java @@ -17,6 +17,7 @@ package org.springframework.http.client.reactive; import java.net.URI; +import java.util.Map; import java.util.function.Supplier; import org.reactivestreams.Publisher; @@ -85,6 +86,11 @@ public class ClientHttpRequestDecorator implements ClientHttpRequest { return this.delegate.getNativeRequest(); } + @Override + public Map getAttributes() { + return this.delegate.getAttributes(); + } + @Override public void beforeCommit(Supplier> action) { this.delegate.beforeCommit(action); diff --git a/spring-web/src/main/java/org/springframework/http/client/reactive/HttpComponentsClientHttpConnector.java b/spring-web/src/main/java/org/springframework/http/client/reactive/HttpComponentsClientHttpConnector.java index 6e445868c2d..2e5b08fd474 100644 --- a/spring-web/src/main/java/org/springframework/http/client/reactive/HttpComponentsClientHttpConnector.java +++ b/spring-web/src/main/java/org/springframework/http/client/reactive/HttpComponentsClientHttpConnector.java @@ -59,6 +59,7 @@ public class HttpComponentsClientHttpConnector implements ClientHttpConnector, C private DataBufferFactory dataBufferFactory = DefaultDataBufferFactory.sharedInstance; + private boolean applyAttributes = true; /** * Default constructor that creates and starts a new instance of {@link CloseableHttpAsyncClient}. @@ -67,6 +68,7 @@ public class HttpComponentsClientHttpConnector implements ClientHttpConnector, C this(HttpAsyncClients.createDefault()); } + /** * Constructor with a pre-configured {@link CloseableHttpAsyncClient} instance. * @param client the client to use @@ -111,11 +113,22 @@ public class HttpComponentsClientHttpConnector implements ClientHttpConnector, C context.setCookieStore(new BasicCookieStore()); } - HttpComponentsClientHttpRequest request = - new HttpComponentsClientHttpRequest(method, uri, context, this.dataBufferFactory); + HttpComponentsClientHttpRequest request = new HttpComponentsClientHttpRequest( + method, uri, context, this.dataBufferFactory, this.applyAttributes); + return requestCallback.apply(request).then(Mono.defer(() -> execute(request, context))); } + @Override + public void setApplyAttributes(boolean applyAttributes) { + this.applyAttributes = applyAttributes; + } + + @Override + public boolean getApplyAttributes() { + return this.applyAttributes; + } + private Mono execute(HttpComponentsClientHttpRequest request, HttpClientContext context) { AsyncRequestProducer requestProducer = request.toRequestProducer(); diff --git a/spring-web/src/main/java/org/springframework/http/client/reactive/HttpComponentsClientHttpRequest.java b/spring-web/src/main/java/org/springframework/http/client/reactive/HttpComponentsClientHttpRequest.java index 92e20d32c35..31659fa7bbd 100644 --- a/spring-web/src/main/java/org/springframework/http/client/reactive/HttpComponentsClientHttpRequest.java +++ b/spring-web/src/main/java/org/springframework/http/client/reactive/HttpComponentsClientHttpRequest.java @@ -66,8 +66,8 @@ class HttpComponentsClientHttpRequest extends AbstractClientHttpRequest { public HttpComponentsClientHttpRequest(HttpMethod method, URI uri, HttpClientContext context, - DataBufferFactory dataBufferFactory) { - + DataBufferFactory dataBufferFactory, boolean applyAttributes) { + super(applyAttributes); this.context = context; this.httpRequest = new BasicHttpRequest(method.name(), uri); this.dataBufferFactory = dataBufferFactory; @@ -157,6 +157,18 @@ class HttpComponentsClientHttpRequest extends AbstractClientHttpRequest { }); } + /** + * Applies the attributes to the {@link HttpClientContext}. + */ + @Override + protected void applyAttributes() { + getAttributes().forEach((key, value) -> { + if(this.context.getAttribute(key) == null) { + this.context.setAttribute(key, value); + } + }); + } + @Override protected HttpHeaders initReadOnlyHeaders() { return HttpHeaders.readOnlyHttpHeaders(new HttpComponentsHeadersAdapter(this.httpRequest)); diff --git a/spring-web/src/main/java/org/springframework/http/client/reactive/JdkClientHttpConnector.java b/spring-web/src/main/java/org/springframework/http/client/reactive/JdkClientHttpConnector.java index 4313c658076..33d3b21cfc1 100644 --- a/spring-web/src/main/java/org/springframework/http/client/reactive/JdkClientHttpConnector.java +++ b/spring-web/src/main/java/org/springframework/http/client/reactive/JdkClientHttpConnector.java @@ -96,7 +96,7 @@ public class JdkClientHttpConnector implements ClientHttpConnector { public Mono connect( HttpMethod method, URI uri, Function> requestCallback) { - JdkClientHttpRequest jdkClientHttpRequest = new JdkClientHttpRequest(method, uri, this.bufferFactory); + JdkClientHttpRequest jdkClientHttpRequest = new JdkClientHttpRequest(method, uri, this.bufferFactory, getApplyAttributes()); return requestCallback.apply(jdkClientHttpRequest).then(Mono.defer(() -> { HttpRequest httpRequest = jdkClientHttpRequest.getNativeRequest(); @@ -109,4 +109,19 @@ public class JdkClientHttpConnector implements ClientHttpConnector { })); } + /** + * Sets nothing, since {@link JdkClientHttpConnector} does not offer any possibility to add attributes. + */ + @Override + public void setApplyAttributes(boolean applyAttributes) { + } + + /** + * Returns false, since {@link JdkClientHttpConnector} does not offer any possibility to add attributes. + */ + @Override + public boolean getApplyAttributes() { + return false; + } + } diff --git a/spring-web/src/main/java/org/springframework/http/client/reactive/JdkClientHttpRequest.java b/spring-web/src/main/java/org/springframework/http/client/reactive/JdkClientHttpRequest.java index faf65dc9ed7..28a6ebdc34a 100644 --- a/spring-web/src/main/java/org/springframework/http/client/reactive/JdkClientHttpRequest.java +++ b/spring-web/src/main/java/org/springframework/http/client/reactive/JdkClientHttpRequest.java @@ -56,7 +56,8 @@ class JdkClientHttpRequest extends AbstractClientHttpRequest { private final HttpRequest.Builder builder; - public JdkClientHttpRequest(HttpMethod httpMethod, URI uri, DataBufferFactory bufferFactory) { + public JdkClientHttpRequest(HttpMethod httpMethod, URI uri, DataBufferFactory bufferFactory, boolean applyAttributes) { + super(applyAttributes); Assert.notNull(httpMethod, "HttpMethod is required"); Assert.notNull(uri, "URI is required"); Assert.notNull(bufferFactory, "DataBufferFactory is required"); @@ -112,6 +113,15 @@ class JdkClientHttpRequest extends AbstractClientHttpRequest { .flatMap(List::stream).map(HttpCookie::toString).collect(Collectors.joining(";"))); } + /** + * Not implemented, since {@link HttpRequest} does not offer any possibility to add request attributes. + */ + @Override + protected void applyAttributes() { + // TODO + throw new RuntimeException(String.format("Using attributes is not available for %s", HttpRequest.class.getName())); + } + @Override public Mono writeWith(Publisher body) { return doCommit(() -> { diff --git a/spring-web/src/main/java/org/springframework/http/client/reactive/JettyClientHttpConnector.java b/spring-web/src/main/java/org/springframework/http/client/reactive/JettyClientHttpConnector.java index a2895f6ded5..4bc77cd1201 100644 --- a/spring-web/src/main/java/org/springframework/http/client/reactive/JettyClientHttpConnector.java +++ b/spring-web/src/main/java/org/springframework/http/client/reactive/JettyClientHttpConnector.java @@ -52,6 +52,8 @@ public class JettyClientHttpConnector implements ClientHttpConnector { private DataBufferFactory bufferFactory = DefaultDataBufferFactory.sharedInstance; + private boolean applyAttributes = true; + /** * Default constructor that creates a new instance of {@link HttpClient}. @@ -126,11 +128,21 @@ public class JettyClientHttpConnector implements ClientHttpConnector { } Request jettyRequest = this.httpClient.newRequest(uri).method(method.toString()); - JettyClientHttpRequest request = new JettyClientHttpRequest(jettyRequest, this.bufferFactory); + JettyClientHttpRequest request = new JettyClientHttpRequest(jettyRequest, this.bufferFactory, getApplyAttributes()); return requestCallback.apply(request).then(execute(request)); } + @Override + public void setApplyAttributes(boolean applyAttributes) { + this.applyAttributes = applyAttributes; + } + + @Override + public boolean getApplyAttributes() { + return this.applyAttributes; + } + private Mono execute(JettyClientHttpRequest request) { return Mono.fromDirect(request.toReactiveRequest() .response((reactiveResponse, chunkPublisher) -> { diff --git a/spring-web/src/main/java/org/springframework/http/client/reactive/JettyClientHttpRequest.java b/spring-web/src/main/java/org/springframework/http/client/reactive/JettyClientHttpRequest.java index d5e934c2bd8..1538f27ef9b 100644 --- a/spring-web/src/main/java/org/springframework/http/client/reactive/JettyClientHttpRequest.java +++ b/spring-web/src/main/java/org/springframework/http/client/reactive/JettyClientHttpRequest.java @@ -55,7 +55,8 @@ class JettyClientHttpRequest extends AbstractClientHttpRequest { private final ReactiveRequest.Builder builder; - public JettyClientHttpRequest(Request jettyRequest, DataBufferFactory bufferFactory) { + public JettyClientHttpRequest(Request jettyRequest, DataBufferFactory bufferFactory, boolean applyAttributes) { + super(applyAttributes); this.jettyRequest = jettyRequest; this.bufferFactory = bufferFactory; this.builder = ReactiveRequest.newBuilder(this.jettyRequest).abortOnCancel(true); @@ -138,6 +139,14 @@ class JettyClientHttpRequest extends AbstractClientHttpRequest { .forEach(this.jettyRequest::cookie); } + /** + * Applies the attributes to {@link Request#getAttributes()}. + */ + @Override + protected void applyAttributes() { + getAttributes().forEach(this.jettyRequest::attribute); + } + @Override protected void applyHeaders() { HttpHeaders headers = getHeaders(); diff --git a/spring-web/src/main/java/org/springframework/http/client/reactive/ReactorClientHttpConnector.java b/spring-web/src/main/java/org/springframework/http/client/reactive/ReactorClientHttpConnector.java index 5f1def633ab..7e0e6a995e4 100644 --- a/spring-web/src/main/java/org/springframework/http/client/reactive/ReactorClientHttpConnector.java +++ b/spring-web/src/main/java/org/springframework/http/client/reactive/ReactorClientHttpConnector.java @@ -66,6 +66,7 @@ public class ReactorClientHttpConnector implements ClientHttpConnector, SmartLif private final Object lifecycleMonitor = new Object(); + private boolean applyAttributes = true; /** * Default constructor. Initializes {@link HttpClient} via: @@ -170,10 +171,20 @@ public class ReactorClientHttpConnector implements ClientHttpConnector, SmartLif return requestSender.uri(uri.toString()); } + @Override + public void setApplyAttributes(boolean applyAttributes) { + this.applyAttributes = applyAttributes; + } + + @Override + public boolean getApplyAttributes() { + return this.applyAttributes; + } + private ReactorClientHttpRequest adaptRequest(HttpMethod method, URI uri, HttpClientRequest request, NettyOutbound nettyOutbound) { - return new ReactorClientHttpRequest(method, uri, request, nettyOutbound); + return new ReactorClientHttpRequest(method, uri, request, nettyOutbound, this.applyAttributes); } @Override diff --git a/spring-web/src/main/java/org/springframework/http/client/reactive/ReactorClientHttpRequest.java b/spring-web/src/main/java/org/springframework/http/client/reactive/ReactorClientHttpRequest.java index 6895ad30a82..bb3bd255cda 100644 --- a/spring-web/src/main/java/org/springframework/http/client/reactive/ReactorClientHttpRequest.java +++ b/spring-web/src/main/java/org/springframework/http/client/reactive/ReactorClientHttpRequest.java @@ -18,13 +18,16 @@ package org.springframework.http.client.reactive; import java.net.URI; import java.nio.file.Path; +import java.util.Map; import io.netty.buffer.ByteBuf; import io.netty.handler.codec.http.cookie.DefaultCookie; +import io.netty.util.AttributeKey; import org.reactivestreams.Publisher; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import reactor.netty.NettyOutbound; +import reactor.netty.channel.ChannelOperations; import reactor.netty.http.client.HttpClientRequest; import org.springframework.core.io.buffer.DataBuffer; @@ -45,6 +48,8 @@ import org.springframework.http.support.Netty4HeadersAdapter; */ class ReactorClientHttpRequest extends AbstractClientHttpRequest implements ZeroCopyHttpOutputMessage { + public static final String ATTRIBUTES_CHANNEL_KEY = "attributes"; + private final HttpMethod httpMethod; private final URI uri; @@ -56,7 +61,8 @@ class ReactorClientHttpRequest extends AbstractClientHttpRequest implements Zero private final NettyDataBufferFactory bufferFactory; - public ReactorClientHttpRequest(HttpMethod method, URI uri, HttpClientRequest request, NettyOutbound outbound) { + public ReactorClientHttpRequest(HttpMethod method, URI uri, HttpClientRequest request, NettyOutbound outbound, boolean applyAttributes) { + super(applyAttributes); this.httpMethod = method; this.uri = uri; this.request = request; @@ -135,6 +141,17 @@ class ReactorClientHttpRequest extends AbstractClientHttpRequest implements Zero })); } + /** + * Applies the request attributes to the {@link reactor.netty.http.client.HttpClientRequest} by setting + * a single {@link Map} into the {@link reactor.netty.channel.ChannelOperations#channel()}, + * with {@link io.netty5.util.AttributeKey#name()} equal to {@link #ATTRIBUTES_CHANNEL_KEY}. + */ + @Override + protected void applyAttributes() { + ((ChannelOperations) this.request) + .channel().attr(AttributeKey.valueOf(ATTRIBUTES_CHANNEL_KEY)).set(getAttributes()); + } + @Override protected HttpHeaders initReadOnlyHeaders() { return HttpHeaders.readOnlyHttpHeaders(new Netty4HeadersAdapter(this.request.requestHeaders())); diff --git a/spring-web/src/main/java/org/springframework/http/client/reactive/ReactorNetty2ClientHttpConnector.java b/spring-web/src/main/java/org/springframework/http/client/reactive/ReactorNetty2ClientHttpConnector.java index 4bed71fbec8..3535397cb53 100644 --- a/spring-web/src/main/java/org/springframework/http/client/reactive/ReactorNetty2ClientHttpConnector.java +++ b/spring-web/src/main/java/org/springframework/http/client/reactive/ReactorNetty2ClientHttpConnector.java @@ -46,6 +46,8 @@ public class ReactorNetty2ClientHttpConnector implements ClientHttpConnector { private final HttpClient httpClient; + private boolean applyAttributes = true; + /** * Default constructor. Initializes {@link HttpClient} via: @@ -126,10 +128,20 @@ public class ReactorNetty2ClientHttpConnector implements ClientHttpConnector { }); } + @Override + public void setApplyAttributes(boolean applyAttributes) { + this.applyAttributes = applyAttributes; + } + + @Override + public boolean getApplyAttributes() { + return this.applyAttributes; + } + private ReactorNetty2ClientHttpRequest adaptRequest(HttpMethod method, URI uri, HttpClientRequest request, NettyOutbound nettyOutbound) { - return new ReactorNetty2ClientHttpRequest(method, uri, request, nettyOutbound); + return new ReactorNetty2ClientHttpRequest(method, uri, request, nettyOutbound, getApplyAttributes()); } } diff --git a/spring-web/src/main/java/org/springframework/http/client/reactive/ReactorNetty2ClientHttpRequest.java b/spring-web/src/main/java/org/springframework/http/client/reactive/ReactorNetty2ClientHttpRequest.java index 749326fa5f1..8ecb1d5a11c 100644 --- a/spring-web/src/main/java/org/springframework/http/client/reactive/ReactorNetty2ClientHttpRequest.java +++ b/spring-web/src/main/java/org/springframework/http/client/reactive/ReactorNetty2ClientHttpRequest.java @@ -18,13 +18,16 @@ package org.springframework.http.client.reactive; import java.net.URI; import java.nio.file.Path; +import java.util.Map; import io.netty5.buffer.Buffer; import io.netty5.handler.codec.http.headers.DefaultHttpCookiePair; +import io.netty5.util.AttributeKey; import org.reactivestreams.Publisher; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import reactor.netty5.NettyOutbound; +import reactor.netty5.channel.ChannelOperations; import reactor.netty5.http.client.HttpClientRequest; import org.springframework.core.io.buffer.DataBuffer; @@ -46,6 +49,8 @@ import org.springframework.http.support.Netty5HeadersAdapter; */ class ReactorNetty2ClientHttpRequest extends AbstractClientHttpRequest implements ZeroCopyHttpOutputMessage { + public static final String ATTRIBUTES_CHANNEL_KEY = "attributes"; + private final HttpMethod httpMethod; private final URI uri; @@ -57,7 +62,8 @@ class ReactorNetty2ClientHttpRequest extends AbstractClientHttpRequest implement private final Netty5DataBufferFactory bufferFactory; - public ReactorNetty2ClientHttpRequest(HttpMethod method, URI uri, HttpClientRequest request, NettyOutbound outbound) { + public ReactorNetty2ClientHttpRequest(HttpMethod method, URI uri, HttpClientRequest request, NettyOutbound outbound, boolean applyAttributes) { + super(applyAttributes); this.httpMethod = method; this.uri = uri; this.request = request; @@ -136,6 +142,17 @@ class ReactorNetty2ClientHttpRequest extends AbstractClientHttpRequest implement })); } + /** + * Applies the request attributes to the {@link reactor.netty.http.client.HttpClientRequest} by setting + * a single {@link Map} into the {@link reactor.netty.channel.ChannelOperations#channel()}, + * with {@link AttributeKey#name()} equal to {@link #ATTRIBUTES_CHANNEL_KEY}. + */ + @Override + protected void applyAttributes() { + ((ChannelOperations) this.request) + .channel().attr(AttributeKey.valueOf(ATTRIBUTES_CHANNEL_KEY)).set(getAttributes()); + } + @Override protected HttpHeaders initReadOnlyHeaders() { return HttpHeaders.readOnlyHttpHeaders(new Netty5HeadersAdapter(this.request.requestHeaders())); diff --git a/spring-web/src/testFixtures/java/org/springframework/web/testfixture/http/client/reactive/MockClientHttpRequest.java b/spring-web/src/testFixtures/java/org/springframework/web/testfixture/http/client/reactive/MockClientHttpRequest.java index daeefdf13f9..4327808eafc 100644 --- a/spring-web/src/testFixtures/java/org/springframework/web/testfixture/http/client/reactive/MockClientHttpRequest.java +++ b/spring-web/src/testFixtures/java/org/springframework/web/testfixture/http/client/reactive/MockClientHttpRequest.java @@ -121,6 +121,10 @@ public class MockClientHttpRequest extends AbstractClientHttpRequest implements .forEach(cookie -> getHeaders().add(HttpHeaders.COOKIE, cookie.toString())); } + @Override + protected void applyAttributes() { + } + @Override public Mono writeWith(Publisher body) { return doCommit(() -> Mono.defer(() -> this.writeHandler.apply(Flux.from(body)))); diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/DefaultClientRequestBuilder.java b/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/DefaultClientRequestBuilder.java index 1e400fad7d4..9324f972c17 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/DefaultClientRequestBuilder.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/DefaultClientRequestBuilder.java @@ -265,6 +265,12 @@ final class DefaultClientRequestBuilder implements ClientRequest.Builder { requestCookies.add(name, cookie); })); } + + Map requestAttributes = request.getAttributes(); + if (!this.attributes.isEmpty()) { + this.attributes.forEach((key, value) -> requestAttributes.put(key, value)); + } + if (this.httpRequestConsumer != null) { this.httpRequestConsumer.accept(request); } diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/DefaultWebClientBuilder.java b/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/DefaultWebClientBuilder.java index 59da4e80d02..5fe52d73156 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/DefaultWebClientBuilder.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/DefaultWebClientBuilder.java @@ -89,6 +89,8 @@ final class DefaultWebClientBuilder implements WebClient.Builder { @Nullable private MultiValueMap defaultCookies; + private boolean applyAttributes; + @Nullable private Consumer> defaultRequest; @@ -137,6 +139,7 @@ final class DefaultWebClientBuilder implements WebClient.Builder { this.defaultCookies = (other.defaultCookies != null ? new LinkedMultiValueMap<>(other.defaultCookies) : null); + this.applyAttributes = other.applyAttributes; this.defaultRequest = other.defaultRequest; this.statusHandlers = (other.statusHandlers != null ? new LinkedHashMap<>(other.statusHandlers) : null); this.filters = (other.filters != null ? new ArrayList<>(other.filters) : null); @@ -200,6 +203,12 @@ final class DefaultWebClientBuilder implements WebClient.Builder { return this; } + @Override + public WebClient.Builder applyAttributes(boolean applyAttributes) { + this.applyAttributes = applyAttributes; + return this; + } + private MultiValueMap initCookies() { if (this.defaultCookies == null) { this.defaultCookies = new LinkedMultiValueMap<>(3); @@ -335,21 +344,24 @@ final class DefaultWebClientBuilder implements WebClient.Builder { } private ClientHttpConnector initConnector() { + final ClientHttpConnector connector; if (reactorNettyClientPresent) { - return new ReactorClientHttpConnector(); + connector = new ReactorClientHttpConnector(); } else if (reactorNetty2ClientPresent) { return new ReactorNetty2ClientHttpConnector(); } else if (jettyClientPresent) { - return new JettyClientHttpConnector(); + connector = new JettyClientHttpConnector(); } else if (httpComponentsClientPresent) { - return new HttpComponentsClientHttpConnector(); + connector = new HttpComponentsClientHttpConnector(); } else { - return new JdkClientHttpConnector(); + connector = new JdkClientHttpConnector(); } + connector.setApplyAttributes(this.applyAttributes); + return connector; } private ExchangeStrategies initExchangeStrategies() { diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/WebClient.java b/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/WebClient.java index 02586478e33..60a25f70f5f 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/WebClient.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/WebClient.java @@ -250,6 +250,13 @@ public interface WebClient { */ Builder defaultCookies(Consumer> cookiesConsumer); + /** + * Global option to specify whether or not the request attributes should be applied + * to the underlying http-client request, if the used {@link ClientHttpConnector} allows it. + * @param applyAttributes whether or not to apply the attributes + */ + Builder applyAttributes(boolean applyAttributes); + /** * Provide a consumer to customize every request being built. * @param defaultRequest the consumer to use for modifying requests diff --git a/spring-webflux/src/test/java/org/springframework/web/reactive/function/client/WebClientIntegrationTests.java b/spring-webflux/src/test/java/org/springframework/web/reactive/function/client/WebClientIntegrationTests.java index 85f9156ba7b..39e23da07cf 100644 --- a/spring-webflux/src/test/java/org/springframework/web/reactive/function/client/WebClientIntegrationTests.java +++ b/spring-webflux/src/test/java/org/springframework/web/reactive/function/client/WebClientIntegrationTests.java @@ -34,14 +34,18 @@ import java.time.Duration; import java.util.Arrays; import java.util.List; import java.util.Map; +import java.util.concurrent.atomic.AtomicReference; import java.util.function.Consumer; import java.util.function.Function; import java.util.stream.Collectors; import java.util.stream.Stream; +import io.netty.util.Attribute; +import io.netty.util.AttributeKey; import okhttp3.mockwebserver.MockResponse; import okhttp3.mockwebserver.MockWebServer; import okhttp3.mockwebserver.RecordedRequest; +import org.eclipse.jetty.client.Request; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Named; import org.junit.jupiter.api.Test; @@ -50,6 +54,7 @@ import org.junit.jupiter.params.provider.MethodSource; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import reactor.core.publisher.Sinks; +import reactor.netty.channel.ChannelOperations; import reactor.netty.http.client.HttpClient; import reactor.netty.resources.ConnectionProvider; import reactor.test.StepVerifier; @@ -186,6 +191,67 @@ class WebClientIntegrationTests { }); } + @ParameterizedWebClientTest + void applyAttributesInNativeRequest(ClientHttpConnector connector) { + startServer(connector); + connector.setApplyAttributes(true); + checkAttributesInNativeRequest(true); + } + + @ParameterizedWebClientTest + void dontApplyAttributesInNativeRequest(ClientHttpConnector connector) { + startServer(connector); + connector.setApplyAttributes(false); + checkAttributesInNativeRequest(false); + } + + private void checkAttributesInNativeRequest(boolean expectAttributesApplied){ + prepareResponse(response -> {}); + + final AtomicReference nativeRequest = new AtomicReference<>(); + Mono result = this.webClient.get() + .uri("/pojo") + .attribute("foo","bar") + .httpRequest(clientHttpRequest -> nativeRequest.set(clientHttpRequest.getNativeRequest())) + .retrieve() + .bodyToMono(Void.class); + StepVerifier.create(result) + .expectComplete() + .verify(); + if (nativeRequest.get() instanceof ChannelOperations nativeReq) { + Attribute> attributes = nativeReq.channel().attr(AttributeKey.valueOf("attributes")); + if (expectAttributesApplied) { + assertThat(attributes.get()).isNotNull(); + assertThat(attributes.get()).containsEntry("foo", "bar"); + } + else { + assertThat(attributes.get()).isNull(); + } + } + else if (nativeRequest.get() instanceof reactor.netty5.channel.ChannelOperations nativeReq) { + io.netty5.util.Attribute> attributes = nativeReq.channel().attr(io.netty5.util.AttributeKey.valueOf("attributes")); + if (expectAttributesApplied) { + assertThat(attributes.get()).isNotNull(); + assertThat(attributes.get()).containsEntry("foo", "bar"); + } + else { + assertThat(attributes.get()).isNull(); + } + } + else if (nativeRequest.get() instanceof Request nativeReq) { + if (expectAttributesApplied) { + assertThat(nativeReq.getAttributes()).containsEntry("foo", "bar"); + } + else { + assertThat(nativeReq.getAttributes()).doesNotContainEntry("foo", "bar"); + } + } + else if (nativeRequest.get() instanceof org.apache.hc.core5.http.HttpRequest nativeReq) { + // TODO get attributes from HttpClientContext + } + } + + @ParameterizedWebClientTest void retrieveJsonWithParameterizedTypeReference(ClientHttpConnector connector) { startServer(connector);