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 b0006d2267a..194d572fd6f 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 @@ -34,6 +34,8 @@ import reactor.core.publisher.Mono; import org.springframework.core.io.buffer.DataBufferFactory; import org.springframework.core.io.buffer.DefaultDataBufferFactory; import org.springframework.http.HttpMethod; +import org.springframework.http.support.DefaultHttpCookieParser; +import org.springframework.http.support.HttpCookieParser; import org.springframework.util.Assert; /** @@ -50,6 +52,8 @@ public class JdkClientHttpConnector implements ClientHttpConnector { private DataBufferFactory bufferFactory = DefaultDataBufferFactory.sharedInstance; + private HttpCookieParser httpCookieParser = new DefaultHttpCookieParser(); + private @Nullable Duration readTimeout; @@ -105,6 +109,16 @@ public class JdkClientHttpConnector implements ClientHttpConnector { this.readTimeout = readTimeout; } + /** + * Set the {@code HttpCookieParser} to be used in response parsing. + *

Default is {@code DefaultHttpCookieParser} based on {@code java.net.HttpCookie} capabilities

+ * @param httpCookieParser + */ + public void setHttpCookieParser(HttpCookieParser httpCookieParser) { + Assert.notNull(readTimeout, "httpCookieParser is required"); + this.httpCookieParser = httpCookieParser; + } + @Override public Mono connect( @@ -120,7 +134,7 @@ public class JdkClientHttpConnector implements ClientHttpConnector { this.httpClient.sendAsync(httpRequest, HttpResponse.BodyHandlers.ofPublisher()); return Mono.fromCompletionStage(future) - .map(response -> new JdkClientHttpResponse(response, this.bufferFactory)); + .map(response -> new JdkClientHttpResponse(response, this.bufferFactory, this.httpCookieParser)); })); } diff --git a/spring-web/src/main/java/org/springframework/http/client/reactive/JdkClientHttpResponse.java b/spring-web/src/main/java/org/springframework/http/client/reactive/JdkClientHttpResponse.java index 92ed16d8e45..ab60012f2da 100644 --- a/spring-web/src/main/java/org/springframework/http/client/reactive/JdkClientHttpResponse.java +++ b/spring-web/src/main/java/org/springframework/http/client/reactive/JdkClientHttpResponse.java @@ -16,7 +16,6 @@ package org.springframework.http.client.reactive; -import java.net.HttpCookie; import java.net.http.HttpClient; import java.net.http.HttpResponse; import java.nio.ByteBuffer; @@ -25,10 +24,7 @@ import java.util.Locale; import java.util.Map; import java.util.concurrent.Flow; import java.util.function.Function; -import java.util.regex.Matcher; -import java.util.regex.Pattern; -import org.jspecify.annotations.Nullable; import reactor.adapter.JdkFlowAdapter; import reactor.core.publisher.Flux; @@ -38,6 +34,7 @@ import org.springframework.core.io.buffer.DataBufferUtils; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatusCode; import org.springframework.http.ResponseCookie; +import org.springframework.http.support.HttpCookieParser; import org.springframework.util.CollectionUtils; import org.springframework.util.LinkedCaseInsensitiveMap; import org.springframework.util.LinkedMultiValueMap; @@ -52,16 +49,12 @@ import org.springframework.util.MultiValueMap; */ class JdkClientHttpResponse extends AbstractClientHttpResponse { - private static final Pattern SAME_SITE_PATTERN = Pattern.compile("(?i).*SameSite=(Strict|Lax|None).*"); - - - public JdkClientHttpResponse(HttpResponse>> response, - DataBufferFactory bufferFactory) { + DataBufferFactory bufferFactory, HttpCookieParser httpCookieParser) { super(HttpStatusCode.valueOf(response.statusCode()), adaptHeaders(response), - adaptCookies(response), + adaptCookies(response, httpCookieParser), adaptBody(response, bufferFactory) ); } @@ -74,29 +67,15 @@ class JdkClientHttpResponse extends AbstractClientHttpResponse { return HttpHeaders.readOnlyHttpHeaders(multiValueMap); } - private static MultiValueMap adaptCookies(HttpResponse>> response) { + private static MultiValueMap adaptCookies(HttpResponse>> response, + HttpCookieParser httpCookieParser) { return response.headers().allValues(HttpHeaders.SET_COOKIE).stream() - .flatMap(header -> { - Matcher matcher = SAME_SITE_PATTERN.matcher(header); - String sameSite = (matcher.matches() ? matcher.group(1) : null); - return HttpCookie.parse(header).stream().map(cookie -> toResponseCookie(cookie, sameSite)); - }) + .flatMap(httpCookieParser::parse) .collect(LinkedMultiValueMap::new, (cookies, cookie) -> cookies.add(cookie.getName(), cookie), LinkedMultiValueMap::addAll); } - private static ResponseCookie toResponseCookie(HttpCookie cookie, @Nullable String sameSite) { - return ResponseCookie.from(cookie.getName(), cookie.getValue()) - .domain(cookie.getDomain()) - .httpOnly(cookie.isHttpOnly()) - .maxAge(cookie.getMaxAge()) - .path(cookie.getPath()) - .secure(cookie.getSecure()) - .sameSite(sameSite) - .build(); - } - private static Flux adaptBody(HttpResponse>> response, DataBufferFactory bufferFactory) { return JdkFlowAdapter.flowPublisherToFlux(response.body()) .flatMapIterable(Function.identity()) 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 284733051c8..c341a87cf1a 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 @@ -28,6 +28,8 @@ import reactor.core.publisher.Mono; import org.springframework.core.io.buffer.DataBuffer; import org.springframework.core.io.buffer.JettyDataBufferFactory; import org.springframework.http.HttpMethod; +import org.springframework.http.support.DefaultHttpCookieParser; +import org.springframework.http.support.HttpCookieParser; import org.springframework.util.Assert; /** @@ -43,6 +45,8 @@ public class JettyClientHttpConnector implements ClientHttpConnector { private JettyDataBufferFactory bufferFactory = new JettyDataBufferFactory(); + private HttpCookieParser httpCookieParser = new DefaultHttpCookieParser(); + /** * Default constructor that creates a new instance of {@link HttpClient}. @@ -83,6 +87,12 @@ public class JettyClientHttpConnector implements ClientHttpConnector { this.bufferFactory = bufferFactory; } + /** + * Set the cookie parser to use. + */ + public void setHttpCookieParser(HttpCookieParser httpCookieParser) { + this.httpCookieParser = httpCookieParser; + } @Override public Mono connect(HttpMethod method, URI uri, @@ -111,7 +121,7 @@ public class JettyClientHttpConnector implements ClientHttpConnector { return Mono.fromDirect(request.toReactiveRequest() .response((reactiveResponse, chunkPublisher) -> { Flux content = Flux.from(chunkPublisher).map(this.bufferFactory::wrap); - return Mono.just(new JettyClientHttpResponse(reactiveResponse, content)); + return Mono.just(new JettyClientHttpResponse(reactiveResponse, content, this.httpCookieParser)); })); } diff --git a/spring-web/src/main/java/org/springframework/http/client/reactive/JettyClientHttpResponse.java b/spring-web/src/main/java/org/springframework/http/client/reactive/JettyClientHttpResponse.java index 6f4e161f181..b69e959a622 100644 --- a/spring-web/src/main/java/org/springframework/http/client/reactive/JettyClientHttpResponse.java +++ b/spring-web/src/main/java/org/springframework/http/client/reactive/JettyClientHttpResponse.java @@ -16,20 +16,17 @@ package org.springframework.http.client.reactive; -import java.net.HttpCookie; import java.util.List; -import java.util.regex.Matcher; -import java.util.regex.Pattern; import org.eclipse.jetty.http.HttpField; import org.eclipse.jetty.reactive.client.ReactiveResponse; -import org.jspecify.annotations.Nullable; import reactor.core.publisher.Flux; import org.springframework.core.io.buffer.DataBuffer; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatusCode; import org.springframework.http.ResponseCookie; +import org.springframework.http.support.HttpCookieParser; import org.springframework.http.support.JettyHeadersAdapter; import org.springframework.util.CollectionUtils; import org.springframework.util.LinkedMultiValueMap; @@ -45,14 +42,11 @@ import org.springframework.util.MultiValueMap; */ class JettyClientHttpResponse extends AbstractClientHttpResponse { - private static final Pattern SAME_SITE_PATTERN = Pattern.compile("(?i).*SameSite=(Strict|Lax|None).*"); - - - public JettyClientHttpResponse(ReactiveResponse reactiveResponse, Flux content) { + public JettyClientHttpResponse(ReactiveResponse reactiveResponse, Flux content, HttpCookieParser httpCookieParser) { super(HttpStatusCode.valueOf(reactiveResponse.getStatus()), adaptHeaders(reactiveResponse), - adaptCookies(reactiveResponse), + adaptCookies(reactiveResponse, httpCookieParser), content); } @@ -60,26 +54,14 @@ class JettyClientHttpResponse extends AbstractClientHttpResponse { MultiValueMap headers = new JettyHeadersAdapter(response.getHeaders()); return HttpHeaders.readOnlyHttpHeaders(headers); } - private static MultiValueMap adaptCookies(ReactiveResponse response) { - MultiValueMap result = new LinkedMultiValueMap<>(); + private static MultiValueMap adaptCookies(ReactiveResponse response, HttpCookieParser httpCookieParser) { List cookieHeaders = response.getHeaders().getFields(HttpHeaders.SET_COOKIE); - cookieHeaders.forEach(header -> - HttpCookie.parse(header.getValue()).forEach(cookie -> result.add(cookie.getName(), - ResponseCookie.fromClientResponse(cookie.getName(), cookie.getValue()) - .domain(cookie.getDomain()) - .path(cookie.getPath()) - .maxAge(cookie.getMaxAge()) - .secure(cookie.getSecure()) - .httpOnly(cookie.isHttpOnly()) - .sameSite(parseSameSite(header.getValue())) - .build())) - ); + MultiValueMap result = cookieHeaders.stream() + .flatMap(header -> httpCookieParser.parse(header.getValue())) + .collect(LinkedMultiValueMap::new, + (cookies, cookie) -> cookies.add(cookie.getName(), cookie), + LinkedMultiValueMap::addAll); return CollectionUtils.unmodifiableMultiValueMap(result); } - private static @Nullable String parseSameSite(String headerValue) { - Matcher matcher = SAME_SITE_PATTERN.matcher(headerValue); - return (matcher.matches() ? matcher.group(1) : null); - } - } diff --git a/spring-web/src/main/java/org/springframework/http/support/DefaultHttpCookieParser.java b/spring-web/src/main/java/org/springframework/http/support/DefaultHttpCookieParser.java new file mode 100644 index 00000000000..7fcea7402db --- /dev/null +++ b/spring-web/src/main/java/org/springframework/http/support/DefaultHttpCookieParser.java @@ -0,0 +1,33 @@ +package org.springframework.http.support; + +import org.springframework.http.ResponseCookie; + +import java.net.HttpCookie; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.stream.Stream; + +import org.jspecify.annotations.Nullable; + +public final class DefaultHttpCookieParser implements HttpCookieParser { + + private static final Pattern SAME_SITE_PATTERN = Pattern.compile("(?i).*SameSite=(Strict|Lax|None).*"); + + @Override + public Stream parse(String header) { + Matcher matcher = SAME_SITE_PATTERN.matcher(header); + String sameSite = (matcher.matches() ? matcher.group(1) : null); + return HttpCookie.parse(header).stream().map(cookie -> toResponseCookie(cookie, sameSite)); + } + + private static ResponseCookie toResponseCookie(HttpCookie cookie, @Nullable String sameSite) { + return ResponseCookie.from(cookie.getName(), cookie.getValue()) + .domain(cookie.getDomain()) + .httpOnly(cookie.isHttpOnly()) + .maxAge(cookie.getMaxAge()) + .path(cookie.getPath()) + .secure(cookie.getSecure()) + .sameSite(sameSite) + .build(); + } +} diff --git a/spring-web/src/main/java/org/springframework/http/support/HttpCookieParser.java b/spring-web/src/main/java/org/springframework/http/support/HttpCookieParser.java new file mode 100644 index 00000000000..e9e0703eab9 --- /dev/null +++ b/spring-web/src/main/java/org/springframework/http/support/HttpCookieParser.java @@ -0,0 +1,10 @@ +package org.springframework.http.support; + +import org.springframework.http.ResponseCookie; + +import java.util.stream.Stream; + +public interface HttpCookieParser { + + Stream parse(String header); +}