This is an improvement request I wonder it has not been created before.

Consider the following code:

@GetMapping(path = "/{externalProjectId}", produces = "application/json")
public ResponseEntity<BasicRecord> getProject(
    @PathVariable("externalProjectId") String externalProjectId,
    @RequestHeader(name = "If-None-Match", required = false) RecordEtag ifNoneMatchEtag) {
  Long modifiedId = service.getModifiedId(externalProjectId);

  if (ifNoneMatchEtag != null && ifNoneMatchEtag.getValue() == modifiedId) {
    return ResponseEntity.status(HttpStatus.NOT_MODIFIED)
        .eTag(ifNoneMatchEtag.toString()).build();
  }

  BasicRecord project = service.getProject(externalProjectId);
  return ResponseEntity.ok().eTag(new RecordEtag(modifiedId).toString()).body(project);
}

(Yes, I know the ETag comparison is wrong)

where

public class RecordEtag {

  private long value;

  public RecordEtag(String etag) {
    Validate.notEmpty(etag, "ETag cannot be null/empty");

    if (etag.startsWith("W/"))
      throw new IllegalArgumentException("weak ETags are not supported");

    if (!(etag.startsWith("\"") && etag.endsWith("\"")))
      throw new IllegalArgumentException("ETag must be enclosed in double qoutes");

    String unwrapped = StringUtils.substringBetween(etag, "\"");

    try {
      this.value = Long.parseLong(unwrapped, 16);
    } catch (NumberFormatException e) {
      throw new IllegalArgumentException("ETag is not a hex string");
    }
  }

  public RecordEtag(long value) {
    this.value = value;
  }

  public long getValue() {
    return value;
  }

  @Override
  public String toString() {
    return "\"" + Long.toHexString(value) + "\"";
  }

}

What I am completely missing in Spring Web is to assist the developer to avoid boilerplate code around ETag comparison. I'd expect some comparator:

@GetMapping(path = "/{externalProjectId}", produces = "application/json")
public ResponseEntity<BasicRecord> getProject(
    @PathVariable("externalProjectId") String externalProjectId,
    @RequestHeader(name = "If-None-Match", required = false) ETagComparator ifNoneMatchEtag) {
  Long modifiedId = service.getModifiedId(externalProjectId);

  RecordEtag recordEtag = new RecordEtag(modifiedId);
  if (ifNoneMatchEtag != null && ifNoneMatchEtag.matches(/* optional */ ETagComparator.WEAK_COMPARISION, recordEtag.toString()) {
  //                                                                                                     ^^^^^^^^^^ can also be null if resource doesn't exist
  return ResponseEntity.status(HttpStatus.NOT_MODIFIED)
        .eTag(recordEtag.toString()).build();
  }

  BasicRecord project = service.getProject(externalProjectId);
  return ResponseEntity.ok().eTag(recordEtag,toString()).body(project);
}

Basic idea: * ETagComparator parses the value of the supplied header along with *, a possible list and weak/strong boolean * Ideally, when used from @RequestHeader and the header is wellknown (RFC 7232), the comparison function is already set * @RequestHeader will inject null when value is not provided * ETagComparator can handle null input from the client indicating that the target resource does not exist or by some other means fulfulling this * Match the the supplied value from controller method

The semantic behavior of If-Match/If-None-Match is welldefined. In a case of a custom header, like in my case If-Parent-Match which has If-Match semantic behavior I must be able to configure this semantic behavior also.

Comment From: poutsma

Perhaps WebRequest.checkNotModified is what you are looking for? The reference documentation has more.

Comment From: michael-o

This one looks promising. I need to take a deep look at it tomorrow.

Comment From: rstoyanchev

Simply setting an eTag and/or lastModified on ResponseEntity will perform the resource modified checks and respond with not modified if necessary.

Comment From: rstoyanchev

Closing but feel free to comment.

Comment From: michael-o

Perhaps WebRequest.checkNotModified is what you are looking for? The reference documentation has more.

I so tried the WebRequest#checkNotModified(String) method, thanks for pointing out. There are a few issues though:

  • My If-Match is not covered at all for PUT/PATCH
  • Case * does not correspond to the RFC. It says:
   If the field-value is "*", the condition is false if the origin
   server has a current representation for the target resource. 

From Swagger UI I have provided * to the header and in code the Etag "f535b" was passed, but still the method returns false. I don't see a 304 issued. So the condition is evaluted to true, opposed to the RFC.

Though, I will drop my GET code in favor of this already.

Simply setting an eTag and/or lastModified on ResponseEntity will perform the resource modified checks and respond with not modified if necessary.

This doesn't sound good to me because I still have to perform the operation. The RFC clearly says that I must not perform it is if the condition is not met. The WebRequest merely covers GET/HEAD.

Comment From: michael-o

@rstoyanchev, I don't consider this issue to be invalid. Compared to JAX-RS' support for conditional requests Spring's is very humble.

Comment From: bclozel

Superseded by #24881