Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

What is the right behavior of evaluatePreconditions on a date with milliseconds according to the specification?

While migrating my JAX-RS application from Jersey to Quarkus/Resteasy, I came across a behavior change with the method evaluatePreconditions(Date lastModified). Indeed, in my use case, the last modified date contains milliseconds and unfortunately the date format of the headers If-Modified-Since and Last-Modified doesn't support milliseconds as we can see in the RFC 2616.

Jersey trims the milliseconds from the provided date (as we can see here) while in Resteasy, the date is not modified so it actually compares dates (the date from the header If-Modified-Since and the provided date) with different precisions (respectively seconds versus milliseconds) which ends up with a mismatch so an HTTP status code 200.

The code that illustrates the issue:

@Path("/evaluatePreconditions")
public class EvaluatePreconditionsResource {

    @GET
    @Produces(MediaType.TEXT_PLAIN)
    public Response findData(@Context Request request) {
        final Data data = retrieveData();
        final Date lastModified = Timestamp.valueOf(data.getLastModified());
        final Response.ResponseBuilder responseBuilder = 
            request.evaluatePreconditions(lastModified);
        if (responseBuilder == null) {
            // Last modified date didn't match, send new content
            return Response.ok(data.toString())
                .lastModified(lastModified)
                .build();
        }
        // Sending 304 not modified
        return responseBuilder.build();
    }

    private Data retrieveData() {
        // Let's assume that we call a service here that provides this value
        // The date time is expressed in GMT+2, please adjust it according 
        // to your timezone
        return new Data(
            LocalDateTime.of(2020, 10, 2, 10, 23, 16, 1_000_000), 
            "This is my content"
        );
    }

    public static class Data {
        private final LocalDateTime lastModified;
        private final String content;

        public Data(LocalDateTime lastModified, String content) {
            this.lastModified = lastModified;
            this.content = content;
        }

        public LocalDateTime getLastModified() {
            return lastModified;
        }

        @Override
        public String toString() {
            return content;
        }
    }
}

The corresponding result with Jersey:

curl -H "If-Modified-Since: Fri, 02 Oct 2020 08:23:16 GMT" \
     -I localhost:8080/evaluatePreconditions
HTTP/1.1 304 Not Modified
...

The corresponding result with Quarkus/Resteasy:

curl -H "If-Modified-Since: Fri, 02 Oct 2020 08:23:16 GMT" \
     -I localhost:8080/evaluatePreconditions
HTTP/1.1 200 OK
Last-Modified: Fri, 02 Oct 2020 08:23:16 GMT
...

This behavior has already been raised in the Resteasy project, but for the team, trimming the date would add a new bug because if the data/resource is modified several times within the same second, we would get a 304 if we trim the date and 200 if we don't, which is a fair point. However, I maybe wrong but according to what I understand from the RFC 7232, if several modifications can happen within the same second, we are supposed to rely on an ETag too which means that in the JAX-RS specification, we are supposed to use evaluatePreconditions(Date lastModified, EntityTag eTag) instead.

So what is the correct behavior according to the JAX-RS specification regarding this particular case?

like image 587
Nicolas Filotto Avatar asked Oct 02 '20 11:10

Nicolas Filotto


2 Answers

The implementation of Request.evaluatePreconditions(Date lastModified) at Resteasy 4.5 is wrong. The implementation at class org.jboss.resteasy.specimpl.RequestImpl relies on a helper class DateUtil which expect the Last-Modified header to be in one of the formats: RFC 1123 "EEE, dd MMM yyyy HH:mm:ss zzz", RFC 1036 "EEEE, dd-MMM-yy HH:mm:ss zzz" or ANSI C "EEE MMM d HH:mm:ss yyyy". Of these three formats, only ANSI C is listed at RFC 7231 Section 7.1.1.1 and it is obsolete. The preferred format for an HTTP 1.1. header is as specified in RFC 5322 Section 3.3 and this format does not contain milliseconds. The format that Resteasy implementation refers as RFC 1123 actually comes from RFC 822 Section 5 but RFC 822 is for text messages (mail) not for HTTP headers. Java supports milliseconds at Date but HTTP headers do not. Therefore, comparing dates with different precisions is a bug. The correct implementation is the one at Jersey ContainerRequest which before comparing rounds down the date to the nearest second.

JAX-RS spec 1.1 does not say anything specifically at this regard. Or, at least, I've not been able to find it. JAX-RS spec does not need to address this issue. The implementation must handle HTTP headers as per HTTP specs, which do not include milliseconds in header timestamps.

like image 83
Serg M Ten Avatar answered Oct 22 '22 01:10

Serg M Ten


I think it is not specified, if the evaluatePreconditions methods should cut the fractions of a second or not. But: it is just "not fair" to compare two timestamps with different precision. You either should round the more precise one or truncate the precision to be the same. Especially since RFC 7232 even names the problem of the "low" precision of the HTTP header and suggests a solution (ETag).

I also found a SO question with solutions how to compare timestamps with different precisions: Compare Date objects with different levels of precision

like image 1
cyberbrain Avatar answered Oct 21 '22 23:10

cyberbrain