diff --git a/spring-web/src/main/java/org/springframework/http/HttpHeaders.java b/spring-web/src/main/java/org/springframework/http/HttpHeaders.java index bcedd663b45..5b11c127cc9 100644 --- a/spring-web/src/main/java/org/springframework/http/HttpHeaders.java +++ b/spring-web/src/main/java/org/springframework/http/HttpHeaders.java @@ -29,7 +29,9 @@ import java.time.Instant; import java.time.ZoneId; import java.time.ZonedDateTime; import java.time.format.DateTimeFormatter; +import java.time.format.DateTimeFormatterBuilder; import java.time.format.DateTimeParseException; +import java.time.temporal.ChronoField; import java.util.AbstractSet; import java.util.ArrayList; import java.util.Base64; @@ -435,7 +437,9 @@ public class HttpHeaders implements Serializable { private static final DateTimeFormatter[] DATE_PARSERS = new DateTimeFormatter[] { DateTimeFormatter.RFC_1123_DATE_TIME, DateTimeFormatter.ofPattern("EEEE, dd-MMM-yy HH:mm:ss zzz", Locale.US), - DateTimeFormatter.ofPattern("EEE MMM dd HH:mm:ss yyyy", Locale.US).withZone(GMT) + DateTimeFormatter.ofPattern("EEE MMM dd HH:mm:ss yyyy", Locale.US).withZone(GMT), + // RFC 9651: Structured Field Values for HTTP + new DateTimeFormatterBuilder().appendLiteral('@').appendValue(ChronoField.INSTANT_SECONDS).toFormatter(Locale.US).withZone(GMT) }; @@ -1621,7 +1625,7 @@ public class HttpHeaders implements Serializable { // No header value sent at all return null; } - if (headerValue.length() >= 3) { + if (headerValue.length() >= 3 || headerValue.startsWith("@")) { // Short "0" or "-1" like values are never valid HTTP date headers... // Let's only bother with DateTimeFormatter parsing for long enough values. diff --git a/spring-web/src/test/java/org/springframework/http/HttpHeadersTests.java b/spring-web/src/test/java/org/springframework/http/HttpHeadersTests.java index 8970cbfc049..112c17fbed0 100644 --- a/spring-web/src/test/java/org/springframework/http/HttpHeadersTests.java +++ b/spring-web/src/test/java/org/springframework/http/HttpHeadersTests.java @@ -36,6 +36,9 @@ import java.util.TimeZone; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; +import org.junit.jupiter.params.provider.ValueSource; import static java.util.stream.Collectors.toList; import static org.assertj.core.api.Assertions.assertThat; @@ -654,6 +657,36 @@ class HttpHeadersTests { assertThat(authorization).isEqualTo("Bearer foo"); } + @ParameterizedTest + @CsvSource(value = { + "@0,0", + "@1,1000", + "@10,10000", + "@-1,-1000", + "@1659578233,1659578233000", // Example from RFC + "@-62135596800,-62135596800000", // Example from RFC + "@253402214400,253402214400000", // Example from RFC + }) + void rfc9651Dates_valid(String value, long timestampMillis) { + headers.set("Deprecation", value); + assertThat(headers.getFirstDate("Deprecation")).isEqualTo(timestampMillis); + } + + @ParameterizedTest + @ValueSource(strings = { + "@", + " @1", + "@ 1", + "@0000-", + "@15+", + "@12p", + "@0x15", + }) + void rfc9651Dates_invalid(String value) { + headers.set("Deprecation", value); + assertThatIllegalArgumentException().isThrownBy(() -> headers.getFirstDate("Deprecation")); + } + @Nested class MapEntriesTests {