Author: markt Date: Thu May 25 20:05:55 2017 New Revision: 1796186 URL: http://svn.apache.org/viewvc?rev=1796186&view=rev Log: Servlet 4.0 Implement writing trailer headers where the protocol supports it
Added: tomcat/trunk/webapps/examples/WEB-INF/classes/trailers/ tomcat/trunk/webapps/examples/WEB-INF/classes/trailers/ResponseTrailers.java (with props) Modified: tomcat/trunk/java/javax/servlet/http/HttpServletResponse.java tomcat/trunk/java/javax/servlet/http/HttpServletResponseWrapper.java tomcat/trunk/java/org/apache/catalina/connector/Response.java tomcat/trunk/java/org/apache/catalina/connector/ResponseFacade.java tomcat/trunk/java/org/apache/coyote/AbstractProcessor.java tomcat/trunk/java/org/apache/coyote/ActionCode.java tomcat/trunk/java/org/apache/coyote/LocalStrings.properties tomcat/trunk/java/org/apache/coyote/Response.java tomcat/trunk/java/org/apache/coyote/http11/Http11OutputBuffer.java tomcat/trunk/java/org/apache/coyote/http11/Http11Processor.java tomcat/trunk/java/org/apache/coyote/http11/filters/ChunkedOutputFilter.java tomcat/trunk/java/org/apache/coyote/http2/Http2AsyncUpgradeHandler.java tomcat/trunk/java/org/apache/coyote/http2/Http2UpgradeHandler.java tomcat/trunk/java/org/apache/coyote/http2/LocalStrings.properties tomcat/trunk/java/org/apache/coyote/http2/Stream.java tomcat/trunk/java/org/apache/coyote/http2/StreamProcessor.java tomcat/trunk/test/org/apache/catalina/filters/TesterHttpServletResponse.java tomcat/trunk/test/org/apache/coyote/http2/TestStream.java tomcat/trunk/webapps/docs/changelog.xml tomcat/trunk/webapps/examples/WEB-INF/web.xml tomcat/trunk/webapps/examples/servlets/index.html Modified: tomcat/trunk/java/javax/servlet/http/HttpServletResponse.java URL: http://svn.apache.org/viewvc/tomcat/trunk/java/javax/servlet/http/HttpServletResponse.java?rev=1796186&r1=1796185&r2=1796186&view=diff ============================================================================== --- tomcat/trunk/java/javax/servlet/http/HttpServletResponse.java (original) +++ tomcat/trunk/java/javax/servlet/http/HttpServletResponse.java Thu May 25 20:05:55 2017 @@ -18,6 +18,8 @@ package javax.servlet.http; import java.io.IOException; import java.util.Collection; +import java.util.Map; +import java.util.function.Supplier; import javax.servlet.ServletResponse; @@ -340,6 +342,25 @@ public interface HttpServletResponse ext */ public Collection<String> getHeaderNames(); + /** + * Configure the supplier of the trailer headers. The supplier will be + * called in the scope of the thread that completes the response. + * <b> + * Trailers that don't meet the requirements of RFC 7230, section 4.1.2 will + * be ignored. + * + * @param supplier The supplier for the trailer headers + * + * @throws IllegalStateException if this method is called when the + * underlying protocol does not support trailer headers or if using + * HTTP/1.1 and the response has already been committed + * + * @since Servlet 4.0 + */ + public default void setTrailerFields(Supplier<Map<String, String>> supplier) { + // NO-OP + } + /* * Server status codes; see RFC 2068. */ Modified: tomcat/trunk/java/javax/servlet/http/HttpServletResponseWrapper.java URL: http://svn.apache.org/viewvc/tomcat/trunk/java/javax/servlet/http/HttpServletResponseWrapper.java?rev=1796186&r1=1796185&r2=1796186&view=diff ============================================================================== --- tomcat/trunk/java/javax/servlet/http/HttpServletResponseWrapper.java (original) +++ tomcat/trunk/java/javax/servlet/http/HttpServletResponseWrapper.java Thu May 25 20:05:55 2017 @@ -18,6 +18,8 @@ package javax.servlet.http; import java.io.IOException; import java.util.Collection; +import java.util.Map; +import java.util.function.Supplier; import javax.servlet.ServletResponseWrapper; @@ -266,4 +268,18 @@ public class HttpServletResponseWrapper public Collection<String> getHeaderNames() { return this._getHttpServletResponse().getHeaderNames(); } + + /** + * {@inheritDoc} + * <p> + * The default implementation is to call + * {@link HttpServletResponse#setTrailerFields(Supplier)} + * on the wrapper {@link HttpServletResponse}. + * + * @since Servlet 4.0 + */ + @Override + public void setTrailerFields(Supplier<Map<String, String>> supplier) { + this._getHttpServletResponse().setTrailerFields(supplier); + } } Modified: tomcat/trunk/java/org/apache/catalina/connector/Response.java URL: http://svn.apache.org/viewvc/tomcat/trunk/java/org/apache/catalina/connector/Response.java?rev=1796186&r1=1796185&r2=1796186&view=diff ============================================================================== --- tomcat/trunk/java/org/apache/catalina/connector/Response.java (original) +++ tomcat/trunk/java/org/apache/catalina/connector/Response.java Thu May 25 20:05:55 2017 @@ -33,9 +33,11 @@ import java.util.Collection; import java.util.Enumeration; import java.util.List; import java.util.Locale; +import java.util.Map; import java.util.TimeZone; import java.util.Vector; import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.Supplier; import javax.servlet.ServletOutputStream; import javax.servlet.ServletResponse; @@ -1152,6 +1154,12 @@ public class Response implements HttpSer } + @Override + public void setTrailerFields(Supplier<Map<String, String>> supplier) { + getCoyoteResponse().setTrailerFields(supplier); + } + + /** * Encode the session identifier associated with this response * into the specified redirect URL, if necessary. Modified: tomcat/trunk/java/org/apache/catalina/connector/ResponseFacade.java URL: http://svn.apache.org/viewvc/tomcat/trunk/java/org/apache/catalina/connector/ResponseFacade.java?rev=1796186&r1=1796185&r2=1796186&view=diff ============================================================================== --- tomcat/trunk/java/org/apache/catalina/connector/ResponseFacade.java (original) +++ tomcat/trunk/java/org/apache/catalina/connector/ResponseFacade.java Thu May 25 20:05:55 2017 @@ -24,6 +24,8 @@ import java.security.PrivilegedActionExc import java.security.PrivilegedExceptionAction; import java.util.Collection; import java.util.Locale; +import java.util.Map; +import java.util.function.Supplier; import javax.servlet.ServletOutputStream; import javax.servlet.http.Cookie; @@ -640,4 +642,10 @@ public class ResponseFacade implements H public Collection<String> getHeaders(String name) { return response.getHeaders(name); } + + + @Override + public void setTrailerFields(Supplier<Map<String, String>> supplier) { + response.setTrailerFields(supplier); + } } Modified: tomcat/trunk/java/org/apache/coyote/AbstractProcessor.java URL: http://svn.apache.org/viewvc/tomcat/trunk/java/org/apache/coyote/AbstractProcessor.java?rev=1796186&r1=1796185&r2=1796186&view=diff ============================================================================== --- tomcat/trunk/java/org/apache/coyote/AbstractProcessor.java (original) +++ tomcat/trunk/java/org/apache/coyote/AbstractProcessor.java Thu May 25 20:05:55 2017 @@ -492,6 +492,11 @@ public abstract class AbstractProcessor result.set(isTrailerFieldsReady()); break; } + case IS_TRAILER_FIELDS_SUPPORTED: { + AtomicBoolean result = (AtomicBoolean) param; + result.set(isTrailerFieldsSupported()); + break; + } } } @@ -770,6 +775,18 @@ public abstract class AbstractProcessor /** + * Protocols that support trailer fields should override this method and + * return {@code true}. + * + * @return {@code true} if trailer fields are supported by this processor, + * otherwise {@code false}. + */ + protected boolean isTrailerFieldsSupported() { + return false; + } + + + /** * Flush any pending writes. Used during non-blocking writes to flush any * remaining data from a previous incomplete write. * Modified: tomcat/trunk/java/org/apache/coyote/ActionCode.java URL: http://svn.apache.org/viewvc/tomcat/trunk/java/org/apache/coyote/ActionCode.java?rev=1796186&r1=1796185&r2=1796186&view=diff ============================================================================== --- tomcat/trunk/java/org/apache/coyote/ActionCode.java (original) +++ tomcat/trunk/java/org/apache/coyote/ActionCode.java Thu May 25 20:05:55 2017 @@ -252,5 +252,12 @@ public enum ActionCode { * true if it is known that request trailer fields are not supported so an * empty collection of trailers can then be read. */ - IS_TRAILER_FIELDS_READY + IS_TRAILER_FIELDS_READY, + + /** + * Are HTTP trailer fields supported for the current response? Note that + * once an HTTP/1.1 response has been committed, it will no longer support + * trailer fields. + */ + IS_TRAILER_FIELDS_SUPPORTED } Modified: tomcat/trunk/java/org/apache/coyote/LocalStrings.properties URL: http://svn.apache.org/viewvc/tomcat/trunk/java/org/apache/coyote/LocalStrings.properties?rev=1796186&r1=1796185&r2=1796186&view=diff ============================================================================== --- tomcat/trunk/java/org/apache/coyote/LocalStrings.properties (original) +++ tomcat/trunk/java/org/apache/coyote/LocalStrings.properties Thu May 25 20:05:55 2017 @@ -49,5 +49,6 @@ request.readListenerSet=The non-blocking response.encoding.invalid=The encoding [{0}] is not recognised by the JRE response.notAsync=It is only valid to switch to non-blocking IO within async processing or HTTP upgrade processing response.notNonBlocking=It is invalid to call isReady() when the response has not been put into non-blocking mode +response.noTrailers.notSupported=A trailer fields supplier may not be set for this response. Either the underlying protocol does not support trailer fields or the protocol requires that the supplier is set before the response is committed response.nullWriteListener=The listener passed to setWriteListener() may not be null response.writeListenerSet=The non-blocking write listener has already been set Modified: tomcat/trunk/java/org/apache/coyote/Response.java URL: http://svn.apache.org/viewvc/tomcat/trunk/java/org/apache/coyote/Response.java?rev=1796186&r1=1796185&r2=1796186&view=diff ============================================================================== --- tomcat/trunk/java/org/apache/coyote/Response.java (original) +++ tomcat/trunk/java/org/apache/coyote/Response.java Thu May 25 20:05:55 2017 @@ -22,7 +22,9 @@ import java.io.UnsupportedEncodingExcept import java.nio.ByteBuffer; import java.nio.charset.Charset; import java.util.Locale; +import java.util.Map; import java.util.concurrent.atomic.AtomicBoolean; +import java.util.function.Supplier; import javax.servlet.WriteListener; @@ -78,6 +80,8 @@ public final class Response { final MimeHeaders headers = new MimeHeaders(); + private Supplier<Map<String,String>> trailerFieldsSupplier = null; + /** * Associated output buffer. */ @@ -322,6 +326,22 @@ public final class Response { } + public void setTrailerFields(Supplier<Map<String, String>> supplier) { + AtomicBoolean trailerFieldsSupported = new AtomicBoolean(false); + action(ActionCode.IS_TRAILER_FIELDS_SUPPORTED, trailerFieldsSupported); + if (!trailerFieldsSupported.get()) { + throw new IllegalStateException(sm.getString("response.noTrailers.notSupported")); + } + + this.trailerFieldsSupplier = supplier; + } + + + public Supplier<Map<String, String>> getTrailerFields() { + return trailerFieldsSupplier; + } + + /** * Set internal fields for special header names. * Called from set/addHeader. @@ -530,6 +550,7 @@ public final class Response { commitTime = -1; errorException = null; headers.clear(); + trailerFieldsSupplier = null; // Servlet 3.1 non-blocking write listener listener = null; fireListener = false; Modified: tomcat/trunk/java/org/apache/coyote/http11/Http11OutputBuffer.java URL: http://svn.apache.org/viewvc/tomcat/trunk/java/org/apache/coyote/http11/Http11OutputBuffer.java?rev=1796186&r1=1796185&r2=1796186&view=diff ============================================================================== --- tomcat/trunk/java/org/apache/coyote/http11/Http11OutputBuffer.java (original) +++ tomcat/trunk/java/org/apache/coyote/http11/Http11OutputBuffer.java Thu May 25 20:05:55 2017 @@ -524,6 +524,16 @@ public class Http11OutputBuffer implemen } + boolean isChunking() { + for (int i = 0; i < lastActiveFilter; i++) { + if (activeFilters[i] == filterLibrary[Constants.CHUNKED_FILTER]) { + return true; + } + } + return false; + } + + // ------------------------------------------ SocketOutputBuffer Inner Class /** Modified: tomcat/trunk/java/org/apache/coyote/http11/Http11Processor.java URL: http://svn.apache.org/viewvc/tomcat/trunk/java/org/apache/coyote/http11/Http11Processor.java?rev=1796186&r1=1796185&r2=1796186&view=diff ============================================================================== --- tomcat/trunk/java/org/apache/coyote/http11/Http11Processor.java (original) +++ tomcat/trunk/java/org/apache/coyote/http11/Http11Processor.java Thu May 25 20:05:55 2017 @@ -900,23 +900,25 @@ public class Http11Processor extends Abs long contentLength = response.getContentLengthLong(); boolean connectionClosePresent = false; - if (contentLength != -1) { + if (http11 && response.getTrailerFields() != null) { + // If trailer fields are set, always use chunking + outputBuffer.addActiveFilter(outputFilters[Constants.CHUNKED_FILTER]); + contentDelimitation = true; + headers.addValue(Constants.TRANSFERENCODING).setString(Constants.CHUNKED); + } else if (contentLength != -1) { headers.setValue("Content-Length").setLong(contentLength); - outputBuffer.addActiveFilter - (outputFilters[Constants.IDENTITY_FILTER]); + outputBuffer.addActiveFilter(outputFilters[Constants.IDENTITY_FILTER]); contentDelimitation = true; } else { // 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) { - outputBuffer.addActiveFilter - (outputFilters[Constants.CHUNKED_FILTER]); + if (http11 && entityBody && !connectionClosePresent) { + outputBuffer.addActiveFilter(outputFilters[Constants.CHUNKED_FILTER]); contentDelimitation = true; headers.addValue(Constants.TRANSFERENCODING).setString(Constants.CHUNKED); } else { - outputBuffer.addActiveFilter - (outputFilters[Constants.IDENTITY_FILTER]); + outputBuffer.addActiveFilter(outputFilters[Constants.IDENTITY_FILTER]); } } @@ -1317,6 +1319,24 @@ public class Http11Processor extends Abs } + @Override + protected boolean isTrailerFieldsSupported() { + // Request must be HTTP/1.1 to support trailer fields + if (!http11) { + return false; + } + + // If the response is not yet committed, chunked encoding can be used + // and the trailer fields sent + if (!response.isCommitted()) { + return true; + } + + // Response has been committed - need to see if chunked is being used + return outputBuffer.isChunking(); + } + + /** * Trigger sendfile processing if required. * Modified: tomcat/trunk/java/org/apache/coyote/http11/filters/ChunkedOutputFilter.java URL: http://svn.apache.org/viewvc/tomcat/trunk/java/org/apache/coyote/http11/filters/ChunkedOutputFilter.java?rev=1796186&r1=1796185&r2=1796186&view=diff ============================================================================== --- tomcat/trunk/java/org/apache/coyote/http11/filters/ChunkedOutputFilter.java (original) +++ tomcat/trunk/java/org/apache/coyote/http11/filters/ChunkedOutputFilter.java Thu May 25 20:05:55 2017 @@ -17,12 +17,20 @@ package org.apache.coyote.http11.filters; import java.io.IOException; +import java.io.OutputStreamWriter; import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; +import java.util.HashSet; +import java.util.Locale; +import java.util.Map; +import java.util.Set; +import java.util.function.Supplier; import org.apache.coyote.OutputBuffer; import org.apache.coyote.Response; import org.apache.coyote.http11.OutputFilter; import org.apache.tomcat.util.buf.HexUtils; +import org.apache.tomcat.util.http.fileupload.ByteArrayOutputStream; /** * Chunked output filter. @@ -31,9 +39,30 @@ import org.apache.tomcat.util.buf.HexUti */ public class ChunkedOutputFilter implements OutputFilter { + private static final byte[] LAST_CHUNK_BYTES = {(byte) '0', (byte) '\r', (byte) '\n'}; + private static final byte[] CRLF_BYTES = {(byte) '\r', (byte) '\n'}; private static final byte[] END_CHUNK_BYTES = {(byte) '0', (byte) '\r', (byte) '\n', (byte) '\r', (byte) '\n'}; + private static final Set<String> disallowedTrailerFieldNames = new HashSet<>(); + + static { + // Always add these in lower case + disallowedTrailerFieldNames.add("age"); + disallowedTrailerFieldNames.add("cache-control"); + disallowedTrailerFieldNames.add("content-length"); + disallowedTrailerFieldNames.add("content-encoding"); + disallowedTrailerFieldNames.add("content-range"); + disallowedTrailerFieldNames.add("content-type"); + disallowedTrailerFieldNames.add("date"); + disallowedTrailerFieldNames.add("expires"); + disallowedTrailerFieldNames.add("location"); + disallowedTrailerFieldNames.add("retry-after"); + disallowedTrailerFieldNames.add("trailer"); + disallowedTrailerFieldNames.add("transfer-encoding"); + disallowedTrailerFieldNames.add("vary"); + disallowedTrailerFieldNames.add("warning"); + } /** * Next buffer in the pipeline. @@ -47,12 +76,17 @@ public class ChunkedOutputFilter impleme protected final ByteBuffer chunkHeader = ByteBuffer.allocate(10); + protected final ByteBuffer lastChunk = ByteBuffer.wrap(LAST_CHUNK_BYTES); + protected final ByteBuffer crlfChunk = ByteBuffer.wrap(CRLF_BYTES); /** * End chunk. */ protected final ByteBuffer endChunk = ByteBuffer.wrap(END_CHUNK_BYTES); + private Response response; + + public ChunkedOutputFilter() { chunkHeader.put(8, (byte) '\r'); chunkHeader.put(9, (byte) '\n'); @@ -112,7 +146,7 @@ public class ChunkedOutputFilter impleme */ @Override public void setResponse(Response response) { - // NOOP: No need for parameters from response in this filter + this.response = response; } @@ -132,9 +166,40 @@ public class ChunkedOutputFilter impleme @Override public long end() throws IOException { - // Write end chunk - buffer.doWrite(endChunk); - endChunk.position(0).limit(endChunk.capacity()); + Supplier<Map<String,String>> trailerFieldsSupplier = response.getTrailerFields(); + Map<String,String> trailerFields = null; + + if (trailerFieldsSupplier != null) { + trailerFields = trailerFieldsSupplier.get(); + } + + if (trailerFields == null) { + // Write end chunk + buffer.doWrite(endChunk); + endChunk.position(0).limit(endChunk.capacity()); + } else { + buffer.doWrite(lastChunk); + lastChunk.position(0).limit(lastChunk.capacity()); + + ByteArrayOutputStream baos = new ByteArrayOutputStream(1024); + OutputStreamWriter osw = new OutputStreamWriter(baos, StandardCharsets.ISO_8859_1); + for (Map.Entry<String,String> trailerField : trailerFields.entrySet()) { + // Ignore disallowed headers + if (disallowedTrailerFieldNames.contains( + trailerField.getKey().toLowerCase(Locale.ENGLISH))) { + continue; + } + osw.write(trailerField.getKey()); + osw.write(':'); + osw.write(trailerField.getValue()); + osw.write("\r\n"); + } + osw.close(); + buffer.doWrite(ByteBuffer.wrap(baos.toByteArray())); + + buffer.doWrite(crlfChunk); + crlfChunk.position(0).limit(crlfChunk.capacity()); + } return 0; } @@ -145,6 +210,6 @@ public class ChunkedOutputFilter impleme */ @Override public void recycle() { - // NOOP: Nothing to recycle + response = null; } } Modified: tomcat/trunk/java/org/apache/coyote/http2/Http2AsyncUpgradeHandler.java URL: http://svn.apache.org/viewvc/tomcat/trunk/java/org/apache/coyote/http2/Http2AsyncUpgradeHandler.java?rev=1796186&r1=1796185&r2=1796186&view=diff ============================================================================== --- tomcat/trunk/java/org/apache/coyote/http2/Http2AsyncUpgradeHandler.java (original) +++ tomcat/trunk/java/org/apache/coyote/http2/Http2AsyncUpgradeHandler.java Thu May 25 20:05:55 2017 @@ -136,6 +136,9 @@ public class Http2AsyncUpgradeHandler ex void writeHeaders(Stream stream, int pushedStreamId, MimeHeaders mimeHeaders, boolean endOfStream, int payloadSize) throws IOException { doWriteHeaders(stream, pushedStreamId, mimeHeaders, endOfStream, payloadSize); + if (endOfStream) { + stream.sentEndOfStream(); + } } Modified: tomcat/trunk/java/org/apache/coyote/http2/Http2UpgradeHandler.java URL: http://svn.apache.org/viewvc/tomcat/trunk/java/org/apache/coyote/http2/Http2UpgradeHandler.java?rev=1796186&r1=1796185&r2=1796186&view=diff ============================================================================== --- tomcat/trunk/java/org/apache/coyote/http2/Http2UpgradeHandler.java (original) +++ tomcat/trunk/java/org/apache/coyote/http2/Http2UpgradeHandler.java Thu May 25 20:05:55 2017 @@ -531,6 +531,9 @@ class Http2UpgradeHandler extends Abstra synchronized (socketWrapper) { doWriteHeaders(stream, pushedStreamId, mimeHeaders, endOfStream, payloadSize); } + if (endOfStream) { + stream.sentEndOfStream(); + } } Modified: tomcat/trunk/java/org/apache/coyote/http2/LocalStrings.properties URL: http://svn.apache.org/viewvc/tomcat/trunk/java/org/apache/coyote/http2/LocalStrings.properties?rev=1796186&r1=1796185&r2=1796186&view=diff ============================================================================== --- tomcat/trunk/java/org/apache/coyote/http2/LocalStrings.properties (original) +++ tomcat/trunk/java/org/apache/coyote/http2/LocalStrings.properties Thu May 25 20:05:55 2017 @@ -143,7 +143,7 @@ upgradeHandler.windowSizeTooBig=Connecti upgradeHandler.windowSizeReservationInterrupted=Connection [{0}], Stream [{1}], reservation for [{2}] bytes upgradeHandler.writeBody=Connection [{0}], Stream [{1}], Data length [{2}] upgradeHandler.writeHeaders=Connection [{0}], Stream [{1}] -upgradeHandler.writePushHeaders=Connection [{0}], Stream [{1}], Pushed stream [{2}] +upgradeHandler.writePushHeaders=Connection [{0}], Stream [{1}], Pushed stream [{2}], EndOfStream [{3}] writeStateMachine.endWrite.ise=It is illegal to specify [{0}] for the new state once a write has completed writeStateMachine.ise=It is illegal to call [{0}()] in state [{1}] \ No newline at end of file Modified: tomcat/trunk/java/org/apache/coyote/http2/Stream.java URL: http://svn.apache.org/viewvc/tomcat/trunk/java/org/apache/coyote/http2/Stream.java?rev=1796186&r1=1796185&r2=1796186&view=diff ============================================================================== --- tomcat/trunk/java/org/apache/coyote/http2/Stream.java (original) +++ tomcat/trunk/java/org/apache/coyote/http2/Stream.java Thu May 25 20:05:55 2017 @@ -22,8 +22,11 @@ import java.nio.charset.StandardCharsets import java.security.AccessController; import java.security.PrivilegedActionException; import java.security.PrivilegedExceptionAction; +import java.util.Collections; import java.util.Iterator; import java.util.Locale; +import java.util.Map; +import java.util.function.Supplier; import org.apache.coyote.ActionCode; import org.apache.coyote.CloseNowException; @@ -35,6 +38,7 @@ import org.apache.coyote.http2.HpackDeco import org.apache.juli.logging.Log; import org.apache.juli.logging.LogFactory; import org.apache.tomcat.util.buf.ByteChunk; +import org.apache.tomcat.util.buf.MessageBytes; import org.apache.tomcat.util.http.FastHttpDateFormat; import org.apache.tomcat.util.http.MimeHeaders; import org.apache.tomcat.util.net.ApplicationBufferHandler; @@ -392,11 +396,42 @@ class Stream extends AbstractStream impl final void writeHeaders() throws IOException { prepareHeaders(coyoteResponse); - boolean endOfStream = getOutputBuffer().hasNoBody(); + boolean endOfStream = getOutputBuffer().hasNoBody() && + coyoteResponse.getTrailerFields() == null; // TODO: Is 1k the optimal value? handler.writeHeaders(this, 0, coyoteResponse.getMimeHeaders(), endOfStream, 1024); } + + final void writeTrailers() throws IOException { + Supplier<Map<String,String>> supplier = coyoteResponse.getTrailerFields(); + if (supplier == null) { + // No supplier was set, end of stream will already have been sent + return; + } + + // We can re-use the MimeHeaders from the response since they have + // already been processed by the encoder at this point + MimeHeaders mimeHeaders = coyoteResponse.getMimeHeaders(); + mimeHeaders.recycle(); + + Map<String,String> headerMap = supplier.get(); + if (headerMap == null) { + headerMap = Collections.emptyMap(); + } + + // Copy the contents of the Map to the MimeHeaders + // TODO: Is there benefit in refactoring this? Is MimeHeaders too + // heavyweight? Can we reduce the copy/conversions? + for (Map.Entry<String, String> headerEntry : headerMap.entrySet()) { + MessageBytes mb = mimeHeaders.addValue(headerEntry.getKey()); + mb.setString(headerEntry.getValue()); + } + + handler.writeHeaders(this, 0, mimeHeaders, true, 1024); + } + + final void writeAck() throws IOException { // TODO: Is 64 too big? Just the status header with compression handler.writeHeaders(this, 0, ACK_HEADERS, false, 64); @@ -586,12 +621,17 @@ class Stream extends AbstractStream impl } - public boolean isTrailerFieldsReady() { + boolean isTrailerFieldsReady() { // Once EndOfStream has been received, canRead will be false return !state.canRead(); } + boolean isTrailerFieldsSupported() { + return !getOutputBuffer().endOfStreamSent; + } + + private static void push(final Http2UpgradeHandler handler, final Request request, final Stream stream) throws IOException { if (org.apache.coyote.Constants.IS_SECURITY_ENABLED) { @@ -716,7 +756,8 @@ class Stream extends AbstractStream impl if (closed && !endOfStreamSent) { // Handling this special case here is simpler than trying // to modify the following code to handle it. - handler.writeBody(Stream.this, buffer, 0, true); + handler.writeBody(Stream.this, buffer, 0, + coyoteResponse.getTrailerFields() == null); } // Buffer is empty. Nothing to do. return false; @@ -735,7 +776,8 @@ class Stream extends AbstractStream impl handler.reserveWindowSize(Stream.this, streamReservation); // Do the write handler.writeBody(Stream.this, buffer, connectionReservation, - !writeInProgress && closed && left == connectionReservation); + !writeInProgress && closed && left == connectionReservation && + coyoteResponse.getTrailerFields() == null); streamReservation -= connectionReservation; left -= connectionReservation; } @@ -760,6 +802,7 @@ class Stream extends AbstractStream impl final void close() throws IOException { closed = true; flushData(); + writeTrailers(); } /** Modified: tomcat/trunk/java/org/apache/coyote/http2/StreamProcessor.java URL: http://svn.apache.org/viewvc/tomcat/trunk/java/org/apache/coyote/http2/StreamProcessor.java?rev=1796186&r1=1796185&r2=1796186&view=diff ============================================================================== --- tomcat/trunk/java/org/apache/coyote/http2/StreamProcessor.java (original) +++ tomcat/trunk/java/org/apache/coyote/http2/StreamProcessor.java Thu May 25 20:05:55 2017 @@ -190,7 +190,7 @@ class StreamProcessor extends AbstractPr synchronized (this) { /* * TODO Check if this sync is necessary. - * Compare with superrclass that uses SocketWrapper + * Compare with superclass that uses SocketWrapper */ while (dispatches != null && dispatches.hasNext()) { DispatchType dispatchType = dispatches.next(); @@ -223,6 +223,12 @@ class StreamProcessor extends AbstractPr } + @Override + protected boolean isTrailerFieldsSupported() { + return stream.isTrailerFieldsSupported(); + } + + @Override public final void recycle() { // StreamProcessor instances are not re-used. Modified: tomcat/trunk/test/org/apache/catalina/filters/TesterHttpServletResponse.java URL: http://svn.apache.org/viewvc/tomcat/trunk/test/org/apache/catalina/filters/TesterHttpServletResponse.java?rev=1796186&r1=1796185&r2=1796186&view=diff ============================================================================== --- tomcat/trunk/test/org/apache/catalina/filters/TesterHttpServletResponse.java (original) +++ tomcat/trunk/test/org/apache/catalina/filters/TesterHttpServletResponse.java Thu May 25 20:05:55 2017 @@ -24,6 +24,8 @@ import java.util.ArrayList; import java.util.Collection; import java.util.List; import java.util.Locale; +import java.util.Map; +import java.util.function.Supplier; import javax.servlet.ServletOutputStream; import javax.servlet.ServletResponse; @@ -247,4 +249,6 @@ public class TesterHttpServletResponse i public void setStatus(int status, String message) {/* NOOP */} @Override public void setContentLengthLong(long length) {/* NOOP */} + @Override + public void setTrailerFields(Supplier<Map<String, String>> supplier) { /* NOOP */ } } Modified: tomcat/trunk/test/org/apache/coyote/http2/TestStream.java URL: http://svn.apache.org/viewvc/tomcat/trunk/test/org/apache/coyote/http2/TestStream.java?rev=1796186&r1=1796185&r2=1796186&view=diff ============================================================================== --- tomcat/trunk/test/org/apache/coyote/http2/TestStream.java (original) +++ tomcat/trunk/test/org/apache/coyote/http2/TestStream.java Thu May 25 20:05:55 2017 @@ -30,6 +30,9 @@ import org.junit.Test; import org.apache.catalina.Context; import org.apache.catalina.startup.Tomcat; +import trailers.ResponseTrailers; + + public class TestStream extends Http2TestBase { /* @@ -73,6 +76,53 @@ public class TestStream extends Http2Tes "3-EndOfStream\n", output.getTrace()); } + + @Test + public void testResponseTrailerFields() throws Exception { + + enableHttp2(); + + Tomcat tomcat = getTomcatInstance(); + + Context ctxt = tomcat.addContext("", null); + Tomcat.addServlet(ctxt, "simple", new SimpleServlet()); + ctxt.addServletMappingDecoded("/simple", "simple"); + Tomcat.addServlet(ctxt, "trailers", new ResponseTrailers()); + ctxt.addServletMappingDecoded("/trailers", "trailers"); + + tomcat.start(); + + openClientConnection(); + doHttpUpgrade(); + sendClientPreface(); + validateHttp2InitialResponse(); + + byte[] frameHeader = new byte[9]; + ByteBuffer headersPayload = ByteBuffer.allocate(128); + buildGetRequest(frameHeader, headersPayload, null, 3, "/trailers"); + writeFrame(frameHeader, headersPayload); + + // Headers + parser.readFrame(true); + // Body + parser.readFrame(true); + // Trailers + parser.readFrame(true); + + Assert.assertEquals( + "3-HeadersStart\n" + + "3-Header-[:status]-[200]\n" + + "3-Header-[content-type]-[text/plain;charset=UTF-8]\n" + + "3-Header-[date]-[Wed, 11 Nov 2015 19:18:42 GMT]\n" + + "3-HeadersEnd\n" + + "3-Body-43\n" + + "3-HeadersStart\n" + + "3-Header-[x-trailer-2]-[Trailer value two]\n" + + "3-Header-[x-trailer-1]-[Trailer value one]\n" + + "3-HeadersEnd\n" + + "3-EndOfStream\n", output.getTrace()); + } + private static final class PathParam extends HttpServlet { Modified: tomcat/trunk/webapps/docs/changelog.xml URL: http://svn.apache.org/viewvc/tomcat/trunk/webapps/docs/changelog.xml?rev=1796186&r1=1796185&r2=1796186&view=diff ============================================================================== --- tomcat/trunk/webapps/docs/changelog.xml (original) +++ tomcat/trunk/webapps/docs/changelog.xml Thu May 25 20:05:55 2017 @@ -61,6 +61,10 @@ <bug>61101</bug>: CORS filter should set Vary header in response. Submitted by Rick Riemer. (remm) </fix> + <update> + Update the Servlet 4.0 implementation to add support for setting + trailer fields for HTTP responses. (markt) + </update> </changelog> </subsection> <subsection name="Coyote"> Added: tomcat/trunk/webapps/examples/WEB-INF/classes/trailers/ResponseTrailers.java URL: http://svn.apache.org/viewvc/tomcat/trunk/webapps/examples/WEB-INF/classes/trailers/ResponseTrailers.java?rev=1796186&view=auto ============================================================================== --- tomcat/trunk/webapps/examples/WEB-INF/classes/trailers/ResponseTrailers.java (added) +++ tomcat/trunk/webapps/examples/WEB-INF/classes/trailers/ResponseTrailers.java Thu May 25 20:05:55 2017 @@ -0,0 +1,68 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package trailers; + +import java.io.IOException; +import java.io.PrintWriter; +import java.util.HashMap; +import java.util.Map; +import java.util.function.Supplier; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +/** + * This example writes some trailer fields to the HTTP response. + */ +public class ResponseTrailers extends HttpServlet { + + private static final long serialVersionUID = 1L; + private static final Supplier<Map<String,String>> TRAILER_FIELD_SUPPLIER = + new TrailerFieldSupplier(); + + + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) + throws ServletException, IOException { + + resp.setTrailerFields(TRAILER_FIELD_SUPPLIER); + resp.setContentType("text/plain"); + resp.setCharacterEncoding("UTF-8"); + + PrintWriter pw = resp.getWriter(); + + pw.print("This reponse should include trailer fields."); + } + + + private static class TrailerFieldSupplier implements Supplier<Map<String,String>> { + + private static final Map<String,String> trailerFields = new HashMap<>(); + + static { + trailerFields.put("x-trailer-1", "Trailer value one"); + trailerFields.put("x-trailer-2", "Trailer value two"); + } + + @Override + public Map<String, String> get() { + return trailerFields; + } + } +} Propchange: tomcat/trunk/webapps/examples/WEB-INF/classes/trailers/ResponseTrailers.java ------------------------------------------------------------------------------ svn:eol-style = native Modified: tomcat/trunk/webapps/examples/WEB-INF/web.xml URL: http://svn.apache.org/viewvc/tomcat/trunk/webapps/examples/WEB-INF/web.xml?rev=1796186&r1=1796185&r2=1796186&view=diff ============================================================================== --- tomcat/trunk/webapps/examples/WEB-INF/web.xml (original) +++ tomcat/trunk/webapps/examples/WEB-INF/web.xml Thu May 25 20:05:55 2017 @@ -389,6 +389,16 @@ <url-pattern>/servlets/serverpush/simpleimage</url-pattern> </servlet-mapping> + <!-- Trailer examples --> + <servlet> + <servlet-name>responsetrailer</servlet-name> + <servlet-class>trailers.ResponseTrailers</servlet-class> + </servlet> + <servlet-mapping> + <servlet-name>responsetrailer</servlet-name> + <url-pattern>/servlets/trailers/response</url-pattern> + </servlet-mapping> + <welcome-file-list> <welcome-file>index.html</welcome-file> <welcome-file>index.xhtml</welcome-file> Modified: tomcat/trunk/webapps/examples/servlets/index.html URL: http://svn.apache.org/viewvc/tomcat/trunk/webapps/examples/servlets/index.html?rev=1796186&r1=1796185&r2=1796186&view=diff ============================================================================== --- tomcat/trunk/webapps/examples/servlets/index.html (original) +++ tomcat/trunk/webapps/examples/servlets/index.html Thu May 25 20:05:55 2017 @@ -176,6 +176,17 @@ for clarity.</p> <td style="width: 30%;"></td> </tr> +<tr> + <th colspan="3">Servlet 4.0 Trailer Field examples</th> +</tr> +<tr> + <td>Response trailer fields</td> + <td style="width: 30%;"> + <a href="trailers/response"><img src="images/execute.gif" alt=""> Execute</a> + </td> + <td style="width: 30%;"></td> +</tr> + </table> </body> --------------------------------------------------------------------- To unsubscribe, e-mail: dev-unsubscr...@tomcat.apache.org For additional commands, e-mail: dev-h...@tomcat.apache.org