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 forPUT
/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