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> 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 body) { + return doCommit(() -> { + this.byteBufferFlux = Flux.from(body).map(DataBuffer::asByteBuffer); + return Mono.empty(); + }); + } + + @Override + public Mono writeAndFlushWith(Publisher> 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