Author: markt Date: Wed May 30 14:13:47 2012 New Revision: 1344267 URL: http://svn.apache.org/viewvc?rev=1344267&view=rev Log: Fix https://issues.apache.org/bugzilla/show_bug.cgi?id=53169 Allow servlets to opt to avoid chunked encoding with a response of unknown length by setting the Connection: close header. Based on a patch suggested by Philippe Marschall.
Modified: tomcat/tc7.0.x/trunk/ (props changed) tomcat/tc7.0.x/trunk/java/org/apache/coyote/http11/AbstractHttp11Processor.java tomcat/tc7.0.x/trunk/test/org/apache/coyote/http11/TestAbstractHttp11Processor.java tomcat/tc7.0.x/trunk/webapps/docs/changelog.xml Propchange: tomcat/tc7.0.x/trunk/ ------------------------------------------------------------------------------ Merged /tomcat/trunk:r1344266 Modified: tomcat/tc7.0.x/trunk/java/org/apache/coyote/http11/AbstractHttp11Processor.java URL: http://svn.apache.org/viewvc/tomcat/tc7.0.x/trunk/java/org/apache/coyote/http11/AbstractHttp11Processor.java?rev=1344267&r1=1344266&r2=1344267&view=diff ============================================================================== --- tomcat/tc7.0.x/trunk/java/org/apache/coyote/http11/AbstractHttp11Processor.java (original) +++ tomcat/tc7.0.x/trunk/java/org/apache/coyote/http11/AbstractHttp11Processor.java Wed May 30 14:13:47 2012 @@ -1135,7 +1135,7 @@ public abstract class AbstractHttp11Proc MimeHeaders headers = request.getMimeHeaders(); // Check connection header - MessageBytes connectionValueMB = headers.getValue("connection"); + MessageBytes connectionValueMB = headers.getValue(Constants.CONNECTION); if (connectionValueMB != null) { ByteChunk connectionValueBC = connectionValueMB.getByteChunk(); if (findBytes(connectionValueBC, Constants.CLOSE_BYTES) != -1) { @@ -1374,13 +1374,17 @@ public abstract class AbstractHttp11Proc } long contentLength = response.getContentLengthLong(); + boolean connectionClosePresent = false; if (contentLength != -1) { headers.setValue("Content-Length").setLong(contentLength); getOutputBuffer().addActiveFilter (outputFilters[Constants.IDENTITY_FILTER]); contentDelimitation = true; } else { - if (entityBody && http11) { + // If the response code supports an entity body and we're on + // HTTP 1.1 then we chunk unless we have a Connection: close header + connectionClosePresent = isConnectionClose(headers); + if (entityBody && http11 && !connectionClosePresent) { getOutputBuffer().addActiveFilter (outputFilters[Constants.CHUNKED_FILTER]); contentDelimitation = true; @@ -1426,7 +1430,11 @@ public abstract class AbstractHttp11Proc // Connection: close header. keepAlive = keepAlive && !statusDropsConnection(statusCode); if (!keepAlive) { - headers.addValue(Constants.CONNECTION).setString(Constants.CLOSE); + // Avoid adding the close header twice + if (!connectionClosePresent) { + headers.addValue(Constants.CONNECTION).setString( + Constants.CLOSE); + } } else if (!http11 && !error) { headers.addValue(Constants.CONNECTION).setString(Constants.KEEPALIVE); } @@ -1451,6 +1459,14 @@ public abstract class AbstractHttp11Proc } + private boolean isConnectionClose(MimeHeaders headers) { + MessageBytes connection = headers.getValue(Constants.CONNECTION); + if (connection == null) { + return false; + } + return connection.equals(Constants.CLOSE); + } + abstract boolean prepareSendfile(OutputFilter[] outputFilters); /** Modified: tomcat/tc7.0.x/trunk/test/org/apache/coyote/http11/TestAbstractHttp11Processor.java URL: http://svn.apache.org/viewvc/tomcat/tc7.0.x/trunk/test/org/apache/coyote/http11/TestAbstractHttp11Processor.java?rev=1344267&r1=1344266&r2=1344267&view=diff ============================================================================== --- tomcat/tc7.0.x/trunk/test/org/apache/coyote/http11/TestAbstractHttp11Processor.java (original) +++ tomcat/tc7.0.x/trunk/test/org/apache/coyote/http11/TestAbstractHttp11Processor.java Wed May 30 14:13:47 2012 @@ -16,20 +16,28 @@ */ package org.apache.coyote.http11; -import java.io.File; -import java.io.IOException; - import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; -import org.junit.Test; +import java.io.File; +import java.io.IOException; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; import org.apache.catalina.Context; import org.apache.catalina.startup.SimpleHttpClient; import org.apache.catalina.startup.TesterServlet; import org.apache.catalina.startup.Tomcat; import org.apache.catalina.startup.TomcatBaseTest; +import org.apache.tomcat.util.buf.ByteChunk; +import org.junit.Test; public class TestAbstractHttp11Processor extends TomcatBaseTest { @@ -239,6 +247,105 @@ public class TestAbstractHttp11Processor assertEquals("OK", client.getResponseBody()); } + + @Test + public void testChunking11NoContentLength() throws Exception { + Tomcat tomcat = getTomcatInstance(); + + // Must have a real docBase - just use temp + Context ctxt = tomcat.addContext("", + System.getProperty("java.io.tmpdir")); + + Tomcat.addServlet(ctxt, "NoContentLengthFlushingServlet", + new NoContentLengthFlushingServlet()); + ctxt.addServletMapping("/test", "NoContentLengthFlushingServlet"); + + tomcat.start(); + + ByteChunk responseBody = new ByteChunk(); + Map<String,List<String>> responseHeaders = + new HashMap<String,List<String>>(); + int rc = getUrl("http://localhost:" + getPort() + "/test", responseBody, + responseHeaders); + + assertEquals(HttpServletResponse.SC_OK, rc); + assertTrue(responseHeaders.containsKey("Transfer-Encoding")); + List<String> encodings = responseHeaders.get("Transfer-Encoding"); + assertEquals(1, encodings.size()); + assertEquals("chunked", encodings.get(0)); + } + + @Test + public void testNoChunking11NoContentLengthConnectionClose() + throws Exception { + + Tomcat tomcat = getTomcatInstance(); + + // Must have a real docBase - just use temp + Context ctxt = tomcat.addContext("", + System.getProperty("java.io.tmpdir")); + + Tomcat.addServlet(ctxt, "NoContentLengthConnectionCloseFlushingServlet", + new NoContentLengthConnectionCloseFlushingServlet()); + ctxt.addServletMapping("/test", + "NoContentLengthConnectionCloseFlushingServlet"); + + tomcat.start(); + + ByteChunk responseBody = new ByteChunk(); + Map<String,List<String>> responseHeaders = + new HashMap<String,List<String>>(); + int rc = getUrl("http://localhost:" + getPort() + "/test", responseBody, + responseHeaders); + + assertEquals(HttpServletResponse.SC_OK, rc); + + assertTrue(responseHeaders.containsKey("Connection")); + List<String> connections = responseHeaders.get("Connection"); + assertEquals(1, connections.size()); + assertEquals("close", connections.get(0)); + + assertFalse(responseHeaders.containsKey("Transfer-Encoding")); + + assertEquals("OK", responseBody.toString()); + } + + // flushes with no content-length set + // should result in chunking on HTTP 1.1 + private static final class NoContentLengthFlushingServlet + extends HttpServlet { + + private static final long serialVersionUID = 1L; + + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) + throws ServletException, IOException { + resp.setStatus(HttpServletResponse.SC_OK); + resp.setContentType("text/plain"); + resp.getWriter().write("OK"); + resp.flushBuffer(); + } + } + + // flushes with no content-length set but sets Connection: close header + // should no result in chunking on HTTP 1.1 + private static final class NoContentLengthConnectionCloseFlushingServlet + extends HttpServlet { + + private static final long serialVersionUID = 1L; + + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) + throws ServletException, IOException { + resp.setStatus(HttpServletResponse.SC_OK); + resp.setContentType("text/event-stream"); + resp.addHeader("Connection", "close"); + resp.flushBuffer(); + resp.getWriter().write("OK"); + resp.flushBuffer(); + } + } + private static final class Client extends SimpleHttpClient { public Client(int port) { Modified: tomcat/tc7.0.x/trunk/webapps/docs/changelog.xml URL: http://svn.apache.org/viewvc/tomcat/tc7.0.x/trunk/webapps/docs/changelog.xml?rev=1344267&r1=1344266&r2=1344267&view=diff ============================================================================== --- tomcat/tc7.0.x/trunk/webapps/docs/changelog.xml (original) +++ tomcat/tc7.0.x/trunk/webapps/docs/changelog.xml Wed May 30 14:13:47 2012 @@ -180,6 +180,11 @@ client disconnects before the response has been fully written from an AJP connection using the APR/native connector. (markt) </fix> + <add> + <bug>53169</bug>: Allow developers to avoid chunked encoding for a + response of unknown length by setting the <code>Connection: close</code> + header. Based on a patch suggested by Philippe Marschall. (markt) + </add> <fix> <bug>53173</bug> (<rev>1333116</rev>) : Properly count down maxConnections (fhanik) --------------------------------------------------------------------- To unsubscribe, e-mail: dev-unsubscr...@tomcat.apache.org For additional commands, e-mail: dev-h...@tomcat.apache.org