From 5efb385e64c2b4aaaf5017ba9bfdfee09632991c Mon Sep 17 00:00:00 2001 From: Brian Clozel Date: Fri, 13 Sep 2024 15:49:06 +0200 Subject: [PATCH] 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 --- .../HttpComponentsClientHttpResponse.java | 21 ++++++++++++-- .../reactive/ClientHttpConnectorTests.java | 29 ++++++++++++++++++- 2 files changed, 47 insertions(+), 3 deletions(-) diff --git a/spring-web/src/main/java/org/springframework/http/client/reactive/HttpComponentsClientHttpResponse.java b/spring-web/src/main/java/org/springframework/http/client/reactive/HttpComponentsClientHttpResponse.java index ce8b9482d60..52e28133393 100644 --- a/spring-web/src/main/java/org/springframework/http/client/reactive/HttpComponentsClientHttpResponse.java +++ b/spring-web/src/main/java/org/springframework/http/client/reactive/HttpComponentsClientHttpResponse.java @@ -17,6 +17,10 @@ package org.springframework.http.client.reactive; import java.nio.ByteBuffer; +import java.time.Duration; +import java.time.ZonedDateTime; +import java.time.format.DateTimeFormatter; +import java.time.format.DateTimeParseException; import org.apache.hc.client5.http.cookie.Cookie; import org.apache.hc.client5.http.protocol.HttpClientContext; @@ -70,9 +74,22 @@ class HttpComponentsClientHttpResponse extends AbstractClientHttpResponse { } private static long getMaxAgeSeconds(Cookie cookie) { + String expiresAttribute = cookie.getAttribute(Cookie.EXPIRES_ATTR); String maxAgeAttribute = cookie.getAttribute(Cookie.MAX_AGE_ATTR); - return (maxAgeAttribute != null ? Long.parseLong(maxAgeAttribute) : -1); + if (maxAgeAttribute != null) { + return Long.parseLong(maxAgeAttribute); + } + // only consider expires if max-age is not present + else if (expiresAttribute != null) { + try { + ZonedDateTime expiresDate = ZonedDateTime.parse(expiresAttribute, DateTimeFormatter.RFC_1123_DATE_TIME); + return Duration.between(ZonedDateTime.now(expiresDate.getZone()), expiresDate).toSeconds(); + } + catch (DateTimeParseException ex) { + // ignore + } + } + return -1; } - } diff --git a/spring-web/src/test/java/org/springframework/http/client/reactive/ClientHttpConnectorTests.java b/spring-web/src/test/java/org/springframework/http/client/reactive/ClientHttpConnectorTests.java index d3ee539b9c4..84fd36860ef 100644 --- a/spring-web/src/test/java/org/springframework/http/client/reactive/ClientHttpConnectorTests.java +++ b/spring-web/src/test/java/org/springframework/http/client/reactive/ClientHttpConnectorTests.java @@ -23,6 +23,10 @@ import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; import java.net.URI; import java.nio.charset.StandardCharsets; +import java.time.Duration; +import java.time.ZoneId; +import java.time.ZonedDateTime; +import java.time.format.DateTimeFormatter; import java.util.ArrayList; import java.util.Arrays; import java.util.List; @@ -57,7 +61,9 @@ import static org.assertj.core.api.Assertions.fail; import static org.junit.jupiter.api.Named.named; /** + * Tests for {@link ClientHttpConnector} implementations. * @author Arjen Poutsma + * @author Brian Clozel */ class ClientHttpConnectorTests { @@ -172,6 +178,26 @@ class ClientHttpConnectorTests { .verify(); } + @ParameterizedConnectorTest + void cookieExpireValueSetAsMaxAge(ClientHttpConnector connector) { + ZonedDateTime tomorrow = ZonedDateTime.now(ZoneId.of("UTC")).plusDays(1); + String formattedDate = tomorrow.format(DateTimeFormatter.RFC_1123_DATE_TIME); + + prepareResponse(response -> { + response.setResponseCode(200); + response.addHeader("Set-Cookie", "id=test; Expires= " + formattedDate + ";"); + }); + Mono futureResponse = + connector.connect(HttpMethod.GET, this.server.url("/").uri(), ReactiveHttpOutputMessage::setComplete); + StepVerifier.create(futureResponse) + .assertNext(response -> { + assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); + assertThat(response.getCookies().getFirst("id").getMaxAge()).isCloseTo(Duration.ofDays(1), Duration.ofSeconds(10)); + } + ) + .verifyComplete(); + } + private Buffer randomBody(int size) { Buffer responseBody = new Buffer(); Random rnd = new Random(); @@ -211,7 +237,8 @@ class ClientHttpConnectorTests { return Arrays.asList( named("Reactor Netty", new ReactorClientHttpConnector()), named("Jetty", new JettyClientHttpConnector()), - named("HttpComponents", new HttpComponentsClientHttpConnector()) + named("HttpComponents", new HttpComponentsClientHttpConnector()), + named("Jdk", new JdkClientHttpConnector()) ); }