Skip to content

Commit 5efb385

Browse files
committed
Read Expires cookie attribute in HttpComponents connector
Prior to this commit, the HttpComponents implementation for the `WebClient` would only consider the max-age attribute of response cookies when parsing the response. This is not aligned with other client implementations that consider the max-age attribute first, and then the expires if the former was not present. The expires date is then translated into a max-age duration. This behavior is done naturally by several implementations. This commit updates the `HttpComponentsClientHttpResponse` to do the same. Fixes gh-33157
1 parent b388ff6 commit 5efb385

File tree

2 files changed

+47
-3
lines changed

2 files changed

+47
-3
lines changed

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

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

1919
import java.nio.ByteBuffer;
20+
import java.time.Duration;
21+
import java.time.ZonedDateTime;
22+
import java.time.format.DateTimeFormatter;
23+
import java.time.format.DateTimeParseException;
2024

2125
import org.apache.hc.client5.http.cookie.Cookie;
2226
import org.apache.hc.client5.http.protocol.HttpClientContext;
@@ -70,9 +74,22 @@ private static MultiValueMap<String, ResponseCookie> adaptCookies(HttpClientCont
7074
}
7175

7276
private static long getMaxAgeSeconds(Cookie cookie) {
77+
String expiresAttribute = cookie.getAttribute(Cookie.EXPIRES_ATTR);
7378
String maxAgeAttribute = cookie.getAttribute(Cookie.MAX_AGE_ATTR);
74-
return (maxAgeAttribute != null ? Long.parseLong(maxAgeAttribute) : -1);
79+
if (maxAgeAttribute != null) {
80+
return Long.parseLong(maxAgeAttribute);
81+
}
82+
// only consider expires if max-age is not present
83+
else if (expiresAttribute != null) {
84+
try {
85+
ZonedDateTime expiresDate = ZonedDateTime.parse(expiresAttribute, DateTimeFormatter.RFC_1123_DATE_TIME);
86+
return Duration.between(ZonedDateTime.now(expiresDate.getZone()), expiresDate).toSeconds();
87+
}
88+
catch (DateTimeParseException ex) {
89+
// ignore
90+
}
91+
}
92+
return -1;
7593
}
7694

77-
7895
}

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

+28-1
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,10 @@
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.ZoneId;
28+
import java.time.ZonedDateTime;
29+
import java.time.format.DateTimeFormatter;
2630
import java.util.ArrayList;
2731
import java.util.Arrays;
2832
import java.util.List;
@@ -57,7 +61,9 @@
5761
import static org.junit.jupiter.api.Named.named;
5862

5963
/**
64+
* Tests for {@link ClientHttpConnector} implementations.
6065
* @author Arjen Poutsma
66+
* @author Brian Clozel
6167
*/
6268
class ClientHttpConnectorTests {
6369

@@ -172,6 +178,26 @@ void cancelResponseBody(ClientHttpConnector connector) {
172178
.verify();
173179
}
174180

181+
@ParameterizedConnectorTest
182+
void cookieExpireValueSetAsMaxAge(ClientHttpConnector connector) {
183+
ZonedDateTime tomorrow = ZonedDateTime.now(ZoneId.of("UTC")).plusDays(1);
184+
String formattedDate = tomorrow.format(DateTimeFormatter.RFC_1123_DATE_TIME);
185+
186+
prepareResponse(response -> {
187+
response.setResponseCode(200);
188+
response.addHeader("Set-Cookie", "id=test; Expires= " + formattedDate + ";");
189+
});
190+
Mono<ClientHttpResponse> futureResponse =
191+
connector.connect(HttpMethod.GET, this.server.url("/").uri(), ReactiveHttpOutputMessage::setComplete);
192+
StepVerifier.create(futureResponse)
193+
.assertNext(response -> {
194+
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
195+
assertThat(response.getCookies().getFirst("id").getMaxAge()).isCloseTo(Duration.ofDays(1), Duration.ofSeconds(10));
196+
}
197+
)
198+
.verifyComplete();
199+
}
200+
175201
private Buffer randomBody(int size) {
176202
Buffer responseBody = new Buffer();
177203
Random rnd = new Random();
@@ -211,7 +237,8 @@ static List<Named<ClientHttpConnector>> connectors() {
211237
return Arrays.asList(
212238
named("Reactor Netty", new ReactorClientHttpConnector()),
213239
named("Jetty", new JettyClientHttpConnector()),
214-
named("HttpComponents", new HttpComponentsClientHttpConnector())
240+
named("HttpComponents", new HttpComponentsClientHttpConnector()),
241+
named("Jdk", new JdkClientHttpConnector())
215242
);
216243
}
217244

0 commit comments

Comments
 (0)