Skip to content

Commit 7fc4937

Browse files
committedJun 7, 2024·
Add Partitioned cookie attribute support for servers
This commit adds support for the "Partitioned" cookie attribute in WebFlux servers and the related testing infrastructure. Note, Undertow does not support this feature at the moment. Closes gh-31454
1 parent 2aabe23 commit 7fc4937

File tree

18 files changed

+178
-10
lines changed

18 files changed

+178
-10
lines changed
 

‎spring-test/src/main/java/org/springframework/mock/web/MockCookie.java

+27
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,24 @@ public String getSameSite() {
9898
return getAttribute(SAME_SITE);
9999
}
100100

101+
/**
102+
* Set the "Partitioned" attribute for this cookie.
103+
* @since 6.2
104+
* @see <a href="https://datatracker.ietf.org/doc/html/draft-cutler-httpbis-partitioned-cookies#section-2.1">The Partitioned attribute spec</a>
105+
*/
106+
public void setPartitioned(boolean partitioned) {
107+
setAttribute("Partitioned", "");
108+
}
109+
110+
/**
111+
* Return whether the "Partitioned" attribute is set for this cookie.
112+
* @since 6.2
113+
* @see <a href="https://datatracker.ietf.org/doc/html/draft-cutler-httpbis-partitioned-cookies#section-2.1">The Partitioned attribute spec</a>
114+
*/
115+
public boolean isPartitioned() {
116+
return getAttribute("Partitioned") != null;
117+
}
118+
101119
/**
102120
* Factory method that parses the value of the supplied "Set-Cookie" header.
103121
* @param setCookieHeader the "Set-Cookie" value; never {@code null} or empty
@@ -146,6 +164,9 @@ else if (StringUtils.startsWithIgnoreCase(attribute, SAME_SITE)) {
146164
else if (StringUtils.startsWithIgnoreCase(attribute, "Comment")) {
147165
cookie.setComment(extractAttributeValue(attribute, setCookieHeader));
148166
}
167+
else if (!attribute.isEmpty()) {
168+
cookie.setAttribute(attribute, extractOptionalAttributeValue(attribute, setCookieHeader));
169+
}
149170
}
150171
return cookie;
151172
}
@@ -157,6 +178,11 @@ private static String extractAttributeValue(String attribute, String header) {
157178
return nameAndValue[1];
158179
}
159180

181+
private static String extractOptionalAttributeValue(String attribute, String header) {
182+
String[] nameAndValue = attribute.split("=");
183+
return nameAndValue.length == 2 ? nameAndValue[1] : "";
184+
}
185+
160186
@Override
161187
public void setAttribute(String name, @Nullable String value) {
162188
if (EXPIRES.equalsIgnoreCase(name)) {
@@ -176,6 +202,7 @@ public String toString() {
176202
.append("Comment", getComment())
177203
.append("Secure", getSecure())
178204
.append("HttpOnly", isHttpOnly())
205+
.append("Partitioned", isPartitioned())
179206
.append(SAME_SITE, getSameSite())
180207
.append("Max-Age", getMaxAge())
181208
.append(EXPIRES, getAttribute(EXPIRES))

‎spring-test/src/main/java/org/springframework/mock/web/MockHttpServletResponse.java

+3
Original file line numberDiff line numberDiff line change
@@ -481,6 +481,9 @@ else if (expires != null) {
481481
if (cookie.isHttpOnly()) {
482482
buf.append("; HttpOnly");
483483
}
484+
if (cookie.getAttribute("Partitioned") != null) {
485+
buf.append("; Partitioned");
486+
}
484487
if (cookie instanceof MockCookie mockCookie) {
485488
if (StringUtils.hasText(mockCookie.getSameSite())) {
486489
buf.append("; SameSite=").append(mockCookie.getSameSite());

‎spring-test/src/main/java/org/springframework/test/web/reactive/server/CookieAssertions.java

+13
Original file line numberDiff line numberDiff line change
@@ -197,6 +197,19 @@ public WebTestClient.ResponseSpec httpOnly(String name, boolean expected) {
197197
return this.responseSpec;
198198
}
199199

200+
/**
201+
* Assert a cookie's "Partitioned" attribute.
202+
* @since 6.2
203+
*/
204+
public WebTestClient.ResponseSpec partitioned(String name, boolean expected) {
205+
boolean isPartitioned = getCookie(name).isPartitioned();
206+
this.exchangeResult.assertWithDiagnostics(() -> {
207+
String message = getMessage(name) + " isPartitioned";
208+
assertEquals(message, expected, isPartitioned);
209+
});
210+
return this.responseSpec;
211+
}
212+
200213
/**
201214
* Assert a cookie's "SameSite" attribute.
202215
*/

‎spring-test/src/main/java/org/springframework/test/web/servlet/client/MockMvcHttpConnector.java

+1
Original file line numberDiff line numberDiff line change
@@ -209,6 +209,7 @@ private MockClientHttpResponse adaptResponse(MvcResult mvcResult) {
209209
.path(cookie.getPath())
210210
.secure(cookie.getSecure())
211211
.httpOnly(cookie.isHttpOnly())
212+
.partitioned(cookie.getAttribute("Partitioned") != null)
212213
.sameSite(cookie.getAttribute("samesite"))
213214
.build();
214215
clientResponse.getCookies().add(httpCookie.getName(), httpCookie);

‎spring-test/src/main/java/org/springframework/test/web/servlet/result/CookieResultMatchers.java

+12-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2023 the original author or authors.
2+
* Copyright 2002-2024 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.
@@ -229,6 +229,17 @@ public ResultMatcher httpOnly(String name, boolean httpOnly) {
229229
};
230230
}
231231

232+
/**
233+
* Assert whether the cookie is partitioned.
234+
* @since 6.2
235+
*/
236+
public ResultMatcher partitioned(String name, boolean partitioned) {
237+
return result -> {
238+
Cookie cookie = getCookie(result, name);
239+
assertEquals("Response cookie '" + name + "' partitioned", partitioned, cookie.getAttribute("Partitioned") != null);
240+
};
241+
}
242+
232243
/**
233244
* Assert a cookie's specified attribute with a Hamcrest {@link Matcher}.
234245
* @param cookieAttribute the name of the Cookie attribute (case-insensitive)

‎spring-test/src/main/kotlin/org/springframework/test/web/servlet/result/CookieResultMatchersDsl.kt

+8
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,14 @@ class CookieResultMatchersDsl internal constructor (private val actions: ResultA
157157
actions.andExpect(matchers.httpOnly(name, httpOnly))
158158
}
159159

160+
/**
161+
* @see CookieResultMatchers.partitioned
162+
* @since 6.2
163+
*/
164+
fun partitioned(name: String, partitioned: Boolean) {
165+
actions.andExpect(matchers.partitioned(name, partitioned))
166+
}
167+
160168
/**
161169
* @see CookieResultMatchers.attribute
162170
* @since 6.0.8

‎spring-test/src/test/java/org/springframework/mock/web/MockCookieTests.java

+10-1
Original file line numberDiff line numberDiff line change
@@ -71,14 +71,15 @@ void parseHeaderWithoutAttributes() {
7171
@Test
7272
void parseHeaderWithAttributes() {
7373
MockCookie cookie = MockCookie.parse("SESSION=123; Domain=example.com; Max-Age=60; " +
74-
"Expires=Tue, 8 Oct 2019 19:50:00 GMT; Path=/; Secure; HttpOnly; SameSite=Lax");
74+
"Expires=Tue, 8 Oct 2019 19:50:00 GMT; Path=/; Secure; HttpOnly; Partitioned; SameSite=Lax");
7575

7676
assertCookie(cookie, "SESSION", "123");
7777
assertThat(cookie.getDomain()).isEqualTo("example.com");
7878
assertThat(cookie.getMaxAge()).isEqualTo(60);
7979
assertThat(cookie.getPath()).isEqualTo("/");
8080
assertThat(cookie.getSecure()).isTrue();
8181
assertThat(cookie.isHttpOnly()).isTrue();
82+
assertThat(cookie.isPartitioned()).isTrue();
8283
assertThat(cookie.getExpires()).isEqualTo(ZonedDateTime.parse("Tue, 8 Oct 2019 19:50:00 GMT",
8384
DateTimeFormatter.RFC_1123_DATE_TIME));
8485
assertThat(cookie.getSameSite()).isEqualTo("Lax");
@@ -203,4 +204,12 @@ void setInvalidAttributeExpiresShouldThrow() {
203204
assertThatThrownBy(() -> cookie.setAttribute("expires", "12345")).isInstanceOf(DateTimeParseException.class);
204205
}
205206

207+
@Test
208+
void setPartitioned() {
209+
MockCookie cookie = new MockCookie("SESSION", "123");
210+
cookie.setAttribute("Partitioned", "");
211+
212+
assertThat(cookie.isPartitioned()).isTrue();
213+
}
214+
206215
}

‎spring-test/src/test/java/org/springframework/mock/web/MockHttpServletResponseTests.java

+2-1
Original file line numberDiff line numberDiff line change
@@ -274,12 +274,13 @@ void cookies() {
274274
cookie.setMaxAge(0);
275275
cookie.setSecure(true);
276276
cookie.setHttpOnly(true);
277+
cookie.setAttribute("Partitioned", "");
277278

278279
response.addCookie(cookie);
279280

280281
assertThat(response.getHeader(SET_COOKIE)).isEqualTo(("foo=bar; Path=/path; Domain=example.com; " +
281282
"Max-Age=0; Expires=Thu, 01 Jan 1970 00:00:00 GMT; " +
282-
"Secure; HttpOnly"));
283+
"Secure; HttpOnly; Partitioned"));
283284
}
284285

285286
@Test

‎spring-test/src/test/java/org/springframework/test/web/reactive/server/CookieAssertionTests.java ‎spring-test/src/test/java/org/springframework/test/web/reactive/server/CookieAssertionsTests.java

+8-1
Original file line numberDiff line numberDiff line change
@@ -37,14 +37,15 @@
3737
*
3838
* @author Rossen Stoyanchev
3939
*/
40-
public class CookieAssertionTests {
40+
public class CookieAssertionsTests {
4141

4242
private final ResponseCookie cookie = ResponseCookie.from("foo", "bar")
4343
.maxAge(Duration.ofMinutes(30))
4444
.domain("foo.com")
4545
.path("/foo")
4646
.secure(true)
4747
.httpOnly(true)
48+
.partitioned(true)
4849
.sameSite("Lax")
4950
.build();
5051

@@ -117,6 +118,12 @@ void httpOnly() {
117118
assertThatExceptionOfType(AssertionError.class).isThrownBy(() -> assertions.httpOnly("foo", false));
118119
}
119120

121+
@Test
122+
void partitioned() {
123+
assertions.partitioned("foo", true);
124+
assertThatExceptionOfType(AssertionError.class).isThrownBy(() -> assertions.partitioned("foo", false));
125+
}
126+
120127
@Test
121128
void sameSite() {
122129
assertions.sameSite("foo", "Lax");

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

+33-2
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,8 @@ public final class ResponseCookie extends HttpCookie {
4747

4848
private final boolean httpOnly;
4949

50+
private final boolean partitioned;
51+
5052
@Nullable
5153
private final String sameSite;
5254

@@ -55,7 +57,7 @@ public final class ResponseCookie extends HttpCookie {
5557
* Private constructor. See {@link #from(String, String)}.
5658
*/
5759
private ResponseCookie(String name, @Nullable String value, Duration maxAge, @Nullable String domain,
58-
@Nullable String path, boolean secure, boolean httpOnly, @Nullable String sameSite) {
60+
@Nullable String path, boolean secure, boolean httpOnly, boolean partitioned, @Nullable String sameSite) {
5961

6062
super(name, value);
6163
Assert.notNull(maxAge, "Max age must not be null");
@@ -65,6 +67,7 @@ private ResponseCookie(String name, @Nullable String value, Duration maxAge, @Nu
6567
this.path = path;
6668
this.secure = secure;
6769
this.httpOnly = httpOnly;
70+
this.partitioned = partitioned;
6871
this.sameSite = sameSite;
6972

7073
Rfc6265Utils.validateCookieName(name);
@@ -116,6 +119,15 @@ public boolean isHttpOnly() {
116119
return this.httpOnly;
117120
}
118121

122+
/**
123+
* Return {@code true} if the cookie has the "Partitioned" attribute.
124+
* @since 6.2
125+
* @see <a href="https://datatracker.ietf.org/doc/html/draft-cutler-httpbis-partitioned-cookies#section-2.1">The Partitioned attribute spec</a>
126+
*/
127+
public boolean isPartitioned() {
128+
return this.partitioned;
129+
}
130+
119131
/**
120132
* Return the cookie "SameSite" attribute, or {@code null} if not set.
121133
* <p>This limits the scope of the cookie such that it will only be attached to
@@ -139,6 +151,7 @@ public ResponseCookieBuilder mutate() {
139151
.path(this.path)
140152
.secure(this.secure)
141153
.httpOnly(this.httpOnly)
154+
.partitioned(this.partitioned)
142155
.sameSite(this.sameSite);
143156
}
144157

@@ -180,6 +193,9 @@ public String toString() {
180193
if (this.httpOnly) {
181194
sb.append("; HttpOnly");
182195
}
196+
if (this.partitioned) {
197+
sb.append("; Partitioned");
198+
}
183199
if (StringUtils.hasText(this.sameSite)) {
184200
sb.append("; SameSite=").append(this.sameSite);
185201
}
@@ -272,6 +288,13 @@ public interface ResponseCookieBuilder {
272288
*/
273289
ResponseCookieBuilder httpOnly(boolean httpOnly);
274290

291+
/**
292+
* Add the "Partitioned" attribute to the cookie.
293+
* @since 6.2
294+
* @see <a href="https://datatracker.ietf.org/doc/html/draft-cutler-httpbis-partitioned-cookies#section-2.1">The Partitioned attribute spec</a>
295+
*/
296+
ResponseCookieBuilder partitioned(boolean partitioned);
297+
275298
/**
276299
* Add the "SameSite" attribute to the cookie.
277300
* <p>This limits the scope of the cookie such that it will only be
@@ -397,6 +420,8 @@ private static class DefaultResponseCookieBuilder implements ResponseCookieBuild
397420

398421
private boolean httpOnly;
399422

423+
private boolean partitioned;
424+
400425
@Nullable
401426
private String sameSite;
402427

@@ -461,6 +486,12 @@ public ResponseCookieBuilder httpOnly(boolean httpOnly) {
461486
return this;
462487
}
463488

489+
@Override
490+
public ResponseCookieBuilder partitioned(boolean partitioned) {
491+
this.partitioned = partitioned;
492+
return this;
493+
}
494+
464495
@Override
465496
public ResponseCookieBuilder sameSite(@Nullable String sameSite) {
466497
this.sameSite = sameSite;
@@ -470,7 +501,7 @@ public ResponseCookieBuilder sameSite(@Nullable String sameSite) {
470501
@Override
471502
public ResponseCookie build() {
472503
return new ResponseCookie(this.name, this.value, this.maxAge,
473-
this.domain, this.path, this.secure, this.httpOnly, this.sameSite);
504+
this.domain, this.path, this.secure, this.httpOnly, this.partitioned, this.sameSite);
474505
}
475506
}
476507

‎spring-web/src/main/java/org/springframework/http/server/reactive/ReactorNetty2ServerHttpResponse.java

+1
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,7 @@ protected void applyCookies() {
111111
for (ResponseCookie httpCookie : getCookies().get(name)) {
112112
Long maxAge = (!httpCookie.getMaxAge().isNegative()) ? httpCookie.getMaxAge().getSeconds() : null;
113113
HttpSetCookie.SameSite sameSite = (httpCookie.getSameSite() != null) ? HttpSetCookie.SameSite.valueOf(httpCookie.getSameSite()) : null;
114+
// TODO: support Partitioned attribute when available in Netty 5 API
114115
DefaultHttpSetCookie cookie = new DefaultHttpSetCookie(name, httpCookie.getValue(), httpCookie.getPath(),
115116
httpCookie.getDomain(), null, maxAge, sameSite, false, httpCookie.isSecure(), httpCookie.isHttpOnly());
116117
this.response.addCookie(cookie);

‎spring-web/src/main/java/org/springframework/http/server/reactive/ReactorServerHttpResponse.java

+1
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,7 @@ protected void applyCookies() {
120120
}
121121
cookie.setSecure(httpCookie.isSecure());
122122
cookie.setHttpOnly(httpCookie.isHttpOnly());
123+
cookie.setPartitioned(httpCookie.isPartitioned());
123124
if (httpCookie.getSameSite() != null) {
124125
cookie.setSameSite(CookieHeaderNames.SameSite.valueOf(httpCookie.getSameSite()));
125126
}

‎spring-web/src/main/java/org/springframework/http/server/reactive/ServletServerHttpResponse.java

+11
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@
3939
import org.springframework.http.ResponseCookie;
4040
import org.springframework.lang.Nullable;
4141
import org.springframework.util.Assert;
42+
import org.springframework.util.ReflectionUtils;
4243

4344
/**
4445
* Adapt {@link ServerHttpResponse} to the Servlet {@link HttpServletResponse}.
@@ -49,6 +50,8 @@
4950
*/
5051
class ServletServerHttpResponse extends AbstractListenerServerHttpResponse {
5152

53+
private static final boolean IS_SERVLET61 = ReflectionUtils.findField(HttpServletResponse.class, "SC_PERMANENT_REDIRECT") != null;
54+
5255
private final HttpServletResponse response;
5356

5457
private final ServletOutputStream outputStream;
@@ -181,6 +184,14 @@ protected void applyCookies() {
181184
}
182185
cookie.setSecure(httpCookie.isSecure());
183186
cookie.setHttpOnly(httpCookie.isHttpOnly());
187+
if (httpCookie.isPartitioned()) {
188+
if (IS_SERVLET61) {
189+
cookie.setAttribute("Partitioned", "");
190+
}
191+
else {
192+
cookie.setAttribute("Partitioned", "true");
193+
}
194+
}
184195
this.response.addCookie(cookie);
185196
}
186197
}

‎spring-web/src/main/java/org/springframework/http/server/reactive/UndertowServerHttpResponse.java

+1
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,7 @@ protected void applyCookies() {
122122
}
123123
cookie.setSecure(httpCookie.isSecure());
124124
cookie.setHttpOnly(httpCookie.isHttpOnly());
125+
// TODO: add "Partitioned" attribute when Undertow supports it
125126
cookie.setSameSiteMode(httpCookie.getSameSite());
126127
this.exchange.setResponseCookie(cookie);
127128
}

‎spring-web/src/test/java/org/springframework/http/ResponseCookieTests.java

+2-2
Original file line numberDiff line numberDiff line change
@@ -37,12 +37,12 @@ void basic() {
3737
assertThat(ResponseCookie.from("id", "1fWa").build().toString()).isEqualTo("id=1fWa");
3838

3939
ResponseCookie cookie = ResponseCookie.from("id", "1fWa")
40-
.domain("abc").path("/path").maxAge(0).httpOnly(true).secure(true).sameSite("None")
40+
.domain("abc").path("/path").maxAge(0).httpOnly(true).partitioned(true).secure(true).sameSite("None")
4141
.build();
4242

4343
assertThat(cookie.toString()).isEqualTo("id=1fWa; Path=/path; Domain=abc; " +
4444
"Max-Age=0; Expires=Thu, 01 Jan 1970 00:00:00 GMT; " +
45-
"Secure; HttpOnly; SameSite=None");
45+
"Secure; HttpOnly; Partitioned; SameSite=None");
4646
}
4747

4848
@Test

‎spring-web/src/test/java/org/springframework/http/server/reactive/CookieIntegrationTests.java

+21-2
Original file line numberDiff line numberDiff line change
@@ -70,13 +70,32 @@ public void basicTest(HttpServer httpServer) throws Exception {
7070
List<String> cookie0 = splitCookie(headerValues.get(0));
7171
assertThat(cookie0.remove("SID=31d4d96e407aad42")).as("SID").isTrue();
7272
assertThat(cookie0.stream().map(String::toLowerCase))
73-
.containsExactlyInAnyOrder("path=/", "secure", "httponly");
73+
.contains("path=/", "secure", "httponly");
7474
List<String> cookie1 = splitCookie(headerValues.get(1));
7575
assertThat(cookie1.remove("lang=en-US")).as("lang").isTrue();
7676
assertThat(cookie1.stream().map(String::toLowerCase))
7777
.containsExactlyInAnyOrder("path=/", "domain=example.com");
7878
}
7979

80+
@ParameterizedHttpServerTest
81+
public void partitionedAttributeTest(HttpServer httpServer) throws Exception {
82+
assumeFalse(httpServer instanceof UndertowHttpServer, "Undertow does not support Partitioned cookies");
83+
startServer(httpServer);
84+
85+
URI url = URI.create("http://localhost:" + port);
86+
String header = "SID=31d4d96e407aad42; lang=en-US";
87+
ResponseEntity<Void> response = new RestTemplate().exchange(
88+
RequestEntity.get(url).header("Cookie", header).build(), Void.class);
89+
90+
List<String> headerValues = response.getHeaders().get("Set-Cookie");
91+
assertThat(headerValues).hasSize(2);
92+
93+
List<String> cookie0 = splitCookie(headerValues.get(0));
94+
assertThat(cookie0.remove("SID=31d4d96e407aad42")).as("SID").isTrue();
95+
assertThat(cookie0.stream().map(String::toLowerCase))
96+
.contains("partitioned");
97+
}
98+
8099
@ParameterizedHttpServerTest
81100
public void cookiesWithSameNameTest(HttpServer httpServer) throws Exception {
82101
assumeFalse(httpServer instanceof UndertowHttpServer, "Bug in Undertow in Cookies with same name handling");
@@ -116,7 +135,7 @@ public Mono<Void> handle(ServerHttpRequest request, ServerHttpResponse response)
116135
this.requestCookies.size(); // Cause lazy loading
117136

118137
response.getCookies().add("SID", ResponseCookie.from("SID", "31d4d96e407aad42")
119-
.path("/").secure(true).httpOnly(true).build());
138+
.path("/").secure(true).httpOnly(true).partitioned(true).build());
120139
response.getCookies().add("lang", ResponseCookie.from("lang", "en-US")
121140
.domain("example.com").path("/").build());
122141

‎spring-web/src/testFixtures/java/org/springframework/web/testfixture/servlet/MockCookie.java

+21
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,24 @@ public String getSameSite() {
9898
return getAttribute(SAME_SITE);
9999
}
100100

101+
/**
102+
* Set the "Partitioned" attribute for this cookie.
103+
* @since 6.2
104+
* @see <a href="https://datatracker.ietf.org/doc/html/draft-cutler-httpbis-partitioned-cookies#section-2.1">The Partitioned attribute spec</a>
105+
*/
106+
public void setPartitioned(boolean partitioned) {
107+
setAttribute("Partitioned", "");
108+
}
109+
110+
/**
111+
* Return whether the "Partitioned" attribute is set for this cookie.
112+
* @since 6.2
113+
* @see <a href="https://datatracker.ietf.org/doc/html/draft-cutler-httpbis-partitioned-cookies#section-2.1">The Partitioned attribute spec</a>
114+
*/
115+
public boolean isPartitioned() {
116+
return getAttribute("Partitioned") != null;
117+
}
118+
101119
/**
102120
* Factory method that parses the value of the supplied "Set-Cookie" header.
103121
* @param setCookieHeader the "Set-Cookie" value; never {@code null} or empty
@@ -146,6 +164,9 @@ else if (StringUtils.startsWithIgnoreCase(attribute, SAME_SITE)) {
146164
else if (StringUtils.startsWithIgnoreCase(attribute, "Comment")) {
147165
cookie.setComment(extractAttributeValue(attribute, setCookieHeader));
148166
}
167+
else {
168+
cookie.setAttribute(attribute, extractAttributeValue(attribute, setCookieHeader));
169+
}
149170
}
150171
return cookie;
151172
}

‎spring-web/src/testFixtures/java/org/springframework/web/testfixture/servlet/MockHttpServletResponse.java

+3
Original file line numberDiff line numberDiff line change
@@ -481,6 +481,9 @@ else if (expires != null) {
481481
if (cookie.isHttpOnly()) {
482482
buf.append("; HttpOnly");
483483
}
484+
if (cookie.getAttribute("Partitioned") != null) {
485+
buf.append("; Partitioned");
486+
}
484487
if (cookie instanceof MockCookie mockCookie) {
485488
if (StringUtils.hasText(mockCookie.getSameSite())) {
486489
buf.append("; SameSite=").append(mockCookie.getSameSite());

0 commit comments

Comments
 (0)
Please sign in to comment.