Skip to content

Add generic ETag comparator #24868

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
michael-o opened this issue Apr 6, 2020 · 7 comments
Closed

Add generic ETag comparator #24868

michael-o opened this issue Apr 6, 2020 · 7 comments
Labels
in: web Issues in web modules (web, webmvc, webflux, websocket) status: superseded An issue that has been superseded by another

Comments

@michael-o
Copy link

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.

@spring-projects-issues spring-projects-issues added the status: waiting-for-triage An issue we've not yet triaged or decided on label Apr 6, 2020
@poutsma
Copy link
Contributor

poutsma commented Apr 6, 2020

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

@poutsma poutsma added the status: waiting-for-feedback We need additional information before we can continue label Apr 6, 2020
@michael-o
Copy link
Author

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

@spring-projects-issues spring-projects-issues added status: feedback-provided Feedback has been provided and removed status: waiting-for-feedback We need additional information before we can continue labels Apr 6, 2020
@rstoyanchev
Copy link
Contributor

rstoyanchev commented Apr 7, 2020

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

@rstoyanchev rstoyanchev self-assigned this Apr 7, 2020
@rstoyanchev rstoyanchev added status: invalid An issue that we don't feel is valid and removed status: feedback-provided Feedback has been provided status: waiting-for-triage An issue we've not yet triaged or decided on labels Apr 7, 2020
@rstoyanchev
Copy link
Contributor

Closing but feel free to comment.

@michael-o
Copy link
Author

michael-o commented Apr 7, 2020

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.

@bclozel bclozel changed the title Add generic ETag comprator Add generic ETag comparator Apr 7, 2020
@michael-o
Copy link
Author

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

@rstoyanchev rstoyanchev removed their assignment Apr 22, 2020
@rstoyanchev rstoyanchev added status: feedback-provided Feedback has been provided status: waiting-for-triage An issue we've not yet triaged or decided on and removed status: invalid An issue that we don't feel is valid labels Apr 22, 2020
@rstoyanchev rstoyanchev added the in: web Issues in web modules (web, webmvc, webflux, websocket) label Nov 10, 2021
@bclozel
Copy link
Member

bclozel commented Jul 17, 2023

Superseded by #24881

@bclozel bclozel closed this as not planned Won't fix, can't repro, duplicate, stale Jul 17, 2023
@bclozel bclozel added status: superseded An issue that has been superseded by another and removed status: waiting-for-triage An issue we've not yet triaged or decided on status: feedback-provided Feedback has been provided labels Jul 17, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
in: web Issues in web modules (web, webmvc, webflux, websocket) status: superseded An issue that has been superseded by another
Projects
None yet
Development

No branches or pull requests

5 participants