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);
+}