Browse Source

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
pull/970/head
Brian Clozel 9 years ago
parent
commit
1c2ac49f2a
  1. 37
      spring-web/src/main/java/org/springframework/web/filter/ShallowEtagHeaderFilter.java
  2. 43
      spring-web/src/test/java/org/springframework/web/filter/ShallowEtagHeaderFilterTests.java
  3. 16
      src/asciidoc/web-mvc.adoc

37
spring-web/src/main/java/org/springframework/web/filter/ShallowEtagHeaderFilter.java

@ -19,6 +19,7 @@ package org.springframework.web.filter; @@ -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 { @@ -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 { @@ -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 { @@ -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();

43
spring-web/src/test/java/org/springframework/web/filter/ShallowEtagHeaderFilterTests.java

@ -1,5 +1,5 @@ @@ -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 { @@ -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 { @@ -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");

16
src/asciidoc/web-mvc.adoc

@ -4302,7 +4302,6 @@ responsible for this, along with conditional headers such as `'Last-Modified'` a @@ -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 @@ -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`: @@ -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>

Loading…
Cancel
Save