Skip to content

Commit 667004e

Browse files
committedFeb 12, 2025
Update contribution
See gh-34081
1 parent ba74de9 commit 667004e

8 files changed

+136
-99
lines changed
 

‎spring-web/src/main/java/org/springframework/http/ResponseCookie.java

+46-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2024 the original author or authors.
2+
* Copyright 2002-2025 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -17,10 +17,13 @@
1717
package org.springframework.http;
1818

1919
import java.time.Duration;
20+
import java.util.List;
2021

2122
import org.jspecify.annotations.Nullable;
2223

2324
import org.springframework.util.Assert;
25+
import org.springframework.util.LinkedMultiValueMap;
26+
import org.springframework.util.MultiValueMap;
2427
import org.springframework.util.ObjectUtils;
2528
import org.springframework.util.StringUtils;
2629

@@ -235,6 +238,22 @@ public static ResponseCookieBuilder fromClientResponse(final String name, final
235238
return new DefaultResponseCookieBuilder(name, value, true);
236239
}
237240

241+
/**
242+
* Factory method to obtain a builder that copies from {@link java.net.HttpCookie}.
243+
* @param cookie the source cookie to copy from
244+
* @return a builder to create the cookie with
245+
* @since 7.0
246+
*/
247+
public static ResponseCookieBuilder from(java.net.HttpCookie cookie) {
248+
return ResponseCookie.from(cookie.getName(), cookie.getValue())
249+
.domain(cookie.getDomain())
250+
.httpOnly(cookie.isHttpOnly())
251+
.maxAge(cookie.getMaxAge())
252+
.path(cookie.getPath())
253+
.secure(cookie.getSecure());
254+
}
255+
256+
238257

239258
/**
240259
* A builder for a server-defined HttpCookie with attributes.
@@ -307,6 +326,32 @@ public interface ResponseCookieBuilder {
307326
}
308327

309328

329+
/**
330+
* Contract to parse {@code "Set-Cookie"} headers.
331+
* @since 7.0
332+
*/
333+
public interface Parser {
334+
335+
/**
336+
* Parse the given header.
337+
*/
338+
List<ResponseCookie> parse(String header);
339+
340+
/**
341+
* Convenience method to parse a list of headers into a {@link MultiValueMap}.
342+
*/
343+
default MultiValueMap<String, ResponseCookie> parse(List<String> headers) {
344+
MultiValueMap<String, ResponseCookie> result = new LinkedMultiValueMap<>();
345+
for (String header : headers) {
346+
for (ResponseCookie cookie : parse(header)) {
347+
result.add(cookie.getName(), cookie);
348+
}
349+
}
350+
return result;
351+
}
352+
}
353+
354+
310355
private static class Rfc6265Utils {
311356

312357
private static final String SEPARATOR_CHARS = new String(new char[] {

‎spring-web/src/main/java/org/springframework/http/client/reactive/JdkClientHttpConnector.java

+17-12
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2021 the original author or authors.
2+
* Copyright 2002-2025 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -33,9 +33,9 @@
3333

3434
import org.springframework.core.io.buffer.DataBufferFactory;
3535
import org.springframework.core.io.buffer.DefaultDataBufferFactory;
36+
import org.springframework.http.HttpHeaders;
3637
import org.springframework.http.HttpMethod;
37-
import org.springframework.http.support.DefaultHttpCookieParser;
38-
import org.springframework.http.support.HttpCookieParser;
38+
import org.springframework.http.ResponseCookie;
3939
import org.springframework.util.Assert;
4040

4141
/**
@@ -52,10 +52,10 @@ public class JdkClientHttpConnector implements ClientHttpConnector {
5252

5353
private DataBufferFactory bufferFactory = DefaultDataBufferFactory.sharedInstance;
5454

55-
private HttpCookieParser httpCookieParser = new DefaultHttpCookieParser();
56-
5755
private @Nullable Duration readTimeout;
5856

57+
private ResponseCookie.Parser cookieParser = new JdkResponseCookieParser();
58+
5959

6060
/**
6161
* Default constructor that uses {@link HttpClient#newHttpClient()}.
@@ -110,13 +110,15 @@ public void setReadTimeout(Duration readTimeout) {
110110
}
111111

112112
/**
113-
* Set the {@code HttpCookieParser} to be used in response parsing.
114-
* <p>Default is {@code DefaultHttpCookieParser} based on {@code java.net.HttpCookie} capabilities</p>
115-
* @param httpCookieParser
113+
* Customize the parsing of response cookies.
114+
* <p>By default, {@link java.net.HttpCookie#parse(String)} is used, and
115+
* additionally the sameSite attribute is parsed and set.
116+
* @param parser the parser to use
117+
* @since 7.0
116118
*/
117-
public void setHttpCookieParser(HttpCookieParser httpCookieParser) {
118-
Assert.notNull(readTimeout, "httpCookieParser is required");
119-
this.httpCookieParser = httpCookieParser;
119+
public void setCookieParser(ResponseCookie.Parser parser) {
120+
Assert.notNull(parser, "ResponseCookie parser is required");
121+
this.cookieParser = parser;
120122
}
121123

122124

@@ -134,7 +136,10 @@ public Mono<ClientHttpResponse> connect(
134136
this.httpClient.sendAsync(httpRequest, HttpResponse.BodyHandlers.ofPublisher());
135137

136138
return Mono.fromCompletionStage(future)
137-
.map(response -> new JdkClientHttpResponse(response, this.bufferFactory, this.httpCookieParser));
139+
.map(response -> {
140+
List<String> headers = response.headers().allValues(HttpHeaders.SET_COOKIE);
141+
return new JdkClientHttpResponse(response, this.bufferFactory, this.cookieParser.parse(headers));
142+
});
138143
}));
139144
}
140145

‎spring-web/src/main/java/org/springframework/http/client/reactive/JdkClientHttpResponse.java

+3-16
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2024 the original author or authors.
2+
* Copyright 2002-2025 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -34,10 +34,8 @@
3434
import org.springframework.http.HttpHeaders;
3535
import org.springframework.http.HttpStatusCode;
3636
import org.springframework.http.ResponseCookie;
37-
import org.springframework.http.support.HttpCookieParser;
3837
import org.springframework.util.CollectionUtils;
3938
import org.springframework.util.LinkedCaseInsensitiveMap;
40-
import org.springframework.util.LinkedMultiValueMap;
4139
import org.springframework.util.MultiValueMap;
4240

4341
/**
@@ -50,12 +48,10 @@
5048
class JdkClientHttpResponse extends AbstractClientHttpResponse {
5149

5250
public JdkClientHttpResponse(HttpResponse<Flow.Publisher<List<ByteBuffer>>> response,
53-
DataBufferFactory bufferFactory, HttpCookieParser httpCookieParser) {
51+
DataBufferFactory bufferFactory, MultiValueMap<String, ResponseCookie> cookies) {
5452

5553
super(HttpStatusCode.valueOf(response.statusCode()),
56-
adaptHeaders(response),
57-
adaptCookies(response, httpCookieParser),
58-
adaptBody(response, bufferFactory)
54+
adaptHeaders(response), cookies, adaptBody(response, bufferFactory)
5955
);
6056
}
6157

@@ -67,15 +63,6 @@ private static HttpHeaders adaptHeaders(HttpResponse<Flow.Publisher<List<ByteBuf
6763
return HttpHeaders.readOnlyHttpHeaders(multiValueMap);
6864
}
6965

70-
private static MultiValueMap<String, ResponseCookie> adaptCookies(HttpResponse<Flow.Publisher<List<ByteBuffer>>> response,
71-
HttpCookieParser httpCookieParser) {
72-
return response.headers().allValues(HttpHeaders.SET_COOKIE).stream()
73-
.flatMap(httpCookieParser::parse)
74-
.collect(LinkedMultiValueMap::new,
75-
(cookies, cookie) -> cookies.add(cookie.getName(), cookie),
76-
LinkedMultiValueMap::addAll);
77-
}
78-
7966
private static Flux<DataBuffer> adaptBody(HttpResponse<Flow.Publisher<List<ByteBuffer>>> response, DataBufferFactory bufferFactory) {
8067
return JdkFlowAdapter.flowPublisherToFlux(response.body())
8168
.flatMapIterable(Function.identity())
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
/*
2+
* Copyright 2002-2025 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.http.client.reactive;
18+
19+
import java.util.ArrayList;
20+
import java.util.List;
21+
import java.util.regex.Matcher;
22+
import java.util.regex.Pattern;
23+
24+
import org.springframework.http.ResponseCookie;
25+
26+
27+
/**
28+
* Parser that delegates to {@link java.net.HttpCookie#parse(String)} for parsing,
29+
* but also extracts and sets {@code sameSite}.
30+
*
31+
* @author Rossen Stoyanchev
32+
* @since 7.0
33+
*/
34+
final class JdkResponseCookieParser implements ResponseCookie.Parser {
35+
36+
private static final Pattern SAME_SITE_PATTERN = Pattern.compile("(?i).*SameSite=(Strict|Lax|None).*");
37+
38+
39+
/**
40+
* Parse the given headers.
41+
*/
42+
public List<ResponseCookie> parse(String header) {
43+
Matcher matcher = SAME_SITE_PATTERN.matcher(header);
44+
String sameSite = (matcher.matches() ? matcher.group(1) : null);
45+
List<java.net.HttpCookie> cookies = java.net.HttpCookie.parse(header);
46+
List<ResponseCookie> result = new ArrayList<>(cookies.size());
47+
cookies.forEach(cookie -> result.add(ResponseCookie.from(cookie).sameSite(sameSite).build()));
48+
return result;
49+
}
50+
51+
}

‎spring-web/src/main/java/org/springframework/http/client/reactive/JettyClientHttpConnector.java

+15-7
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
package org.springframework.http.client.reactive;
1818

1919
import java.net.URI;
20+
import java.util.List;
2021
import java.util.function.Function;
2122

2223
import org.eclipse.jetty.client.HttpClient;
@@ -27,9 +28,9 @@
2728

2829
import org.springframework.core.io.buffer.DataBuffer;
2930
import org.springframework.core.io.buffer.JettyDataBufferFactory;
31+
import org.springframework.http.HttpHeaders;
3032
import org.springframework.http.HttpMethod;
31-
import org.springframework.http.support.DefaultHttpCookieParser;
32-
import org.springframework.http.support.HttpCookieParser;
33+
import org.springframework.http.ResponseCookie;
3334
import org.springframework.util.Assert;
3435

3536
/**
@@ -45,7 +46,7 @@ public class JettyClientHttpConnector implements ClientHttpConnector {
4546

4647
private JettyDataBufferFactory bufferFactory = new JettyDataBufferFactory();
4748

48-
private HttpCookieParser httpCookieParser = new DefaultHttpCookieParser();
49+
private ResponseCookie.Parser cookieParser = new JdkResponseCookieParser();
4950

5051

5152
/**
@@ -88,12 +89,18 @@ public void setBufferFactory(JettyDataBufferFactory bufferFactory) {
8889
}
8990

9091
/**
91-
* Set the cookie parser to use.
92+
* Customize the parsing of response cookies.
93+
* <p>By default, {@link java.net.HttpCookie#parse(String)} is used, and
94+
* additionally the sameSite attribute is parsed and set.
95+
* @param parser the parser to use
96+
* @since 7.0
9297
*/
93-
public void setHttpCookieParser(HttpCookieParser httpCookieParser) {
94-
this.httpCookieParser = httpCookieParser;
98+
public void setCookieParser(ResponseCookie.Parser parser) {
99+
Assert.notNull(parser, "ResponseCookie parser is required");
100+
this.cookieParser = parser;
95101
}
96102

103+
97104
@Override
98105
public Mono<ClientHttpResponse> connect(HttpMethod method, URI uri,
99106
Function<? super ClientHttpRequest, Mono<Void>> requestCallback) {
@@ -121,7 +128,8 @@ private Mono<ClientHttpResponse> execute(JettyClientHttpRequest request) {
121128
return Mono.fromDirect(request.toReactiveRequest()
122129
.response((reactiveResponse, chunkPublisher) -> {
123130
Flux<DataBuffer> content = Flux.from(chunkPublisher).map(this.bufferFactory::wrap);
124-
return Mono.just(new JettyClientHttpResponse(reactiveResponse, content, this.httpCookieParser));
131+
List<String> headers = reactiveResponse.getHeaders().getValuesList(HttpHeaders.SET_COOKIE);
132+
return Mono.just(new JettyClientHttpResponse(reactiveResponse, content, this.cookieParser.parse(headers)));
125133
}));
126134
}
127135

‎spring-web/src/main/java/org/springframework/http/client/reactive/JettyClientHttpResponse.java

+4-20
Original file line numberDiff line numberDiff line change
@@ -16,20 +16,14 @@
1616

1717
package org.springframework.http.client.reactive;
1818

19-
import java.util.List;
20-
21-
import org.eclipse.jetty.http.HttpField;
2219
import org.eclipse.jetty.reactive.client.ReactiveResponse;
2320
import reactor.core.publisher.Flux;
2421

2522
import org.springframework.core.io.buffer.DataBuffer;
2623
import org.springframework.http.HttpHeaders;
2724
import org.springframework.http.HttpStatusCode;
2825
import org.springframework.http.ResponseCookie;
29-
import org.springframework.http.support.HttpCookieParser;
3026
import org.springframework.http.support.JettyHeadersAdapter;
31-
import org.springframework.util.CollectionUtils;
32-
import org.springframework.util.LinkedMultiValueMap;
3327
import org.springframework.util.MultiValueMap;
3428

3529
/**
@@ -42,26 +36,16 @@
4236
*/
4337
class JettyClientHttpResponse extends AbstractClientHttpResponse {
4438

45-
public JettyClientHttpResponse(ReactiveResponse reactiveResponse, Flux<DataBuffer> content, HttpCookieParser httpCookieParser) {
39+
public JettyClientHttpResponse(
40+
ReactiveResponse reactiveResponse, Flux<DataBuffer> content,
41+
MultiValueMap<String, ResponseCookie> cookies) {
4642

47-
super(HttpStatusCode.valueOf(reactiveResponse.getStatus()),
48-
adaptHeaders(reactiveResponse),
49-
adaptCookies(reactiveResponse, httpCookieParser),
50-
content);
43+
super(HttpStatusCode.valueOf(reactiveResponse.getStatus()), adaptHeaders(reactiveResponse), cookies, content);
5144
}
5245

5346
private static HttpHeaders adaptHeaders(ReactiveResponse response) {
5447
MultiValueMap<String, String> headers = new JettyHeadersAdapter(response.getHeaders());
5548
return HttpHeaders.readOnlyHttpHeaders(headers);
5649
}
57-
private static MultiValueMap<String, ResponseCookie> adaptCookies(ReactiveResponse response, HttpCookieParser httpCookieParser) {
58-
List<HttpField> cookieHeaders = response.getHeaders().getFields(HttpHeaders.SET_COOKIE);
59-
MultiValueMap<String, ResponseCookie> result = cookieHeaders.stream()
60-
.flatMap(header -> httpCookieParser.parse(header.getValue()))
61-
.collect(LinkedMultiValueMap::new,
62-
(cookies, cookie) -> cookies.add(cookie.getName(), cookie),
63-
LinkedMultiValueMap::addAll);
64-
return CollectionUtils.unmodifiableMultiValueMap(result);
65-
}
6650

6751
}

‎spring-web/src/main/java/org/springframework/http/support/DefaultHttpCookieParser.java

-33
This file was deleted.

‎spring-web/src/main/java/org/springframework/http/support/HttpCookieParser.java

-10
This file was deleted.

0 commit comments

Comments
 (0)
Please sign in to comment.