From 3f392e32f56993e4cf92e5c61ed227b80fa10b82 Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Tue, 22 Apr 2014 23:14:48 +0200 Subject: [PATCH] ShallowEtagHeaderFilter writes body early on sendError/sendRedirect and interprets setContentLength/setBufferSize as a hint for capacity increase Issue: SPR-11705 Issue: SPR-11717 --- .../web/filter/ShallowEtagHeaderFilter.java | 75 ++++++++++++----- .../filter/ShallowEtagHeaderFilterTests.java | 84 +++++++++++++++++-- 2 files changed, 129 insertions(+), 30 deletions(-) diff --git a/spring-web/src/main/java/org/springframework/web/filter/ShallowEtagHeaderFilter.java b/spring-web/src/main/java/org/springframework/web/filter/ShallowEtagHeaderFilter.java index cfe71f0a7cf..9e8c4b067b1 100644 --- a/spring-web/src/main/java/org/springframework/web/filter/ShallowEtagHeaderFilter.java +++ b/spring-web/src/main/java/org/springframework/web/filter/ShallowEtagHeaderFilter.java @@ -72,14 +72,15 @@ public class ShallowEtagHeaderFilter extends OncePerRequestFilter { protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { + HttpServletResponse responseToUse = response; if (!isAsyncDispatch(request)) { - response = new ShallowEtagResponseWrapper(response); + responseToUse = new ShallowEtagResponseWrapper(response); } - filterChain.doFilter(request, response); + filterChain.doFilter(request, responseToUse); if (!isAsyncStarted(request)) { - updateResponse(request, response); + updateResponse(request, responseToUse); } } @@ -88,41 +89,44 @@ public class ShallowEtagHeaderFilter extends OncePerRequestFilter { WebUtils.getNativeResponse(response, ShallowEtagResponseWrapper.class); Assert.notNull(responseWrapper, "ShallowEtagResponseWrapper not found"); - response = (HttpServletResponse) responseWrapper.getResponse(); - byte[] body = responseWrapper.toByteArray(); + HttpServletResponse rawResponse = (HttpServletResponse) responseWrapper.getResponse(); int statusCode = responseWrapper.getStatusCode(); + byte[] body = responseWrapper.toByteArray(); - if (isEligibleForEtag(request, responseWrapper, statusCode, body)) { + if (rawResponse.isCommitted()) { + if (body.length > 0) { + StreamUtils.copy(body, rawResponse.getOutputStream()); + } + } + else if (isEligibleForEtag(request, responseWrapper, statusCode, body)) { String responseETag = generateETagHeaderValue(body); - response.setHeader(HEADER_ETAG, responseETag); - + rawResponse.setHeader(HEADER_ETAG, responseETag); String requestETag = request.getHeader(HEADER_IF_NONE_MATCH); if (responseETag.equals(requestETag)) { if (logger.isTraceEnabled()) { logger.trace("ETag [" + responseETag + "] equal to If-None-Match, sending 304"); } - response.setStatus(HttpServletResponse.SC_NOT_MODIFIED); + rawResponse.setStatus(HttpServletResponse.SC_NOT_MODIFIED); } else { if (logger.isTraceEnabled()) { logger.trace("ETag [" + responseETag + "] not equal to If-None-Match [" + requestETag + "], sending normal response"); } - copyBodyToResponse(body, response); + if (body.length > 0) { + rawResponse.setContentLength(body.length); + StreamUtils.copy(body, rawResponse.getOutputStream()); + } } } else { if (logger.isTraceEnabled()) { logger.trace("Response with status code [" + statusCode + "] not eligible for ETag"); } - copyBodyToResponse(body, response); - } - } - - private void copyBodyToResponse(byte[] body, HttpServletResponse response) throws IOException { - if (body.length > 0) { - response.setContentLength(body.length); - StreamUtils.copy(body, response.getOutputStream()); + if (body.length > 0) { + rawResponse.setContentLength(body.length); + StreamUtils.copy(body, rawResponse.getOutputStream()); + } } } @@ -202,19 +206,22 @@ public class ShallowEtagHeaderFilter extends OncePerRequestFilter { @Override public void sendError(int sc) throws IOException { + copyBodyToResponse(); super.sendError(sc); this.statusCode = sc; } @Override public void sendError(int sc, String msg) throws IOException { + copyBodyToResponse(); super.sendError(sc, msg); this.statusCode = sc; } @Override - public void setContentLength(int len) { - this.content.resize(len); + public void sendRedirect(String location) throws IOException { + copyBodyToResponse(); + super.sendRedirect(location); } @Override @@ -233,9 +240,17 @@ public class ShallowEtagHeaderFilter extends OncePerRequestFilter { } @Override - public void reset() { - super.reset(); - resetBuffer(); + public void setContentLength(int len) { + if (len > this.content.capacity()) { + this.content.resize(len); + } + } + + @Override + public void setBufferSize(int size) { + if (size > this.content.capacity()) { + this.content.resize(size); + } } @Override @@ -243,6 +258,12 @@ public class ShallowEtagHeaderFilter extends OncePerRequestFilter { this.content.reset(); } + @Override + public void reset() { + super.reset(); + this.content.reset(); + } + public int getStatusCode() { return this.statusCode; } @@ -251,6 +272,14 @@ public class ShallowEtagHeaderFilter extends OncePerRequestFilter { return this.content.toByteArray(); } + private void copyBodyToResponse() throws IOException { + if (this.content.size() > 0) { + getResponse().setContentLength(this.content.size()); + StreamUtils.copy(this.content.toByteArray(), getResponse().getOutputStream()); + this.content.reset(); + } + } + private class ResponseServletOutputStream extends ServletOutputStream { diff --git a/spring-web/src/test/java/org/springframework/web/filter/ShallowEtagHeaderFilterTests.java b/spring-web/src/test/java/org/springframework/web/filter/ShallowEtagHeaderFilterTests.java index 8c0318312c3..74d501147b3 100644 --- a/spring-web/src/test/java/org/springframework/web/filter/ShallowEtagHeaderFilterTests.java +++ b/spring-web/src/test/java/org/springframework/web/filter/ShallowEtagHeaderFilterTests.java @@ -32,6 +32,11 @@ import org.springframework.util.FileCopyUtils; import static org.junit.Assert.*; +/** + * @author Arjen Poutsma + * @author Brian Clozel + * @author Juergen Hoeller + */ public class ShallowEtagHeaderFilterTests { private ShallowEtagHeaderFilter filter; @@ -64,7 +69,6 @@ public class ShallowEtagHeaderFilterTests { final byte[] responseBody = "Hello World".getBytes("UTF-8"); FilterChain filterChain = new FilterChain() { - @Override public void doFilter(ServletRequest filterRequest, ServletResponse filterResponse) throws IOException, ServletException { @@ -73,7 +77,6 @@ public class ShallowEtagHeaderFilterTests { FileCopyUtils.copy(responseBody, filterResponse.getOutputStream()); } }; - filter.doFilter(request, response, filterChain); assertEquals("Invalid status", 200, response.getStatus()); @@ -90,7 +93,6 @@ public class ShallowEtagHeaderFilterTests { MockHttpServletResponse response = new MockHttpServletResponse(); FilterChain filterChain = new FilterChain() { - @Override public void doFilter(ServletRequest filterRequest, ServletResponse filterResponse) throws IOException, ServletException { @@ -100,7 +102,6 @@ public class ShallowEtagHeaderFilterTests { filterResponse.setContentLength(responseBody.length); } }; - filter.doFilter(request, response, filterChain); assertEquals("Invalid status", 304, response.getStatus()); @@ -117,7 +118,6 @@ public class ShallowEtagHeaderFilterTests { MockHttpServletResponse response = new MockHttpServletResponse(); FilterChain filterChain = new FilterChain() { - @Override public void doFilter(ServletRequest filterRequest, ServletResponse filterResponse) throws IOException, ServletException { @@ -127,7 +127,6 @@ public class ShallowEtagHeaderFilterTests { FileCopyUtils.copy(responseBody, filterResponse.getWriter()); } }; - filter.doFilter(request, response, filterChain); assertEquals("Invalid status", 304, response.getStatus()); @@ -136,4 +135,75 @@ public class ShallowEtagHeaderFilterTests { assertArrayEquals("Invalid content", new byte[0], response.getContentAsByteArray()); } -} \ No newline at end of file + @Test + public void filterSendError() throws Exception { + final MockHttpServletRequest request = new MockHttpServletRequest("GET", "/hotels"); + MockHttpServletResponse response = new MockHttpServletResponse(); + + final byte[] responseBody = "Hello World".getBytes("UTF-8"); + FilterChain filterChain = new FilterChain() { + @Override + public void doFilter(ServletRequest filterRequest, ServletResponse filterResponse) + throws IOException, ServletException { + assertEquals("Invalid request passed", request, filterRequest); + FileCopyUtils.copy(responseBody, filterResponse.getOutputStream()); + ((HttpServletResponse) filterResponse).sendError(HttpServletResponse.SC_FORBIDDEN); + } + }; + filter.doFilter(request, response, filterChain); + + assertEquals("Invalid status", 403, response.getStatus()); + assertNull("Invalid ETag header", response.getHeader("ETag")); + assertTrue("Invalid Content-Length header", response.getContentLength() > 0); + assertArrayEquals("Invalid content", responseBody, response.getContentAsByteArray()); + } + + @Test + public void filterSendErrorMessage() throws Exception { + final MockHttpServletRequest request = new MockHttpServletRequest("GET", "/hotels"); + MockHttpServletResponse response = new MockHttpServletResponse(); + + final byte[] responseBody = "Hello World".getBytes("UTF-8"); + FilterChain filterChain = new FilterChain() { + @Override + public void doFilter(ServletRequest filterRequest, ServletResponse filterResponse) + throws IOException, ServletException { + assertEquals("Invalid request passed", request, filterRequest); + FileCopyUtils.copy(responseBody, filterResponse.getOutputStream()); + ((HttpServletResponse) filterResponse).sendError(HttpServletResponse.SC_FORBIDDEN, "ERROR"); + } + }; + filter.doFilter(request, response, filterChain); + + assertEquals("Invalid status", 403, response.getStatus()); + assertNull("Invalid ETag header", response.getHeader("ETag")); + assertTrue("Invalid Content-Length header", response.getContentLength() > 0); + assertArrayEquals("Invalid content", responseBody, response.getContentAsByteArray()); + assertEquals("Invalid error message", "ERROR", response.getErrorMessage()); + } + + @Test + public void filterSendRedirect() throws Exception { + final MockHttpServletRequest request = new MockHttpServletRequest("GET", "/hotels"); + MockHttpServletResponse response = new MockHttpServletResponse(); + + final byte[] responseBody = "Hello World".getBytes("UTF-8"); + FilterChain filterChain = new FilterChain() { + @Override + public void doFilter(ServletRequest filterRequest, ServletResponse filterResponse) + throws IOException, ServletException { + assertEquals("Invalid request passed", request, filterRequest); + FileCopyUtils.copy(responseBody, filterResponse.getOutputStream()); + ((HttpServletResponse) filterResponse).sendRedirect("http://www.google.com"); + } + }; + filter.doFilter(request, response, filterChain); + + assertEquals("Invalid status", 302, response.getStatus()); + assertNull("Invalid ETag header", response.getHeader("ETag")); + assertTrue("Invalid Content-Length header", response.getContentLength() > 0); + assertArrayEquals("Invalid content", responseBody, response.getContentAsByteArray()); + assertEquals("Invalid redirect URL", "http://www.google.com", response.getRedirectedUrl()); + } + +}