1

It would appear that since Spring 5.0 the DateTime format: ANSI C's asctime() as specified in the RFC2616 no longer gets parsed correctly when a single-digit is provided (i.e; 9 rather than 09).

When looking at the test-method: HttpHeadersTest.firstZonedDateTime() provided by Spring; we can see that for the: "ANSI C's asctime() format" a double-digit (i.e; 02) is provided as test-input rather than a single-digit (i.e; 2) as specified in the RFC2616 (3.3.1).

I have written a test-method to showcase the potential bug.

/**
 * Assumption: ANSI C's single-digit date (i.e; 0-9) should be viable syntax as specified in the RFC2616: https://www.rfc-editor.org/rfc/rfc2616#section-3.3.1
 * Expected output: assertThat(true) & assertThat(true)
 * Actual output: assertThat(true) & assertThat(false)
 *
 * throws: java.lang.IllegalArgumentException: Cannot parse date value "Fri Jun 2 02:22:00 2017" for "Date" header
 */
@Test
public void firstZonedDateTimeANSI(){
    ZonedDateTime date = ZonedDateTime.of(2017, 6, 2, 2, 22, 0, 0, ZoneId.of("GMT"));

    // ANSI C's asctime() format where single digit dates are represented as double digits (i.e; 2 as 02)
    headers.set(HttpHeaders.DATE, "Fri Jun 02 02:22:00 2017");
    assertThat(headers.getFirstZonedDateTime(HttpHeaders.DATE)                                                      // getFirstZonedDateTime parses the Date Syntax as ANSI (HttpHeaders.DATE_PARSERS[2])
            .isEqual(date))
            .isTrue();                                                                                              // expected assertThat(true) vs actual assertThat(true)
    headers.clear();

    // ANSI C's asctime() format where single digit dates are viable (i.e; 2 as 2 not 02); as
    headers.set(HttpHeaders.DATE, "Fri Jun 2 02:22:00 2017");
    assertThat(headers.getFirstZonedDateTime(HttpHeaders.DATE)                                                      // getFirstZonedDateTime throws java.time.format.DateTimeParseException: Text 'Fri Jun 2 02:22:00 2017' could not be parsed at index 8
            .isEqual(date))
            .isTrue();                                                                                              // expected assertThat(true) vs actual assertThat(false)
}

I would expect the above test to assert true even for single-digit input. But as you can see, by running the test-method, an error is thrown:

throws: java.lang.IllegalArgumentException: Cannot parse date value "Fri Jun 2 02:22:00 2017" for "Date" header.

When taking a closer look using a debugger; the error can be traced to:

java.time.format.DateTimeParseException: Text 'Fri Jun 2 02:22:00 2017' could not be parsed at index 8

It seems that as of Spring 5.0 a new way of parsing Header DateTime is being applied. See HttpHeaders.getFirstZonedDataTime(String headerName):

/**
 * Parse the first header value for the given header name as a date,
 * return {@code null} if there is no value, or raise {@link IllegalArgumentException}
 * if the value cannot be parsed as a date.
 * @param headerName the header name
 * @return the parsed date header, or {@code null} if none
 * @since 5.0
 */
@Nullable
public ZonedDateTime getFirstZonedDateTime(String headerName) {
    return getFirstZonedDateTime(headerName, true);
}

/**
 * Parse the first header value for the given header name as a date,
 * return {@code null} if there is no value or also in case of an invalid value
 * (if {@code rejectInvalid=false}), or raise {@link IllegalArgumentException}
 * if the value cannot be parsed as a date.
 * @param headerName the header name
 * @param rejectInvalid whether to reject invalid values with an
 * {@link IllegalArgumentException} ({@code true}) or rather return {@code null}
 * in that case ({@code false})
 * @return the parsed date header, or {@code null} if none (or invalid)
 */
@Nullable
private ZonedDateTime getFirstZonedDateTime(String headerName, boolean rejectInvalid) {
    String headerValue = getFirst(headerName);
    if (headerValue == null) {
        // No header value sent at all
        return null;
    }
    if (headerValue.length() >= 3) {
        // Short "0" or "-1" like values are never valid HTTP date headers...
        // Let's only bother with DateTimeFormatter parsing for long enough values.

        // See https://stackoverflow.com/questions/12626699/if-modified-since-http-header-passed-by-ie9-includes-length
        int parametersIndex = headerValue.indexOf(';');
        if (parametersIndex != -1) {
            headerValue = headerValue.substring(0, parametersIndex);
        }

        for (DateTimeFormatter dateFormatter : DATE_PARSERS) {
            try {
                return ZonedDateTime.parse(headerValue, dateFormatter);
            }
            catch (DateTimeParseException ex) {
                // ignore
            }
        }

    }
    if (rejectInvalid) {
        throw new IllegalArgumentException("Cannot parse date value \"" + headerValue +
                "\" for \"" + headerName + "\" header");
    }
    return null;
}

I believe the bug was introduced in Spring 5.0, more specifically in this loop at private ZonedDateTime getFirstZonedDateTime(String headerName, boolean rejectInvalid):

for (DateTimeFormatter dateFormatter : DATE_PARSERS) {
        try {
            return ZonedDateTime.parse(headerValue, dateFormatter);
        }
        catch (DateTimeParseException ex) {
            // ignore
        }
    }

When looking at the last functional build: Spring 4.3 a similar loop was used: private long getFirstDate(String headerName, boolean rejectInvalid)

        for (String dateFormat : DATE_FORMATS) {
            SimpleDateFormat simpleDateFormat = new SimpleDateFormat(dateFormat, Locale.US);
            simpleDateFormat.setTimeZone(GMT);
            try {
                return simpleDateFormat.parse(headerValue).getTime();
            }
            catch (ParseException ex) {
                // ignore
            }
        }

But whereas Spring 4.3 still used java.text.SimpleDateFormat to do the parsing, as of Spring 5.0 Java.time.format.ZonedDateTime is being used for parsing.

Both Spring 4.3 and Spring 5.0 consult the same private static array for iteration:

/**
 * Date formats with time zone as specified in the HTTP RFC to use for parsing.
 * @see <a href="https://www.rfc-editor.org/rfc/rfc7231#section-7.1.1.1">Section 7.1.1.1 of RFC 7231</a>
 */
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)
};

To conclude:

I believe a bug was introduced as of Spring 5.0 where ANSI C's asctime() format as defined in RFC2616 is no longer parsed correctly when a single-digit is being parsed;

I believe the cause of the bug is the change from simpleDateFormat to ZonedDateTime for parsing.

I would like anyone to reproduce this bug before I submit it to Spring via Github; to ensure I didn't make any mistakes in the test-case or the assumptions.

This is my first post; forgive any mistakes; (structured)feedback is welcome.

Community
  • 1
  • 1
  • 2
    If you believe this is a bug you should report it to the spring github issues. If there are any mistakes in the test cases you will get comments there. SO is not the right place for this. – seenukarthi Aug 15 '19 at 08:10
  • With a team of 10 authors; the Spring team has 804 open-tickets they currently have to address. I disagree with your sentiment that S.O. would not be the right place for this. Especially since I clarify in my post that I would like someone to falsify or confirm my findings before I make the ticket count 805. Keyword being before, since if the bug is indeed confirmed, Github is indeed the place where I will file a bug report; which is not what I am doing now – Kevin Stoffers Aug 15 '19 at 08:43

0 Answers0