Add weak ETag support in ShallowEtagHeaderFilter
This commit adds weak ETag support in ShallowEtagHeaderFilter. This improves the behavior of the filter in tow ways: * weak ETags in request headers such as `W/"0badc0ffee"` will be compared with a "weak comparison" (matching both weak and strong ETags of the same value) * when enabled with the "writeWeakETag" init param, the filter will write weak Etags in its HTTP responses Issue: SPR-13778
This commit is contained in:
@@ -19,6 +19,7 @@ package org.springframework.web.filter;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.PrintWriter;
|
||||
|
||||
import javax.servlet.FilterChain;
|
||||
import javax.servlet.ServletException;
|
||||
import javax.servlet.ServletOutputStream;
|
||||
@@ -60,11 +61,28 @@ public class ShallowEtagHeaderFilter extends OncePerRequestFilter {
|
||||
|
||||
private static final String STREAMING_ATTRIBUTE = ShallowEtagHeaderFilter.class.getName() + ".STREAMING";
|
||||
|
||||
|
||||
/** Checking for Servlet 3.0+ HttpServletResponse.getHeader(String) */
|
||||
private static final boolean servlet3Present =
|
||||
ClassUtils.hasMethod(HttpServletResponse.class, "getHeader", String.class);
|
||||
|
||||
private boolean writeWeakETag = false;
|
||||
|
||||
/**
|
||||
* Set whether the ETag value written to the response should be weak, as per rfc7232.
|
||||
* <p>Should be configured using an {@code <init-param>} for parameter name
|
||||
* "writeWeakETag" in the filter definition in {@code web.xml}.
|
||||
* @see <a href="https://tools.ietf.org/html/rfc7232#section-2.3">rfc7232 section-2.3</a>
|
||||
*/
|
||||
public boolean isWriteWeakETag() {
|
||||
return writeWeakETag;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return whether the ETag value written to the response should be weak, as per rfc7232.
|
||||
*/
|
||||
public void setWriteWeakETag(boolean writeWeakETag) {
|
||||
this.writeWeakETag = writeWeakETag;
|
||||
}
|
||||
|
||||
/**
|
||||
* The default value is "false" so that the filter may delay the generation of
|
||||
@@ -102,10 +120,13 @@ public class ShallowEtagHeaderFilter extends OncePerRequestFilter {
|
||||
responseWrapper.copyBodyToResponse();
|
||||
}
|
||||
else if (isEligibleForEtag(request, responseWrapper, statusCode, responseWrapper.getContentInputStream())) {
|
||||
String responseETag = generateETagHeaderValue(responseWrapper.getContentInputStream());
|
||||
String responseETag = generateETagHeaderValue(responseWrapper.getContentInputStream(), this.writeWeakETag);
|
||||
rawResponse.setHeader(HEADER_ETAG, responseETag);
|
||||
String requestETag = request.getHeader(HEADER_IF_NONE_MATCH);
|
||||
if (responseETag.equals(requestETag)) {
|
||||
if (requestETag != null
|
||||
&& (responseETag.equals(requestETag)
|
||||
|| responseETag.replaceFirst("^W/", "").equals(requestETag.replaceFirst("^W/", ""))
|
||||
|| "*".equals(requestETag))) {
|
||||
if (logger.isTraceEnabled()) {
|
||||
logger.trace("ETag [" + responseETag + "] equal to If-None-Match, sending 304");
|
||||
}
|
||||
@@ -163,11 +184,17 @@ public class ShallowEtagHeaderFilter extends OncePerRequestFilter {
|
||||
* Generate the ETag header value from the given response body byte array.
|
||||
* <p>The default implementation generates an MD5 hash.
|
||||
* @param inputStream the response body as an InputStream
|
||||
* @param isWeak whether the generated ETag should be weak
|
||||
* @return the ETag header value
|
||||
* @see org.springframework.util.DigestUtils
|
||||
*/
|
||||
protected String generateETagHeaderValue(InputStream inputStream) throws IOException {
|
||||
StringBuilder builder = new StringBuilder("\"0");
|
||||
protected String generateETagHeaderValue(InputStream inputStream, boolean isWeak) throws IOException {
|
||||
// length of W/ + 0 + " + 32bits md5 hash + "
|
||||
StringBuilder builder = new StringBuilder(37);
|
||||
if (isWeak) {
|
||||
builder.append("W/");
|
||||
}
|
||||
builder.append("\"0");
|
||||
DigestUtils.appendMd5DigestAsHex(inputStream, builder);
|
||||
builder.append('"');
|
||||
return builder.toString();
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2015 the original author or authors.
|
||||
* Copyright 2002-2016 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -73,6 +73,26 @@ public class ShallowEtagHeaderFilterTests {
|
||||
assertArrayEquals("Invalid content", responseBody, response.getContentAsByteArray());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void filterNoMatchWeakETag() throws Exception {
|
||||
this.filter.setWriteWeakETag(true);
|
||||
final MockHttpServletRequest request = new MockHttpServletRequest("GET", "/hotels");
|
||||
MockHttpServletResponse response = new MockHttpServletResponse();
|
||||
|
||||
final byte[] responseBody = "Hello World".getBytes("UTF-8");
|
||||
FilterChain filterChain = (filterRequest, filterResponse) -> {
|
||||
assertEquals("Invalid request passed", request, filterRequest);
|
||||
((HttpServletResponse) filterResponse).setStatus(HttpServletResponse.SC_OK);
|
||||
FileCopyUtils.copy(responseBody, filterResponse.getOutputStream());
|
||||
};
|
||||
filter.doFilter(request, response, filterChain);
|
||||
|
||||
assertEquals("Invalid status", 200, response.getStatus());
|
||||
assertEquals("Invalid ETag header", "W/\"0b10a8db164e0754105b7a99be72e3fe5\"", response.getHeader("ETag"));
|
||||
assertTrue("Invalid Content-Length header", response.getContentLength() > 0);
|
||||
assertArrayEquals("Invalid content", responseBody, response.getContentAsByteArray());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void filterMatch() throws Exception {
|
||||
final MockHttpServletRequest request = new MockHttpServletRequest("GET", "/hotels");
|
||||
@@ -94,6 +114,27 @@ public class ShallowEtagHeaderFilterTests {
|
||||
assertArrayEquals("Invalid content", new byte[0], response.getContentAsByteArray());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void filterMatchWeakEtag() throws Exception {
|
||||
final MockHttpServletRequest request = new MockHttpServletRequest("GET", "/hotels");
|
||||
String etag = "\"0b10a8db164e0754105b7a99be72e3fe5\"";
|
||||
request.addHeader("If-None-Match", "W/" + etag);
|
||||
MockHttpServletResponse response = new MockHttpServletResponse();
|
||||
|
||||
FilterChain filterChain = (filterRequest, filterResponse) -> {
|
||||
assertEquals("Invalid request passed", request, filterRequest);
|
||||
byte[] responseBody = "Hello World".getBytes("UTF-8");
|
||||
FileCopyUtils.copy(responseBody, filterResponse.getOutputStream());
|
||||
filterResponse.setContentLength(responseBody.length);
|
||||
};
|
||||
filter.doFilter(request, response, filterChain);
|
||||
|
||||
assertEquals("Invalid status", 304, response.getStatus());
|
||||
assertEquals("Invalid ETag header", "\"0b10a8db164e0754105b7a99be72e3fe5\"", response.getHeader("ETag"));
|
||||
assertFalse("Response has Content-Length header", response.containsHeader("Content-Length"));
|
||||
assertArrayEquals("Invalid content", new byte[0], response.getContentAsByteArray());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void filterWriter() throws Exception {
|
||||
final MockHttpServletRequest request = new MockHttpServletRequest("GET", "/hotels");
|
||||
|
||||
@@ -4302,7 +4302,6 @@ responsible for this, along with conditional headers such as `'Last-Modified'` a
|
||||
The `'Cache-Control'` HTTP response header advises private caches (e.g. browsers) and
|
||||
public caches (e.g. proxies) on how they can cache HTTP responses for further reuse.
|
||||
|
||||
mvc-config-static-resources
|
||||
An http://en.wikipedia.org/wiki/HTTP_ETag[ETag] (entity tag) is an HTTP response header
|
||||
returned by an HTTP/1.1 compliant web server used to determine change in content at a
|
||||
given URL. It can be considered to be the more sophisticated successor to the
|
||||
@@ -4473,14 +4472,15 @@ ETags, more about that later).The filter caches the content of the rendered JSP
|
||||
other content), generates an MD5 hash over that, and returns that as an ETag header in
|
||||
the response. The next time a client sends a request for the same resource, it uses that
|
||||
hash as the `If-None-Match` value. The filter detects this, renders the view again, and
|
||||
compares the two hashes. If they are equal, a `304` is returned. This filter will not
|
||||
save processing power, as the view is still rendered. The only thing it saves is
|
||||
bandwidth, as the rendered response is not sent back over the wire.
|
||||
compares the two hashes. If they are equal, a `304` is returned.
|
||||
|
||||
Note that this strategy saves network bandwidth but not CPU, as the full response must be
|
||||
computed for each request. Other strategies at the controller level (described above) can
|
||||
save network bandwidth and avoid computation.
|
||||
mvc-config-static-resources
|
||||
|
||||
This filter has a `writeWeakETag` parameter that configures the filter to write Weak ETags,
|
||||
like this: `W/"02a2d595e6ed9a0b24f027f2b63b134d6"`, as defined in
|
||||
https://tools.ietf.org/html/rfc7232#section-2.3[RFC 7232 Section 2.3].
|
||||
|
||||
You configure the `ShallowEtagHeaderFilter` in `web.xml`:
|
||||
|
||||
@@ -4490,6 +4490,12 @@ You configure the `ShallowEtagHeaderFilter` in `web.xml`:
|
||||
<filter>
|
||||
<filter-name>etagFilter</filter-name>
|
||||
<filter-class>org.springframework.web.filter.ShallowEtagHeaderFilter</filter-class>
|
||||
<!-- Optional parameter that configures the filter to write weak ETags
|
||||
<init-param>
|
||||
<param-name>writeWeakETag</param-name>
|
||||
<param-value>true</param-value>
|
||||
</init-param>
|
||||
-->
|
||||
</filter>
|
||||
|
||||
<filter-mapping>
|
||||
|
||||
Reference in New Issue
Block a user