-
Notifications
You must be signed in to change notification settings - Fork 1.9k
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
Jetty 12 : precompressed content support for ResourceService
#8595
Jetty 12 : precompressed content support for ResourceService
#8595
Conversation
Protect bad usages of CachingContentFactory
* @param suffix the suffix for the ETag | ||
* @return the weak etag | ||
*/ | ||
public static String calcWeakEtag(Path path, String suffix) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Shouldn't this method accept a Resource
instead of a Path
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The weak etag is calculated from the path name + last modified timestamp.
Not all resources have them (eg: MemoryResource has neither).
Can't use a ResourceCollection here either, as the path name and last modified timestamp won't always represent the same path object.
It's better to use Path, as even with ResourceCollection in the mix, we'll get a Path then use it.
This is the same Path that is used by the HttpContent to serve the same content.
BTW, Don't get hung up on MemoryResource, the HttpContentFactory reorg PR coming after this will make the case to remove MemoryResource entirely.
* @param path the path to calculate from | ||
* @return the weak etag | ||
*/ | ||
public static String calcWeakEtag(Path path) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Shouldn't this method accept a Resource
instead of a Path
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Different method supports Resource
now, this method still exists.
@@ -119,13 +119,13 @@ public HttpField getETag() | |||
@Override | |||
public String getETagValue() | |||
{ | |||
return _resource.getWeakETag(); | |||
return EtagUtil.calcWeakEtag(_resource.getPath()); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This would only work with path-backed resources. Shouldn't we be capable of calculating the etag of a potential MemoryResource
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed in this PR.
@@ -405,7 +406,7 @@ public class CachedHttpContent implements HttpContent | |||
|
|||
_lastAccessed = Instant.now(); | |||
|
|||
_etag = CachedContentFactory.this._etags ? new PreEncodedHttpField(HttpHeader.ETAG, resource.getWeakETag()) : null; | |||
_etag = CachedContentFactory.this._etags ? new PreEncodedHttpField(HttpHeader.ETAG, EtagUtil.calcWeakEtag(resource.getPath())) : null; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This would only work with path-backed resources. Shouldn't we be capable of calculating the etag of a potential MemoryResource
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There's no ETag support for MemoryResource as it exists currently.
The HttpContentFactory reorg PR (after this one) will remove MemoryResource in favor of a StaticHttpContent object fallback instead (which can hold the etag based on the URI and JAR file last timestamp)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed in this PR anyway.
assertThat(response, containsHeaderValue(HttpHeader.VARY, "Accept-Encoding")); | ||
assertThat(response, containsHeaderValue(HttpHeader.CONTENT_ENCODING, "br")); | ||
String body = response.getContent(); | ||
assertThat(body, containsString("fake br")); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why only fake br
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The contents don't actually have to be compressed to satisfy the test.
The ResourceService doesn't compress it anyway, it just serves the content as-is from the Resource Base.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
But it confuses the reader the fact that you test for fake br
only, while at the beginning of the test the whole content is fake brotli
.
I would test for the whole content, since it's so small, to avoid such scratching head moment from other readers of this test ("why only fake br
"?).
@@ -1892,35 +1912,6 @@ public void testDirectoryOfCollections() throws Exception | |||
assertThat(response.getStatus(), is(HttpStatus.OK_200)); | |||
} | |||
|
|||
@Test | |||
public void testEtagIfMatchAlwaysFailsDueToWeakEtag() throws Exception |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why did you remove this test?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's a bad/duplicate test.
There are other tests that test the same condition.
testIfEtag() for example.
And those expect a 200 OK, not a 412 precondition failure for the same request.
The origin of this test is unknown.
It is not present in jetty-11.0.x.
I can trace as far back as 04acdb7 but not before then.
It seems to have been created during the hackathon.
But etags and preconditions were not working back then.
Interestingly, the github search also shows nothing for this test case...
https://github.com/eclipse/jetty.project/search?q=testEtagIfMatchAlwaysFailsDueToWeakEtag&type=code
@@ -51,20 +51,19 @@ public HttpField getETag() | |||
@Override | |||
public String getETagValue() | |||
{ | |||
//return _content.getResource().getWeakETag(_format.getEtagSuffix()); | |||
return null; | |||
return EtagUtil.calcWeakEtag(_content.getResource().getPath(), _format.getEtagSuffix()); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This would only work with path-backed resources. Shouldn't we be capable of calculating the etag of a potential MemoryResource
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixing etag support
@@ -403,7 +404,7 @@ public class CachedHttpContent implements HttpContent | |||
|
|||
_lastAccessed = Instant.now(); | |||
|
|||
_etag = CachedContentFactory.this._etags ? new PreEncodedHttpField(HttpHeader.ETAG, resource.getWeakETag()) : null; | |||
_etag = CachedContentFactory.this._etags ? new PreEncodedHttpField(HttpHeader.ETAG, EtagUtil.calcWeakEtag(resource.getPath())) : null; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This would only work with path-backed resources. Shouldn't we be capable of calculating the etag of a potential MemoryResource
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixing.
@@ -603,7 +604,7 @@ public class CachedPrecompressedHttpContent extends PrecompressedHttpContent | |||
_content = content; | |||
_precompressedContent = precompressedContent; | |||
|
|||
_etag = (CachedContentFactory.this._etags) ? new PreEncodedHttpField(HttpHeader.ETAG, _content.getResource().getWeakETag(format.getEtagSuffix())) : null; | |||
_etag = (CachedContentFactory.this._etags) ? new PreEncodedHttpField(HttpHeader.ETAG, EtagUtil.calcWeakEtag(_content.getResource().getPath(), format.getEtagSuffix())) : null; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This would only work with path-backed resources. Shouldn't we be capable of calculating the etag of a potential MemoryResource
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixing.
…precompressed-support-resource-service
FileTime lastModifiedTime = Files.getLastModifiedTime(_delegate.getResource().getPath()); | ||
if (lastModifiedTime.equals(_lastModifiedValue)) | ||
if (!_delegate.getResource().exists()) | ||
return false; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Aren't these redundant checks now that the constructor checks them?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done
jetty-core/jetty-http/src/main/java/org/eclipse/jetty/http/CompressedContentFormat.java
Show resolved
Hide resolved
/** | ||
* Utility classes for Etag behaviors | ||
*/ | ||
public class EtagUtil |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Contains more than one utility, so I'd prefer EtagUtils
.
Also, add a private
empty constructor so it cannot be instantiated.
* @param suffix the suffix for the ETag | ||
* @return the weak etag | ||
*/ | ||
public static String calcWeakEtag(Path path, String suffix) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Prefix calc
implies calculations, which to me implies math operations only.
I think something like compute
or resolve
instead of calc
is more appropriate.
continue; // skip, compressed resource modified before actual resource | ||
if (compressedResource.length() >= resource.length()) | ||
continue; // skip, not smaller than actual resource | ||
// add found precompressed format |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Remove the comments, they are just describing what the code does, and the code is very clear.
Less chances that the comments rot.
if (suffix != null) | ||
b.append(suffix); | ||
b.append('"'); | ||
return b.toString(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is there a reference for this algorithm? Is it defined by some spec? If so, please add a reference to it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The Etag is just a string.
The bits and pieces we use are just our take on how to create that string.
I'll add the link to the RFC9110 section in the top level javadoc.
* @param suffix the suffix for the ETag | ||
* @return the weak etag | ||
*/ | ||
public static String calcWeakEtag(Path path, String suffix) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The original implementation in Resource
was not using Path
at all, so I have the same concerns: this method should take a Resource
, not a Path
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The Resource
version didn't handle non-path/file-system based Resources properly.
Btw, there is a Resource
method in this class for working with Resource
to HttpField
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There's also the problem when working with a ResourceCollection
there is a difference in .getPath()
and .getLastModified()
which doesn't match the Resource actually being served in the HttpContent
assertThat(response, containsHeaderValue(HttpHeader.VARY, "Accept-Encoding")); | ||
assertThat(response, containsHeaderValue(HttpHeader.CONTENT_ENCODING, "br")); | ||
String body = response.getContent(); | ||
assertThat(body, containsString("fake br")); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
But it confuses the reader the fact that you test for fake br
only, while at the beginning of the test the whole content is fake brotli
.
I would test for the whole content, since it's so small, to avoid such scratching head moment from other readers of this test ("why only fake br
"?).
* @return True if the tags are equal. | ||
* @see <a href="https://www.rfc-editor.org/rfc/rfc9110#section-8.8.3.2">RFC9110: Section 8.8.3.2 - Etag Comparison</a>. | ||
*/ | ||
public static boolean match(String etag, String etagWithOptionalSuffix) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would call this method matches
because it returns a boolean.
It's kind of a query/question rather than a command.
Signed-off-by: Joakim Erdfelt <[email protected]>
{ | ||
_lastAccessed = NanoTime.now(); | ||
return true; | ||
FileTime lastModifiedTime = Files.getLastModifiedTime(_delegate.getResource().getPath()); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Since this makes caching only work with path-backed resources, shouldn't there be a check in the constructor to prevent accepting those?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good point, let me fix this code block.
+ Using Resource.lastModified were possible + CachingHttpContent has better Resource protection, and uses Etag from delegate + CachingHttpContent uses delegate where possible. + Store Etag HttpField in CachingHttpContent and allow override in constructor
Reenable test cases for precompressed content.
Fix implementation to support precompressed content.