Skip to content

Commit 100bf88

Browse files
committed
Merge expires and Max-Age attributes
1 parent 4f607b5 commit 100bf88

File tree

5 files changed

+133
-15
lines changed

5 files changed

+133
-15
lines changed

spring-web/src/main/java/org/springframework/http/HttpHeaders.java

Lines changed: 23 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1549,13 +1549,9 @@ private ZonedDateTime getFirstZonedDateTime(String headerName, boolean rejectInv
15491549
headerValue = headerValue.substring(0, parametersIndex);
15501550
}
15511551

1552-
for (DateTimeFormatter dateFormatter : DATE_PARSERS) {
1553-
try {
1554-
return ZonedDateTime.parse(headerValue, dateFormatter);
1555-
}
1556-
catch (DateTimeParseException ex) {
1557-
// ignore
1558-
}
1552+
ZonedDateTime zonedDateTime = getZonedDateTime(headerValue);
1553+
if (zonedDateTime != null) {
1554+
return zonedDateTime;
15591555
}
15601556

15611557
}
@@ -1566,6 +1562,26 @@ private ZonedDateTime getFirstZonedDateTime(String headerName, boolean rejectInv
15661562
return null;
15671563
}
15681564

1565+
/**
1566+
* Parses the date in headers with Date formats specified in the HTTP RFC to use for parsing.
1567+
* {@link HttpHeaders#DATE_PARSERS}
1568+
* @param date the date header value as string
1569+
* @return the parsed date header value
1570+
*/
1571+
// used in ClientHttpResponse
1572+
@Nullable
1573+
public static ZonedDateTime getZonedDateTime(String date) {
1574+
for (DateTimeFormatter dateFormatter : DATE_PARSERS) {
1575+
try {
1576+
return ZonedDateTime.parse(date, dateFormatter);
1577+
}
1578+
catch (DateTimeParseException ex) {
1579+
// ignore
1580+
}
1581+
}
1582+
return null;
1583+
}
1584+
15691585
/**
15701586
* Return all values of a given header name, even if this header is set
15711587
* multiple times.

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

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,15 @@
1616

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

19+
import java.time.ZoneOffset;
20+
import java.time.ZonedDateTime;
21+
import java.time.temporal.ChronoUnit;
22+
23+
import org.springframework.http.HttpHeaders;
1924
import org.springframework.http.HttpStatusCode;
2025
import org.springframework.http.ReactiveHttpInputMessage;
2126
import org.springframework.http.ResponseCookie;
27+
import org.springframework.lang.Nullable;
2228
import org.springframework.util.MultiValueMap;
2329
import org.springframework.util.ObjectUtils;
2430

@@ -51,4 +57,24 @@ default String getId() {
5157
*/
5258
MultiValueMap<String, ResponseCookie> getCookies();
5359

60+
static long mergeMaxAgeAndExpires(@Nullable String maxAgeAttribute, @Nullable String expiresAttribute) {
61+
if (maxAgeAttribute != null) {
62+
return Long.parseLong(maxAgeAttribute);
63+
}
64+
else if (expiresAttribute != null) {
65+
ZonedDateTime expiresDate = HttpHeaders.getZonedDateTime(expiresAttribute);
66+
67+
// Verify that the input date is in the future
68+
ZonedDateTime now = ZonedDateTime.now(ZoneOffset.UTC);
69+
if (expiresDate == null || expiresDate.isBefore(now)) {
70+
return -1;
71+
}
72+
else {
73+
// Calculate the difference in seconds
74+
return ChronoUnit.SECONDS.between(now, expiresDate);
75+
}
76+
}
77+
return -1;
78+
}
79+
5480
}

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

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -70,8 +70,7 @@ private static MultiValueMap<String, ResponseCookie> adaptCookies(HttpClientCont
7070
}
7171

7272
private static long getMaxAgeSeconds(Cookie cookie) {
73-
String maxAgeAttribute = cookie.getAttribute(Cookie.MAX_AGE_ATTR);
74-
return (maxAgeAttribute != null ? Long.parseLong(maxAgeAttribute) : -1);
73+
return ClientHttpResponse.mergeMaxAgeAndExpires(cookie.getAttribute(Cookie.MAX_AGE_ATTR), cookie.getAttribute(Cookie.EXPIRES_ATTR));
7574
}
7675

7776

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

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -128,23 +128,26 @@ public MultiValueMap<String, ResponseCookie> getCookies() {
128128
ResponseCookie.fromClientResponse(cookie.name().toString(), cookie.value().toString())
129129
.domain(toString(cookie.domain()))
130130
.path(toString(cookie.path()))
131-
.maxAge(toLong(cookie.maxAge()))
131+
.maxAge(getMaxAgeSeconds(cookie))
132132
.secure(cookie.isSecure())
133133
.httpOnly(cookie.isHttpOnly())
134134
.sameSite(getSameSite(cookie))
135135
.build()));
136136
return CollectionUtils.unmodifiableMultiValueMap(result);
137137
}
138138

139+
private static long getMaxAgeSeconds(HttpSetCookie cookie) {
140+
String maxAge = (cookie.maxAge() == null) ? null : cookie.maxAge().toString();
141+
String expires = toString(cookie.expires());
142+
143+
return ClientHttpResponse.mergeMaxAgeAndExpires(maxAge, expires);
144+
}
145+
139146
@Nullable
140147
private static String toString(@Nullable CharSequence value) {
141148
return (value != null ? value.toString() : null);
142149
}
143150

144-
private static long toLong(@Nullable Long value) {
145-
return (value != null ? value : -1);
146-
}
147-
148151
@Nullable
149152
private static String getSameSite(HttpSetCookie cookie) {
150153
if (cookie instanceof DefaultHttpSetCookie defaultCookie && defaultCookie.sameSite() != null) {

spring-web/src/test/java/org/springframework/http/client/reactive/ClientHttpConnectorTests.java

Lines changed: 75 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,9 @@
2323
import java.lang.annotation.Target;
2424
import java.net.URI;
2525
import java.nio.charset.StandardCharsets;
26+
import java.time.Duration;
27+
import java.time.ZonedDateTime;
28+
import java.time.format.DateTimeFormatter;
2629
import java.util.ArrayList;
2730
import java.util.Arrays;
2831
import java.util.List;
@@ -48,9 +51,11 @@
4851
import org.springframework.core.io.buffer.DataBuffer;
4952
import org.springframework.core.io.buffer.DataBufferUtils;
5053
import org.springframework.core.io.buffer.DefaultDataBufferFactory;
54+
import org.springframework.http.HttpCookie;
5155
import org.springframework.http.HttpMethod;
5256
import org.springframework.http.HttpStatus;
5357
import org.springframework.http.ReactiveHttpOutputMessage;
58+
import org.springframework.http.ResponseCookie;
5459

5560
import static org.assertj.core.api.Assertions.assertThat;
5661
import static org.assertj.core.api.Assertions.fail;
@@ -65,6 +70,9 @@ class ClientHttpConnectorTests {
6570

6671
private static final Set<HttpMethod> METHODS_WITH_BODY =
6772
Set.of(HttpMethod.PUT, HttpMethod.POST, HttpMethod.PATCH);
73+
public static final String MAX_AGE_AND_EXPIRES = "Max-Age-And-Expires";
74+
public static final String MAX_AGE_ONLY = "Max-Age-Only";
75+
public static final String EXPIRES_ONLY = "Expires-Only";
6876

6977
private final MockWebServer server = new MockWebServer();
7078

@@ -172,6 +180,70 @@ void cancelResponseBody(ClientHttpConnector connector) {
172180
.verify();
173181
}
174182

183+
@ParameterizedConnectorTest
184+
void testExpiresMaxAgeAttributes(ClientHttpConnector connector) {
185+
// maxAge is set to 12 days from system time.
186+
long maxAge = 1036800L;
187+
188+
// expires date is set to 10 days from system time.
189+
long expires = 864000L;
190+
ZonedDateTime currentDateTime = ZonedDateTime.now(java.time.ZoneOffset.UTC);
191+
ZonedDateTime futureDateTime = currentDateTime.plusSeconds(expires);
192+
193+
// processing time may affect the calculation of ZonedDateTime.now during merge of expires and max age
194+
// therefore we check range with buffer of 2 seconds
195+
long maxAgeLowerLimit = maxAge - 2;
196+
long maxAgeUpperLimit = maxAge + 2;
197+
long expiresLowerLimit = expires - 2;
198+
long expiresUpperLimit = expires + 2;
199+
200+
List<HttpCookie> httpCookies = getHttpCookies(maxAge, futureDateTime);
201+
prepareResponse(response -> {
202+
response.setResponseCode(200);
203+
httpCookies.forEach(httpCookie -> response.addHeader("Set-Cookie", httpCookie.toString()));
204+
});
205+
206+
ClientHttpResponse response = connector.connect(HttpMethod.POST, this.server.url("/").uri(),
207+
ReactiveHttpOutputMessage::setComplete).block();
208+
assertThat(response).isNotNull();
209+
assertThat(response.getCookies()).isNotEmpty();
210+
211+
List<ResponseCookie> maxAgeAndExpiresCookies = response.getCookies().get(MAX_AGE_AND_EXPIRES);
212+
assertThat(maxAgeAndExpiresCookies).size().isEqualTo(1);
213+
Duration maxAgeAndExpires = maxAgeAndExpiresCookies.get(0).getMaxAge();
214+
assertThat(maxAgeAndExpires.getSeconds()).isGreaterThanOrEqualTo(maxAgeLowerLimit);
215+
assertThat(maxAgeAndExpires.getSeconds()).isLessThanOrEqualTo(maxAgeUpperLimit);
216+
217+
List<ResponseCookie> maxAgeOnlyCookies = response.getCookies().get(MAX_AGE_ONLY);
218+
assertThat(maxAgeOnlyCookies).size().isEqualTo(1);
219+
Duration maxAgeOnly = maxAgeOnlyCookies.get(0).getMaxAge();
220+
assertThat(maxAgeOnly.getSeconds()).isGreaterThanOrEqualTo(maxAgeLowerLimit);
221+
assertThat(maxAgeOnly.getSeconds()).isLessThanOrEqualTo(maxAgeUpperLimit);
222+
223+
List<ResponseCookie> expiresOnlyCookies = response.getCookies().get(EXPIRES_ONLY);
224+
assertThat(expiresOnlyCookies).size().isEqualTo(1);
225+
Duration expiresOnly = expiresOnlyCookies.get(0).getMaxAge();
226+
assertThat(expiresOnly.getSeconds()).isGreaterThanOrEqualTo(expiresLowerLimit);
227+
assertThat(expiresOnly.getSeconds()).isLessThanOrEqualTo(expiresUpperLimit);
228+
}
229+
230+
private List<HttpCookie> getHttpCookies(long maxAge, ZonedDateTime futureDateTime) {
231+
String expires = futureDateTime.format(DateTimeFormatter.RFC_1123_DATE_TIME);
232+
233+
List<HttpCookie> httpCookies = new ArrayList<>();
234+
235+
String maxAgeAndExpiresValue = String.format("; Max-Age=%d; Expires=%s", maxAge, expires);
236+
httpCookies.add(new HttpCookie(MAX_AGE_AND_EXPIRES, maxAgeAndExpiresValue));
237+
238+
String maxAgeOnlyValue = String.format("; Max-Age=%d", maxAge);
239+
httpCookies.add(new HttpCookie(MAX_AGE_ONLY, maxAgeOnlyValue));
240+
241+
String expiresValue = String.format("; Expires=%s", expires);
242+
httpCookies.add(new HttpCookie(EXPIRES_ONLY, expiresValue));
243+
244+
return httpCookies;
245+
}
246+
175247
private Buffer randomBody(int size) {
176248
Buffer responseBody = new Buffer();
177249
Random rnd = new Random();
@@ -211,7 +283,9 @@ static List<Named<ClientHttpConnector>> connectors() {
211283
return Arrays.asList(
212284
named("Reactor Netty", new ReactorClientHttpConnector()),
213285
named("Jetty", new JettyClientHttpConnector()),
214-
named("HttpComponents", new HttpComponentsClientHttpConnector())
286+
named("HttpComponents", new HttpComponentsClientHttpConnector()),
287+
named("Jdk", new JdkClientHttpConnector()),
288+
named("Reactor Netty 2", new ReactorNetty2ClientHttpConnector())
215289
);
216290
}
217291

0 commit comments

Comments
 (0)