diff --git a/build.gradle b/build.gradle
index 71e10e092a9b..319b52fcbac0 100644
--- a/build.gradle
+++ b/build.gradle
@@ -153,6 +153,8 @@ configure(allprojects) { project ->
exclude group: "commons-logging", name: "commons-logging"
}
dependency "org.eclipse.jetty:jetty-reactive-httpclient:1.1.2"
+ dependency 'org.apache.httpcomponents.client5:httpclient5:5.0'
+ dependency 'org.apache.httpcomponents.core5:httpcore5-reactive:5.0'
dependency "org.jruby:jruby:9.2.11.0"
dependency "org.python:jython-standalone:2.7.1"
diff --git a/spring-web/spring-web.gradle b/spring-web/spring-web.gradle
index 60c40ed4eeec..c16ccc80b7d5 100644
--- a/spring-web/spring-web.gradle
+++ b/spring-web/spring-web.gradle
@@ -35,6 +35,8 @@ dependencies {
exclude group: "javax.servlet", module: "javax.servlet-api"
}
optional("org.eclipse.jetty:jetty-reactive-httpclient")
+ optional('org.apache.httpcomponents.client5:httpclient5:5.0')
+ optional('org.apache.httpcomponents.core5:httpcore5-reactive:5.0')
optional("com.squareup.okhttp3:okhttp")
optional("org.apache.httpcomponents:httpclient")
optional("org.apache.httpcomponents:httpasyncclient")
diff --git a/spring-web/src/main/java/org/springframework/http/client/HttpComponentsAsyncClientHttpRequest.java b/spring-web/src/main/java/org/springframework/http/client/HttpComponentsAsyncClientHttpRequest.java
index d1166051c759..ca97edde0bb8 100644
--- a/spring-web/src/main/java/org/springframework/http/client/HttpComponentsAsyncClientHttpRequest.java
+++ b/spring-web/src/main/java/org/springframework/http/client/HttpComponentsAsyncClientHttpRequest.java
@@ -48,7 +48,8 @@
* @author Arjen Poutsma
* @since 4.0
* @see HttpComponentsClientHttpRequestFactory#createRequest
- * @deprecated as of Spring 5.0, with no direct replacement
+ * @deprecated as of Spring 5.0, in favor of
+ * {@link org.springframework.http.client.reactive.HttpComponentsClientHttpConnector}
*/
@Deprecated
final class HttpComponentsAsyncClientHttpRequest extends AbstractBufferingAsyncClientHttpRequest {
diff --git a/spring-web/src/main/java/org/springframework/http/client/HttpComponentsAsyncClientHttpRequestFactory.java b/spring-web/src/main/java/org/springframework/http/client/HttpComponentsAsyncClientHttpRequestFactory.java
index 85cf0bab0d3e..84bd07a159e6 100644
--- a/spring-web/src/main/java/org/springframework/http/client/HttpComponentsAsyncClientHttpRequestFactory.java
+++ b/spring-web/src/main/java/org/springframework/http/client/HttpComponentsAsyncClientHttpRequestFactory.java
@@ -44,7 +44,8 @@
* @author Stephane Nicoll
* @since 4.0
* @see HttpAsyncClient
- * @deprecated as of Spring 5.0, with no direct replacement
+ * @deprecated as of Spring 5.0, in favor of
+ * {@link org.springframework.http.client.reactive.HttpComponentsClientHttpConnector}
*/
@Deprecated
public class HttpComponentsAsyncClientHttpRequestFactory extends HttpComponentsClientHttpRequestFactory
diff --git a/spring-web/src/main/java/org/springframework/http/client/HttpComponentsAsyncClientHttpResponse.java b/spring-web/src/main/java/org/springframework/http/client/HttpComponentsAsyncClientHttpResponse.java
index cba3a722c43f..54297db86008 100644
--- a/spring-web/src/main/java/org/springframework/http/client/HttpComponentsAsyncClientHttpResponse.java
+++ b/spring-web/src/main/java/org/springframework/http/client/HttpComponentsAsyncClientHttpResponse.java
@@ -37,7 +37,8 @@
* @author Arjen Poutsma
* @since 4.0
* @see HttpComponentsAsyncClientHttpRequest#executeAsync()
- * @deprecated as of Spring 5.0, with no direct replacement
+ * @deprecated as of Spring 5.0, in favor of
+ * {@link org.springframework.http.client.reactive.HttpComponentsClientHttpConnector}
*/
@Deprecated
final class HttpComponentsAsyncClientHttpResponse extends AbstractClientHttpResponse {
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
new file mode 100644
index 000000000000..c9b8e4064e0c
--- /dev/null
+++ b/spring-web/src/main/java/org/springframework/http/client/reactive/HttpComponentsClientHttpConnector.java
@@ -0,0 +1,144 @@
+/*
+ * Copyright 2002-2020 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.http.client.reactive;
+
+import java.net.URI;
+import java.nio.ByteBuffer;
+import java.util.function.BiFunction;
+import java.util.function.Function;
+
+import org.apache.hc.client5.http.cookie.BasicCookieStore;
+import org.apache.hc.client5.http.impl.async.CloseableHttpAsyncClient;
+import org.apache.hc.client5.http.impl.async.HttpAsyncClients;
+import org.apache.hc.client5.http.protocol.HttpClientContext;
+import org.apache.hc.core5.concurrent.FutureCallback;
+import org.apache.hc.core5.http.HttpResponse;
+import org.apache.hc.core5.http.Message;
+import org.apache.hc.core5.http.nio.AsyncRequestProducer;
+import org.apache.hc.core5.reactive.ReactiveResponseConsumer;
+import org.reactivestreams.Publisher;
+import reactor.core.publisher.Mono;
+import reactor.core.publisher.MonoSink;
+
+import org.springframework.core.io.buffer.DataBufferFactory;
+import org.springframework.core.io.buffer.DefaultDataBufferFactory;
+import org.springframework.http.HttpMethod;
+
+/**
+ * {@link ClientHttpConnector} implementation for the Apache HttpComponents HttpClient 5.x.
+ *
+ * @author Martin Tarjányi
+ * @since 5.3
+ * @see Apache HttpComponents
+ */
+public class HttpComponentsClientHttpConnector implements ClientHttpConnector {
+
+ private final CloseableHttpAsyncClient client;
+
+ private final BiFunction contextProvider;
+
+ private DataBufferFactory dataBufferFactory = new DefaultDataBufferFactory();
+
+
+ /**
+ * Default constructor that creates and starts a new instance of {@link CloseableHttpAsyncClient}.
+ */
+ public HttpComponentsClientHttpConnector() {
+ this(HttpAsyncClients.createDefault());
+ }
+
+ /**
+ * Constructor with a pre-configured {@link CloseableHttpAsyncClient} instance.
+ * @param client the client to use
+ */
+ public HttpComponentsClientHttpConnector(CloseableHttpAsyncClient client) {
+ this(client, (method, uri) -> HttpClientContext.create());
+ }
+
+ /**
+ * Constructor with a pre-configured {@link CloseableHttpAsyncClient} instance
+ * and a {@link HttpClientContext} supplier lambda which is called before each request
+ * and passed to the client.
+ * @param client the client to use
+ * @param contextProvider a {@link HttpClientContext} supplier
+ */
+ public HttpComponentsClientHttpConnector(CloseableHttpAsyncClient client,
+ BiFunction contextProvider) {
+
+ this.contextProvider = contextProvider;
+ this.client = client;
+ this.client.start();
+ }
+
+
+ public void setBufferFactory(DataBufferFactory bufferFactory) {
+ this.dataBufferFactory = bufferFactory;
+ }
+
+ @Override
+ public Mono connect(HttpMethod method, URI uri,
+ Function super ClientHttpRequest, Mono> requestCallback) {
+
+ HttpClientContext context = this.contextProvider.apply(method, uri);
+
+ if (context.getCookieStore() == null) {
+ context.setCookieStore(new BasicCookieStore());
+ }
+
+ HttpComponentsClientHttpRequest request = new HttpComponentsClientHttpRequest(method, uri,
+ context, this.dataBufferFactory);
+
+ return requestCallback.apply(request).then(Mono.defer(() -> execute(request, context)));
+ }
+
+ private Mono execute(HttpComponentsClientHttpRequest request, HttpClientContext context) {
+ AsyncRequestProducer requestProducer = request.toRequestProducer();
+
+ return Mono.>>create(sink -> {
+ ReactiveResponseConsumer reactiveResponseConsumer =
+ new ReactiveResponseConsumer(new MonoFutureCallbackAdapter(sink));
+
+ this.client.execute(requestProducer, reactiveResponseConsumer, context, null);
+ }).map(message -> new HttpComponentsClientHttpResponse(this.dataBufferFactory, message, context));
+ }
+
+
+ private static class MonoFutureCallbackAdapter
+ implements FutureCallback>> {
+
+ private final MonoSink>> sink;
+
+ public MonoFutureCallbackAdapter(MonoSink>> sink) {
+ this.sink = sink;
+ }
+
+ @Override
+ public void completed(Message> result) {
+ this.sink.success(result);
+ }
+
+ @Override
+ public void failed(Exception ex) {
+ this.sink.error(ex);
+ }
+
+ @Override
+ public void cancelled() {
+ }
+ }
+
+}
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
new file mode 100644
index 000000000000..5486a5362156
--- /dev/null
+++ b/spring-web/src/main/java/org/springframework/http/client/reactive/HttpComponentsClientHttpRequest.java
@@ -0,0 +1,167 @@
+/*
+ * Copyright 2002-2020 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.http.client.reactive;
+
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.nio.ByteBuffer;
+import java.util.Collection;
+
+import org.apache.hc.client5.http.cookie.CookieStore;
+import org.apache.hc.client5.http.impl.cookie.BasicClientCookie;
+import org.apache.hc.client5.http.protocol.HttpClientContext;
+import org.apache.hc.core5.http.ContentType;
+import org.apache.hc.core5.http.HttpRequest;
+import org.apache.hc.core5.http.message.BasicHttpRequest;
+import org.apache.hc.core5.http.nio.AsyncRequestProducer;
+import org.apache.hc.core5.http.nio.support.BasicRequestProducer;
+import org.apache.hc.core5.reactive.ReactiveEntityProducer;
+import org.reactivestreams.Publisher;
+import reactor.core.publisher.Flux;
+import reactor.core.publisher.Mono;
+
+import org.springframework.core.io.buffer.DataBuffer;
+import org.springframework.core.io.buffer.DataBufferFactory;
+import org.springframework.http.HttpHeaders;
+import org.springframework.http.HttpMethod;
+import org.springframework.lang.Nullable;
+
+import static org.springframework.http.MediaType.ALL_VALUE;
+
+/**
+ * {@link ClientHttpRequest} implementation for the Apache HttpComponents HttpClient 5.x.
+ *
+ * @author Martin Tarjányi
+ * @since 5.3
+ * @see Apache HttpComponents
+ */
+class HttpComponentsClientHttpRequest extends AbstractClientHttpRequest {
+
+ private final HttpRequest httpRequest;
+
+ private final DataBufferFactory dataBufferFactory;
+
+ private final HttpClientContext context;
+
+ @Nullable
+ private Flux byteBufferFlux;
+
+
+ public HttpComponentsClientHttpRequest(HttpMethod method, URI uri, HttpClientContext context,
+ DataBufferFactory dataBufferFactory) {
+
+ this.context = context;
+ this.httpRequest = new BasicHttpRequest(method.name(), uri);
+ this.dataBufferFactory = dataBufferFactory;
+ }
+
+
+ @Override
+ public HttpMethod getMethod() {
+ return HttpMethod.resolve(this.httpRequest.getMethod());
+ }
+
+ @Override
+ public URI getURI() {
+ try {
+ return this.httpRequest.getUri();
+ }
+ catch (URISyntaxException ex) {
+ throw new IllegalArgumentException("Invalid URI syntax.", ex);
+ }
+ }
+
+ @Override
+ public DataBufferFactory bufferFactory() {
+ return this.dataBufferFactory;
+ }
+
+ @Override
+ public Mono writeWith(Publisher extends DataBuffer> body) {
+ return doCommit(() -> {
+ this.byteBufferFlux = Flux.from(body).map(DataBuffer::asByteBuffer);
+ return Mono.empty();
+ });
+ }
+
+ @Override
+ public Mono writeAndFlushWith(Publisher extends Publisher extends DataBuffer>> body) {
+ return writeWith(Flux.from(body).flatMap(p -> p));
+ }
+
+ @Override
+ public Mono setComplete() {
+ return doCommit();
+ }
+
+ @Override
+ protected void applyHeaders() {
+ HttpHeaders headers = getHeaders();
+
+ headers.entrySet()
+ .stream()
+ .filter(entry -> !HttpHeaders.CONTENT_LENGTH.equals(entry.getKey()))
+ .forEach(entry -> entry.getValue().forEach(v -> this.httpRequest.addHeader(entry.getKey(), v)));
+
+ if (!this.httpRequest.containsHeader(HttpHeaders.ACCEPT)) {
+ this.httpRequest.addHeader(HttpHeaders.ACCEPT, ALL_VALUE);
+ }
+ }
+
+ @Override
+ protected void applyCookies() {
+ if (getCookies().isEmpty()) {
+ return;
+ }
+
+ CookieStore cookieStore = this.context.getCookieStore();
+
+ getCookies().values()
+ .stream()
+ .flatMap(Collection::stream)
+ .forEach(cookie -> {
+ BasicClientCookie clientCookie = new BasicClientCookie(cookie.getName(), cookie.getValue());
+ clientCookie.setDomain(getURI().getHost());
+ clientCookie.setPath(getURI().getPath());
+ cookieStore.addCookie(clientCookie);
+ });
+ }
+
+ public AsyncRequestProducer toRequestProducer() {
+ ReactiveEntityProducer reactiveEntityProducer = createReactiveEntityProducer();
+
+ return new BasicRequestProducer(this.httpRequest, reactiveEntityProducer);
+ }
+
+ @Nullable
+ private ReactiveEntityProducer createReactiveEntityProducer() {
+ if (this.byteBufferFlux == null) {
+ return null;
+ }
+
+ String contentEncoding = getHeaders().getFirst(HttpHeaders.CONTENT_ENCODING);
+
+ ContentType contentType = null;
+
+ if (getHeaders().getContentType() != null) {
+ contentType = ContentType.parse(getHeaders().getContentType().toString());
+ }
+
+ return new ReactiveEntityProducer(this.byteBufferFlux, getHeaders().getContentLength(),
+ contentType, contentEncoding);
+ }
+}
diff --git a/spring-web/src/main/java/org/springframework/http/client/reactive/HttpComponentsClientHttpResponse.java b/spring-web/src/main/java/org/springframework/http/client/reactive/HttpComponentsClientHttpResponse.java
new file mode 100644
index 000000000000..90669b93d380
--- /dev/null
+++ b/spring-web/src/main/java/org/springframework/http/client/reactive/HttpComponentsClientHttpResponse.java
@@ -0,0 +1,116 @@
+/*
+ * Copyright 2002-2020 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.http.client.reactive;
+
+import java.nio.ByteBuffer;
+import java.util.Arrays;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+import org.apache.hc.client5.http.cookie.Cookie;
+import org.apache.hc.client5.http.protocol.HttpClientContext;
+import org.apache.hc.core5.http.HttpResponse;
+import org.apache.hc.core5.http.Message;
+import org.reactivestreams.Publisher;
+import reactor.core.publisher.Flux;
+
+import org.springframework.core.io.buffer.DataBuffer;
+import org.springframework.core.io.buffer.DataBufferFactory;
+import org.springframework.http.HttpHeaders;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.ResponseCookie;
+import org.springframework.util.LinkedMultiValueMap;
+import org.springframework.util.MultiValueMap;
+
+import static org.apache.hc.client5.http.cookie.Cookie.MAX_AGE_ATTR;
+
+/**
+ * {@link ClientHttpResponse} implementation for the Apache HttpComponents HttpClient 5.x.
+ *
+ * @author Martin Tarjányi
+ * @since 5.3
+ * @see Apache HttpComponents
+ */
+class HttpComponentsClientHttpResponse implements ClientHttpResponse {
+
+ private final DataBufferFactory dataBufferFactory;
+
+ private final Message> message;
+
+ private final HttpClientContext context;
+
+ private final AtomicBoolean rejectSubscribers = new AtomicBoolean();
+
+
+ public HttpComponentsClientHttpResponse(DataBufferFactory dataBufferFactory,
+ Message> message,
+ HttpClientContext context) {
+
+ this.dataBufferFactory = dataBufferFactory;
+ this.message = message;
+ this.context = context;
+ }
+
+
+ @Override
+ public HttpStatus getStatusCode() {
+ return HttpStatus.valueOf(this.message.getHead().getCode());
+ }
+
+ @Override
+ public int getRawStatusCode() {
+ return this.message.getHead().getCode();
+ }
+
+ @Override
+ public MultiValueMap getCookies() {
+ LinkedMultiValueMap result = new LinkedMultiValueMap<>();
+ this.context.getCookieStore().getCookies().forEach(cookie ->
+ result.add(cookie.getName(), ResponseCookie.fromClientResponse(cookie.getName(), cookie.getValue())
+ .domain(cookie.getDomain())
+ .path(cookie.getPath())
+ .maxAge(getMaxAgeSeconds(cookie))
+ .secure(cookie.isSecure())
+ .httpOnly(cookie.containsAttribute("httponly"))
+ .build()));
+ return result;
+ }
+
+ private long getMaxAgeSeconds(Cookie cookie) {
+ String maxAgeAttribute = cookie.getAttribute(MAX_AGE_ATTR);
+
+ return maxAgeAttribute == null ? -1 : Long.parseLong(maxAgeAttribute);
+ }
+
+ @Override
+ public Flux getBody() {
+ return Flux.from(this.message.getBody())
+ .doOnSubscribe(s -> {
+ if (!this.rejectSubscribers.compareAndSet(false, true)) {
+ throw new IllegalStateException("The client response body can only be consumed once.");
+ }
+ })
+ .map(this.dataBufferFactory::wrap);
+ }
+
+ @Override
+ public HttpHeaders getHeaders() {
+ return Arrays.stream(this.message.getHead().getHeaders())
+ .collect(HttpHeaders::new,
+ (httpHeaders, header) -> httpHeaders.add(header.getName(), header.getValue()),
+ HttpHeaders::putAll);
+ }
+}
diff --git a/spring-webflux/spring-webflux.gradle b/spring-webflux/spring-webflux.gradle
index 1028d6e59c42..16f8fa93740e 100644
--- a/spring-webflux/spring-webflux.gradle
+++ b/spring-webflux/spring-webflux.gradle
@@ -46,6 +46,8 @@ dependencies {
testCompile("org.eclipse.jetty:jetty-server")
testCompile("org.eclipse.jetty:jetty-servlet")
testCompile("org.eclipse.jetty:jetty-reactive-httpclient")
+ testCompile('org.apache.httpcomponents.client5:httpclient5:5.0')
+ testCompile('org.apache.httpcomponents.core5:httpcore5-reactive:5.0')
testCompile("com.squareup.okhttp3:mockwebserver")
testCompile("org.jetbrains.kotlin:kotlin-script-runtime")
testRuntime("org.jetbrains.kotlin:kotlin-scripting-jsr223-embeddable")
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 dfd1087598b2..fed9093abd92 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
@@ -25,6 +25,7 @@
import org.springframework.http.HttpHeaders;
import org.springframework.http.client.reactive.ClientHttpConnector;
+import org.springframework.http.client.reactive.HttpComponentsClientHttpConnector;
import org.springframework.http.client.reactive.JettyClientHttpConnector;
import org.springframework.http.client.reactive.ReactorClientHttpConnector;
import org.springframework.http.codec.ClientCodecConfigurer;
@@ -50,10 +51,14 @@ final class DefaultWebClientBuilder implements WebClient.Builder {
private static final boolean jettyClientPresent;
+ private static final boolean httpComponentsClientPresent;
+
static {
ClassLoader loader = DefaultWebClientBuilder.class.getClassLoader();
reactorClientPresent = ClassUtils.isPresent("reactor.netty.http.client.HttpClient", loader);
jettyClientPresent = ClassUtils.isPresent("org.eclipse.jetty.client.HttpClient", loader);
+ httpComponentsClientPresent = ClassUtils.isPresent("org.apache.hc.client5.http.impl.async.CloseableHttpAsyncClient", loader)
+ && ClassUtils.isPresent("org.apache.hc.core5.reactive.ReactiveDataConsumer", loader);
}
@@ -275,6 +280,9 @@ else if (reactorClientPresent) {
else if (jettyClientPresent) {
return new JettyClientHttpConnector();
}
+ else if (httpComponentsClientPresent) {
+ return new HttpComponentsClientHttpConnector();
+ }
throw new IllegalStateException("No suitable default ClientHttpConnector found");
}
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 f0f6939eaa6c..15b2047ca2bd 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
@@ -58,8 +58,10 @@
import org.springframework.http.HttpRequest;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
+import org.springframework.http.ResponseCookie;
import org.springframework.http.ResponseEntity;
import org.springframework.http.client.reactive.ClientHttpConnector;
+import org.springframework.http.client.reactive.HttpComponentsClientHttpConnector;
import org.springframework.http.client.reactive.JettyClientHttpConnector;
import org.springframework.http.client.reactive.ReactorClientHttpConnector;
import org.springframework.web.testfixture.xml.Pojo;
@@ -74,6 +76,7 @@
* @author Denys Ivano
* @author Sebastien Deleuze
* @author Sam Brannen
+ * @author Martin Tarjányi
*/
class WebClientIntegrationTests {
@@ -85,10 +88,13 @@ class WebClientIntegrationTests {
}
static Stream arguments() {
- return Stream.of(new JettyClientHttpConnector(), new ReactorClientHttpConnector());
+ return Stream.of(
+ new ReactorClientHttpConnector(),
+ new JettyClientHttpConnector(),
+ new HttpComponentsClientHttpConnector()
+ );
}
-
private MockWebServer server;
private WebClient webClient;
@@ -564,7 +570,7 @@ void retrieve555UnknownStatus(ClientHttpConnector connector) {
.expectErrorSatisfies(throwable -> {
assertThat(throwable instanceof UnknownHttpStatusCodeException).isTrue();
UnknownHttpStatusCodeException ex = (UnknownHttpStatusCodeException) throwable;
- assertThat(ex.getMessage()).isEqualTo(("Unknown status code ["+errorStatus+"]"));
+ assertThat(ex.getMessage()).isEqualTo(("Unknown status code [" + errorStatus + "]"));
assertThat(ex.getRawStatusCode()).isEqualTo(errorStatus);
assertThat(ex.getStatusText()).isEqualTo("");
assertThat(ex.getHeaders().getContentType()).isEqualTo(MediaType.TEXT_PLAIN);
@@ -1079,6 +1085,42 @@ void filterForErrorHandling(ClientHttpConnector connector) {
expectRequestCount(2);
}
+ @ParameterizedWebClientTest
+ void exchangeResponseCookies(ClientHttpConnector connector) {
+ startServer(connector);
+
+ prepareResponse(response -> response
+ .setHeader("Content-Type", "text/plain")
+ .addHeader("Set-Cookie", "testkey1=testvalue1;")
+ .addHeader("Set-Cookie", "testkey2=testvalue2; Max-Age=42; HttpOnly; Secure")
+ .setBody("test"));
+
+ Mono result = this.webClient.get()
+ .uri("/test")
+ .exchange();
+
+ StepVerifier.create(result)
+ .consumeNextWith(response -> {
+ assertThat(response.cookies()).containsOnlyKeys("testkey1", "testkey2");
+
+ ResponseCookie cookie1 = response.cookies().get("testkey1").get(0);
+ assertThat(cookie1.getValue()).isEqualTo("testvalue1");
+ assertThat(cookie1.isSecure()).isFalse();
+ assertThat(cookie1.isHttpOnly()).isFalse();
+ assertThat(cookie1.getMaxAge().getSeconds()).isEqualTo(-1);
+
+ ResponseCookie cookie2 = response.cookies().get("testkey2").get(0);
+ assertThat(cookie2.getValue()).isEqualTo("testvalue2");
+ assertThat(cookie2.isSecure()).isTrue();
+ assertThat(cookie2.isHttpOnly()).isTrue();
+ assertThat(cookie2.getMaxAge().getSeconds()).isEqualTo(42);
+ })
+ .expectComplete()
+ .verify(Duration.ofSeconds(3));
+
+ expectRequestCount(1);
+ }
+
private void prepareResponse(Consumer consumer) {
MockResponse response = new MockResponse();
diff --git a/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/SseIntegrationTests.java b/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/SseIntegrationTests.java
index b83e8f3b46cd..6344b86c874c 100644
--- a/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/SseIntegrationTests.java
+++ b/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/SseIntegrationTests.java
@@ -34,6 +34,7 @@
import org.springframework.context.annotation.Configuration;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.http.client.reactive.ClientHttpConnector;
+import org.springframework.http.client.reactive.HttpComponentsClientHttpConnector;
import org.springframework.http.client.reactive.JettyClientHttpConnector;
import org.springframework.http.client.reactive.ReactorClientHttpConnector;
import org.springframework.http.codec.ServerSentEvent;
@@ -73,12 +74,16 @@ static Object[][] arguments() {
return new Object[][] {
{new JettyHttpServer(), new ReactorClientHttpConnector()},
{new JettyHttpServer(), new JettyClientHttpConnector()},
+ {new JettyHttpServer(), new HttpComponentsClientHttpConnector()},
{new ReactorHttpServer(), new ReactorClientHttpConnector()},
{new ReactorHttpServer(), new JettyClientHttpConnector()},
+ {new ReactorHttpServer(), new HttpComponentsClientHttpConnector()},
{new TomcatHttpServer(), new ReactorClientHttpConnector()},
{new TomcatHttpServer(), new JettyClientHttpConnector()},
+ {new TomcatHttpServer(), new HttpComponentsClientHttpConnector()},
{new UndertowHttpServer(), new ReactorClientHttpConnector()},
- {new UndertowHttpServer(), new JettyClientHttpConnector()}
+ {new UndertowHttpServer(), new JettyClientHttpConnector()},
+ {new UndertowHttpServer(), new HttpComponentsClientHttpConnector()}
};
}
diff --git a/src/docs/asciidoc/web/webflux-webclient.adoc b/src/docs/asciidoc/web/webflux-webclient.adoc
index 922a50976e11..1e64b1740381 100644
--- a/src/docs/asciidoc/web/webflux-webclient.adoc
+++ b/src/docs/asciidoc/web/webflux-webclient.adoc
@@ -369,6 +369,33 @@ shows:
<2> Plug the connector into the `WebClient.Builder`.
+
+[[webflux-client-builder-http-components]]
+=== HttpComponents
+
+The following example shows how to customize Apache HttpComponents `HttpClient` settings:
+
+[source,java,indent=0,subs="verbatim,quotes",role="primary"]
+.Java
+----
+ HttpAsyncClientBuilder clientBuilder = HttpAsyncClients.custom();
+ clientBuilder.setDefaultRequestConfig(...);
+ CloseableHttpAsyncClient client = clientBuilder.build();
+ ClientHttpConnector connector = new HttpComponentsClientHttpConnector(client);
+
+ WebClient webClient = WebClient.builder().clientConnector(connector).build();
+----
+[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
+.Kotlin
+----
+ val client = HttpAsyncClients.custom().apply {
+ setDefaultRequestConfig(...)
+ }.build()
+ val connector = HttpComponentsClientHttpConnector(client)
+ val webClient = WebClient.builder().clientConnector(connector).build()
+----
+
+
[[webflux-client-retrieve]]
== `retrieve()`
diff --git a/src/docs/asciidoc/web/webflux.adoc b/src/docs/asciidoc/web/webflux.adoc
index 15290ab1ffce..eb7f92d27939 100644
--- a/src/docs/asciidoc/web/webflux.adoc
+++ b/src/docs/asciidoc/web/webflux.adoc
@@ -312,8 +312,9 @@ request handling, on top of which concrete programming models such as annotated
controllers and functional endpoints are built.
* For the client side, there is a basic `ClientHttpConnector` contract to perform HTTP
requests with non-blocking I/O and Reactive Streams back pressure, along with adapters for
-https://github.com/reactor/reactor-netty[Reactor Netty] and for the reactive
-https://github.com/jetty-project/jetty-reactive-httpclient[Jetty HttpClient].
+https://github.com/reactor/reactor-netty[Reactor Netty], reactive
+https://github.com/jetty-project/jetty-reactive-httpclient[Jetty HttpClient]
+and https://hc.apache.org/[Apache HttpComponents].
The higher level <> used in applications
builds on this basic contract.
* For client and server, <> for serialization and