This is an automated email from the ASF dual-hosted git repository. markt pushed a commit to branch 7.0.x in repository https://gitbox.apache.org/repos/asf/tomcat.git
commit c42c1e5a41392b6feb82f790d919dd2cf32b83ce Author: Mark Thomas <ma...@apache.org> AuthorDate: Mon Feb 17 10:00:30 2020 +0000 Fix HEAD response when chunking is used Ensure that the HEAD response is consistent with the GET response when HttpServlet is relied upon to generate the HEAD response and the GET response uses chunking. --- java/javax/servlet/http/HttpServlet.java | 19 +++++-- test/javax/servlet/http/TestHttpServlet.java | 63 ++++++++++++++++++++++ .../apache/catalina/startup/TomcatBaseTest.java | 9 +++- webapps/docs/changelog.xml | 5 ++ 4 files changed, 90 insertions(+), 6 deletions(-) diff --git a/java/javax/servlet/http/HttpServlet.java b/java/javax/servlet/http/HttpServlet.java index fa69ef5..28c2dc8 100644 --- a/java/javax/servlet/http/HttpServlet.java +++ b/java/javax/servlet/http/HttpServlet.java @@ -745,7 +745,7 @@ class NoBodyResponse extends HttpServletResponseWrapper { // file private NoBodyResponse(HttpServletResponse r) { super(r); - noBody = new NoBodyOutputStream(); + noBody = new NoBodyOutputStream(this); } // file private @@ -828,11 +828,13 @@ class NoBodyOutputStream extends ServletOutputStream { private static final ResourceBundle lStrings = ResourceBundle.getBundle(LSTRING_FILE); + private final HttpServletResponse response; + private boolean flushed = false; private int contentLength = 0; // file private - NoBodyOutputStream() { - // NOOP + NoBodyOutputStream(HttpServletResponse response) { + this.response = response; } // file private @@ -841,8 +843,9 @@ class NoBodyOutputStream extends ServletOutputStream { } @Override - public void write(int b) { + public void write(int b) throws IOException { contentLength++; + checkCommit(); } @Override @@ -863,5 +866,13 @@ class NoBodyOutputStream extends ServletOutputStream { } contentLength += len; + checkCommit(); + } + + private void checkCommit() throws IOException { + if (!flushed && contentLength > response.getBufferSize()) { + response.flushBuffer(); + flushed = true; + } } } diff --git a/test/javax/servlet/http/TestHttpServlet.java b/test/javax/servlet/http/TestHttpServlet.java index 2190baa..d7e78d8 100644 --- a/test/javax/servlet/http/TestHttpServlet.java +++ b/test/javax/servlet/http/TestHttpServlet.java @@ -28,6 +28,7 @@ import org.junit.Assert; import org.junit.Test; import org.apache.catalina.core.StandardContext; +import org.apache.catalina.startup.CaseInsensitiveKeyMap; import org.apache.catalina.startup.Tomcat; import org.apache.catalina.startup.TomcatBaseTest; import org.apache.tomcat.util.buf.ByteChunk; @@ -113,6 +114,51 @@ public class TestHttpServlet extends TomcatBaseTest { } + @Test + public void testChunkingWithHead() throws Exception { + Tomcat tomcat = getTomcatInstance(); + + // No file system docBase required + StandardContext ctx = (StandardContext) tomcat.addContext("", null); + + ChunkingServlet s = new ChunkingServlet(); + Tomcat.addServlet(ctx, "ChunkingServlet", s); + ctx.addServletMapping("/chunking", "ChunkingServlet"); + + tomcat.start(); + + Map<String,List<String>> getHeaders = new CaseInsensitiveKeyMap<List<String>>(); + String path = "http://localhost:" + getPort() + "/chunking"; + ByteChunk out = new ByteChunk(); + + int rc = getUrl(path, out, getHeaders); + Assert.assertEquals(HttpServletResponse.SC_OK, rc); + out.recycle(); + + Map<String,List<String>> headHeaders = new HashMap<String,List<String>>(); + rc = headUrl(path, out, headHeaders); + Assert.assertEquals(HttpServletResponse.SC_OK, rc); + + // Headers should be the same (apart from Date) + Assert.assertEquals(getHeaders.size(), headHeaders.size()); + for (Map.Entry<String, List<String>> getHeader : getHeaders.entrySet()) { + String headerName = getHeader.getKey(); + if ("date".equalsIgnoreCase(headerName)) { + continue; + } + Assert.assertTrue(headerName, headHeaders.containsKey(headerName)); + List<String> getValues = getHeader.getValue(); + List<String> headValues = headHeaders.get(headerName); + Assert.assertEquals(getValues.size(), headValues.size()); + for (String value : getValues) { + Assert.assertTrue(headValues.contains(value)); + } + } + + tomcat.stop(); + } + + private static class Bug57602ServletOuter extends HttpServlet { private static final long serialVersionUID = 1L; @@ -143,4 +189,21 @@ public class TestHttpServlet extends TomcatBaseTest { pw.println("Included"); } } + + + private static class ChunkingServlet extends HttpServlet { + + private static final long serialVersionUID = 1L; + + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) + throws ServletException, IOException { + resp.setContentType("text/plain"); + resp.setCharacterEncoding("UTF-8"); + PrintWriter pw = resp.getWriter(); + // Trigger chunking + pw.write(new char[8192 * 16]); + pw.println("Data"); + } + } } diff --git a/test/org/apache/catalina/startup/TomcatBaseTest.java b/test/org/apache/catalina/startup/TomcatBaseTest.java index e234d67..eec6416 100644 --- a/test/org/apache/catalina/startup/TomcatBaseTest.java +++ b/test/org/apache/catalina/startup/TomcatBaseTest.java @@ -667,8 +667,13 @@ public abstract class TomcatBaseTest extends LoggingBaseTest { connection.connect(); int rc = connection.getResponseCode(); if (resHead != null) { - Map<String, List<String>> head = connection.getHeaderFields(); - resHead.putAll(head); + // Skip the entry with null key that is used for the response line + // that some Map implementations may not accept. + for (Map.Entry<String, List<String>> entry : connection.getHeaderFields().entrySet()) { + if (entry.getKey() != null) { + resHead.put(entry.getKey(), entry.getValue()); + } + } } InputStream is; if (rc < 400) { diff --git a/webapps/docs/changelog.xml b/webapps/docs/changelog.xml index b41e0d2..e2a2cd4 100644 --- a/webapps/docs/changelog.xml +++ b/webapps/docs/changelog.xml @@ -67,6 +67,11 @@ file generated from the default <code>web.xml</code> so the MIME type mappings are consistent regardless of how Tomcat is started. (markt) </fix> + <fix> + Ensure that the HEAD response is consistent with the GET response when + <code>HttpServlet</code> is relied upon to generate the HEAD response + and the GET response uses chunking. (markt) + </fix> </changelog> </subsection> </section> --------------------------------------------------------------------- To unsubscribe, e-mail: dev-unsubscr...@tomcat.apache.org For additional commands, e-mail: dev-h...@tomcat.apache.org