This is an automated email from the ASF dual-hosted git repository. markt pushed a commit to branch 10.1.x in repository https://gitbox.apache.org/repos/asf/tomcat.git
commit 2922f53f7d2257b30c009fbda7d1817c549ef59c Author: Mark Thomas <ma...@apache.org> AuthorDate: Thu Jul 4 15:48:18 2024 +0100 Implement early hints for HTTP/1.1 --- java/org/apache/catalina/connector/Response.java | 14 +++++++ .../apache/catalina/connector/ResponseFacade.java | 4 ++ java/org/apache/coyote/AbstractProcessor.java | 11 +++++ java/org/apache/coyote/ActionCode.java | 7 +++- java/org/apache/coyote/ajp/AjpProcessor.java | 7 ++++ .../apache/coyote/http11/Http11OutputBuffer.java | 3 ++ java/org/apache/coyote/http11/Http11Processor.java | 8 ++++ java/org/apache/coyote/http2/StreamProcessor.java | 7 ++++ .../apache/coyote/http11/TestHttp11Processor.java | 49 ++++++++++++++++++++++ 9 files changed, 109 insertions(+), 1 deletion(-) diff --git a/java/org/apache/catalina/connector/Response.java b/java/org/apache/catalina/connector/Response.java index b5906ed7f8..44a94ace51 100644 --- a/java/org/apache/catalina/connector/Response.java +++ b/java/org/apache/catalina/connector/Response.java @@ -1061,6 +1061,20 @@ public class Response implements HttpServletResponse { } + public void sendEarlyHints() { + if (isCommitted()) { + return; + } + + // Ignore any call from an included servlet + if (included) { + return; + } + + getCoyoteResponse().action(ActionCode.EARLY_HINTS, null); + } + + @Override public void sendError(int status) throws IOException { sendError(status, null); diff --git a/java/org/apache/catalina/connector/ResponseFacade.java b/java/org/apache/catalina/connector/ResponseFacade.java index 5ad4c0778b..8bed33742c 100644 --- a/java/org/apache/catalina/connector/ResponseFacade.java +++ b/java/org/apache/catalina/connector/ResponseFacade.java @@ -324,6 +324,10 @@ public class ResponseFacade implements HttpServletResponse { } + public void sendEarlyHints() { + response.sendEarlyHints(); + } + @Override public void sendError(int sc, String msg) throws IOException { checkCommitted("coyoteResponse.sendError.ise"); diff --git a/java/org/apache/coyote/AbstractProcessor.java b/java/org/apache/coyote/AbstractProcessor.java index 49a7882712..8a731014bd 100644 --- a/java/org/apache/coyote/AbstractProcessor.java +++ b/java/org/apache/coyote/AbstractProcessor.java @@ -394,6 +394,14 @@ public abstract class AbstractProcessor extends AbstractProcessorLight implement ack((ContinueResponseTiming) param); break; } + case EARLY_HINTS: { + try { + earlyHints(); + } catch (IOException e) { + handleIOException(e); + } + break; + } case CLIENT_FLUSH: { action(ActionCode.COMMIT, null); try { @@ -742,6 +750,9 @@ public abstract class AbstractProcessor extends AbstractProcessorLight implement protected abstract void ack(ContinueResponseTiming continueResponseTiming); + protected abstract void earlyHints() throws IOException; + + /** * Callback to write data from the buffer. * @throws IOException IO exception during the write diff --git a/java/org/apache/coyote/ActionCode.java b/java/org/apache/coyote/ActionCode.java index 6a24345ed2..62d236633a 100644 --- a/java/org/apache/coyote/ActionCode.java +++ b/java/org/apache/coyote/ActionCode.java @@ -275,5 +275,10 @@ public enum ActionCode { /** * Obtain the servlet connection instance for the network connection supporting the current request. */ - SERVLET_CONNECTION + SERVLET_CONNECTION, + + /** + * Send an RFC 8297 Early Hints informational response. + */ + EARLY_HINTS } diff --git a/java/org/apache/coyote/ajp/AjpProcessor.java b/java/org/apache/coyote/ajp/AjpProcessor.java index caf2d5700e..f879a3f84d 100644 --- a/java/org/apache/coyote/ajp/AjpProcessor.java +++ b/java/org/apache/coyote/ajp/AjpProcessor.java @@ -1054,6 +1054,13 @@ public class AjpProcessor extends AbstractProcessor { } + @Override + protected void earlyHints() throws IOException { + // TODO Auto-generated method stub + // NO-OP for now + } + + @Override protected final int available(boolean doRead) { if (endOfStream) { diff --git a/java/org/apache/coyote/http11/Http11OutputBuffer.java b/java/org/apache/coyote/http11/Http11OutputBuffer.java index 2c51cdfb45..6967c30996 100644 --- a/java/org/apache/coyote/http11/Http11OutputBuffer.java +++ b/java/org/apache/coyote/http11/Http11OutputBuffer.java @@ -300,7 +300,10 @@ public class Http11OutputBuffer implements HttpOutputBuffer { */ protected void commit() throws IOException { response.setCommitted(true); + writeHeaders(); + } + protected void writeHeaders() throws IOException { if (headerBuffer.position() > 0) { // Sending the response header buffer headerBuffer.flip(); diff --git a/java/org/apache/coyote/http11/Http11Processor.java b/java/org/apache/coyote/http11/Http11Processor.java index 3542625fb7..7522962304 100644 --- a/java/org/apache/coyote/http11/Http11Processor.java +++ b/java/org/apache/coyote/http11/Http11Processor.java @@ -1244,6 +1244,14 @@ public class Http11Processor extends AbstractProcessor { } + @Override + protected void earlyHints() throws IOException { + writeHeaders(103, response.getMimeHeaders()); + outputBuffer.writeHeaders(); + outputBuffer.resetHeaderBuffer(); + } + + @Override protected final void flush() throws IOException { outputBuffer.flush(); diff --git a/java/org/apache/coyote/http2/StreamProcessor.java b/java/org/apache/coyote/http2/StreamProcessor.java index 4e5645736e..3f3cde6f50 100644 --- a/java/org/apache/coyote/http2/StreamProcessor.java +++ b/java/org/apache/coyote/http2/StreamProcessor.java @@ -261,6 +261,13 @@ class StreamProcessor extends AbstractProcessor { } + @Override + protected void earlyHints() throws IOException { + // TODO Auto-generated method stub + // NO-OP for now + } + + @Override protected final void flush() throws IOException { stream.getOutputBuffer().flush(); diff --git a/test/org/apache/coyote/http11/TestHttp11Processor.java b/test/org/apache/coyote/http11/TestHttp11Processor.java index 857d51185d..492df00050 100644 --- a/test/org/apache/coyote/http11/TestHttp11Processor.java +++ b/test/org/apache/coyote/http11/TestHttp11Processor.java @@ -53,6 +53,7 @@ import org.junit.Test; import org.apache.catalina.Context; import org.apache.catalina.Wrapper; import org.apache.catalina.connector.Connector; +import org.apache.catalina.connector.ResponseFacade; import org.apache.catalina.startup.SimpleHttpClient; import org.apache.catalina.startup.TesterServlet; import org.apache.catalina.startup.Tomcat; @@ -1912,4 +1913,52 @@ public class TestHttp11Processor extends TomcatBaseTest { } } + + + @Test + public void testEarlyHints() throws Exception { + Tomcat tomcat = getTomcatInstance(); + + // No file system docBase required + Context ctx = getProgrammaticRootContext(); + + // Add servlet + Tomcat.addServlet(ctx, "EarlyHintsServlet", new EarlyHintsServlet()); + ctx.addServletMappingDecoded("/ehs", "EarlyHintsServlet"); + + tomcat.start(); + + String request = "GET /ehs HTTP/1.1" + SimpleHttpClient.CRLF + + "Host: localhost:" + getPort() + SimpleHttpClient.CRLF + + SimpleHttpClient.CRLF; + + Client client = new Client(tomcat.getConnector().getLocalPort()); + client.setRequest(new String[] { request }); + + client.connect(600000, 600000); + client.processRequest(false); + + Assert.assertEquals(103, client.getStatusCode()); + + client.readResponse(false); + Assert.assertEquals(HttpServletResponse.SC_OK, client.getStatusCode()); + } + + + private static class EarlyHintsServlet extends HttpServlet { + + private static final long serialVersionUID = 1L; + + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { + resp.addHeader("Link", "</style.css>; rel=preload; as=style"); + + ((ResponseFacade) resp).sendEarlyHints(); + + resp.setCharacterEncoding("UTF-8"); + resp.setContentType("text/plain"); + + resp.getWriter().write("OK"); + } + } } --------------------------------------------------------------------- To unsubscribe, e-mail: dev-unsubscr...@tomcat.apache.org For additional commands, e-mail: dev-h...@tomcat.apache.org