Skip to content

Commit 1c2ac49

Browse files
committed
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
1 parent 9235345 commit 1c2ac49

File tree

3 files changed

+85
-11
lines changed

3 files changed

+85
-11
lines changed

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

Lines changed: 32 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
import java.io.IOException;
2020
import java.io.InputStream;
2121
import java.io.PrintWriter;
22+
2223
import javax.servlet.FilterChain;
2324
import javax.servlet.ServletException;
2425
import javax.servlet.ServletOutputStream;
@@ -60,11 +61,28 @@ public class ShallowEtagHeaderFilter extends OncePerRequestFilter {
6061

6162
private static final String STREAMING_ATTRIBUTE = ShallowEtagHeaderFilter.class.getName() + ".STREAMING";
6263

63-
6464
/** Checking for Servlet 3.0+ HttpServletResponse.getHeader(String) */
6565
private static final boolean servlet3Present =
6666
ClassUtils.hasMethod(HttpServletResponse.class, "getHeader", String.class);
6767

68+
private boolean writeWeakETag = false;
69+
70+
/**
71+
* Set whether the ETag value written to the response should be weak, as per rfc7232.
72+
* <p>Should be configured using an {@code <init-param>} for parameter name
73+
* "writeWeakETag" in the filter definition in {@code web.xml}.
74+
* @see <a href="https://tools.ietf.org/html/rfc7232#section-2.3">rfc7232 section-2.3</a>
75+
*/
76+
public boolean isWriteWeakETag() {
77+
return writeWeakETag;
78+
}
79+
80+
/**
81+
* Return whether the ETag value written to the response should be weak, as per rfc7232.
82+
*/
83+
public void setWriteWeakETag(boolean writeWeakETag) {
84+
this.writeWeakETag = writeWeakETag;
85+
}
6886

6987
/**
7088
* The default value is "false" so that the filter may delay the generation of
@@ -102,10 +120,13 @@ private void updateResponse(HttpServletRequest request, HttpServletResponse resp
102120
responseWrapper.copyBodyToResponse();
103121
}
104122
else if (isEligibleForEtag(request, responseWrapper, statusCode, responseWrapper.getContentInputStream())) {
105-
String responseETag = generateETagHeaderValue(responseWrapper.getContentInputStream());
123+
String responseETag = generateETagHeaderValue(responseWrapper.getContentInputStream(), this.writeWeakETag);
106124
rawResponse.setHeader(HEADER_ETAG, responseETag);
107125
String requestETag = request.getHeader(HEADER_IF_NONE_MATCH);
108-
if (responseETag.equals(requestETag)) {
126+
if (requestETag != null
127+
&& (responseETag.equals(requestETag)
128+
|| responseETag.replaceFirst("^W/", "").equals(requestETag.replaceFirst("^W/", ""))
129+
|| "*".equals(requestETag))) {
109130
if (logger.isTraceEnabled()) {
110131
logger.trace("ETag [" + responseETag + "] equal to If-None-Match, sending 304");
111132
}
@@ -163,11 +184,17 @@ protected boolean isEligibleForEtag(HttpServletRequest request, HttpServletRespo
163184
* Generate the ETag header value from the given response body byte array.
164185
* <p>The default implementation generates an MD5 hash.
165186
* @param inputStream the response body as an InputStream
187+
* @param isWeak whether the generated ETag should be weak
166188
* @return the ETag header value
167189
* @see org.springframework.util.DigestUtils
168190
*/
169-
protected String generateETagHeaderValue(InputStream inputStream) throws IOException {
170-
StringBuilder builder = new StringBuilder("\"0");
191+
protected String generateETagHeaderValue(InputStream inputStream, boolean isWeak) throws IOException {
192+
// length of W/ + 0 + " + 32bits md5 hash + "
193+
StringBuilder builder = new StringBuilder(37);
194+
if (isWeak) {
195+
builder.append("W/");
196+
}
197+
builder.append("\"0");
171198
DigestUtils.appendMd5DigestAsHex(inputStream, builder);
172199
builder.append('"');
173200
return builder.toString();

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

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2015 the original author or authors.
2+
* Copyright 2002-2016 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -73,6 +73,26 @@ public void filterNoMatch() throws Exception {
7373
assertArrayEquals("Invalid content", responseBody, response.getContentAsByteArray());
7474
}
7575

76+
@Test
77+
public void filterNoMatchWeakETag() throws Exception {
78+
this.filter.setWriteWeakETag(true);
79+
final MockHttpServletRequest request = new MockHttpServletRequest("GET", "/hotels");
80+
MockHttpServletResponse response = new MockHttpServletResponse();
81+
82+
final byte[] responseBody = "Hello World".getBytes("UTF-8");
83+
FilterChain filterChain = (filterRequest, filterResponse) -> {
84+
assertEquals("Invalid request passed", request, filterRequest);
85+
((HttpServletResponse) filterResponse).setStatus(HttpServletResponse.SC_OK);
86+
FileCopyUtils.copy(responseBody, filterResponse.getOutputStream());
87+
};
88+
filter.doFilter(request, response, filterChain);
89+
90+
assertEquals("Invalid status", 200, response.getStatus());
91+
assertEquals("Invalid ETag header", "W/\"0b10a8db164e0754105b7a99be72e3fe5\"", response.getHeader("ETag"));
92+
assertTrue("Invalid Content-Length header", response.getContentLength() > 0);
93+
assertArrayEquals("Invalid content", responseBody, response.getContentAsByteArray());
94+
}
95+
7696
@Test
7797
public void filterMatch() throws Exception {
7898
final MockHttpServletRequest request = new MockHttpServletRequest("GET", "/hotels");
@@ -94,6 +114,27 @@ public void filterMatch() throws Exception {
94114
assertArrayEquals("Invalid content", new byte[0], response.getContentAsByteArray());
95115
}
96116

117+
@Test
118+
public void filterMatchWeakEtag() throws Exception {
119+
final MockHttpServletRequest request = new MockHttpServletRequest("GET", "/hotels");
120+
String etag = "\"0b10a8db164e0754105b7a99be72e3fe5\"";
121+
request.addHeader("If-None-Match", "W/" + etag);
122+
MockHttpServletResponse response = new MockHttpServletResponse();
123+
124+
FilterChain filterChain = (filterRequest, filterResponse) -> {
125+
assertEquals("Invalid request passed", request, filterRequest);
126+
byte[] responseBody = "Hello World".getBytes("UTF-8");
127+
FileCopyUtils.copy(responseBody, filterResponse.getOutputStream());
128+
filterResponse.setContentLength(responseBody.length);
129+
};
130+
filter.doFilter(request, response, filterChain);
131+
132+
assertEquals("Invalid status", 304, response.getStatus());
133+
assertEquals("Invalid ETag header", "\"0b10a8db164e0754105b7a99be72e3fe5\"", response.getHeader("ETag"));
134+
assertFalse("Response has Content-Length header", response.containsHeader("Content-Length"));
135+
assertArrayEquals("Invalid content", new byte[0], response.getContentAsByteArray());
136+
}
137+
97138
@Test
98139
public void filterWriter() throws Exception {
99140
final MockHttpServletRequest request = new MockHttpServletRequest("GET", "/hotels");

src/asciidoc/web-mvc.adoc

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4302,7 +4302,6 @@ responsible for this, along with conditional headers such as `'Last-Modified'` a
43024302
The `'Cache-Control'` HTTP response header advises private caches (e.g. browsers) and
43034303
public caches (e.g. proxies) on how they can cache HTTP responses for further reuse.
43044304

4305-
mvc-config-static-resources
43064305
An http://en.wikipedia.org/wiki/HTTP_ETag[ETag] (entity tag) is an HTTP response header
43074306
returned by an HTTP/1.1 compliant web server used to determine change in content at a
43084307
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
44734472
other content), generates an MD5 hash over that, and returns that as an ETag header in
44744473
the response. The next time a client sends a request for the same resource, it uses that
44754474
hash as the `If-None-Match` value. The filter detects this, renders the view again, and
4476-
compares the two hashes. If they are equal, a `304` is returned. This filter will not
4477-
save processing power, as the view is still rendered. The only thing it saves is
4478-
bandwidth, as the rendered response is not sent back over the wire.
4475+
compares the two hashes. If they are equal, a `304` is returned.
44794476

44804477
Note that this strategy saves network bandwidth but not CPU, as the full response must be
44814478
computed for each request. Other strategies at the controller level (described above) can
44824479
save network bandwidth and avoid computation.
4483-
mvc-config-static-resources
4480+
4481+
This filter has a `writeWeakETag` parameter that configures the filter to write Weak ETags,
4482+
like this: `W/"02a2d595e6ed9a0b24f027f2b63b134d6"`, as defined in
4483+
https://tools.ietf.org/html/rfc7232#section-2.3[RFC 7232 Section 2.3].
44844484

44854485
You configure the `ShallowEtagHeaderFilter` in `web.xml`:
44864486

@@ -4490,6 +4490,12 @@ You configure the `ShallowEtagHeaderFilter` in `web.xml`:
44904490
<filter>
44914491
<filter-name>etagFilter</filter-name>
44924492
<filter-class>org.springframework.web.filter.ShallowEtagHeaderFilter</filter-class>
4493+
<!-- Optional parameter that configures the filter to write weak ETags
4494+
<init-param>
4495+
<param-name>writeWeakETag</param-name>
4496+
<param-value>true</param-value>
4497+
</init-param>
4498+
-->
44934499
</filter>
44944500
44954501
<filter-mapping>

0 commit comments

Comments
 (0)