This is an automated email from the ASF dual-hosted git repository. markt pushed a commit to branch main in repository https://gitbox.apache.org/repos/asf/tomcat.git
commit 3f37960753a54e9cd500268c05f117ca1701e5d6 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 21fc8f1138..66dd7cab47 100644 --- a/java/org/apache/catalina/connector/Response.java +++ b/java/org/apache/catalina/connector/Response.java @@ -1037,6 +1037,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 b09b4731a8..dcc4ee059d 100644 --- a/java/org/apache/catalina/connector/ResponseFacade.java +++ b/java/org/apache/catalina/connector/ResponseFacade.java @@ -245,6 +245,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 21a7ef2304..30e4e12bd5 100644 --- a/java/org/apache/coyote/AbstractProcessor.java +++ b/java/org/apache/coyote/AbstractProcessor.java @@ -393,6 +393,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 { @@ -734,6 +742,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 15531f7c39..2b233fb82c 100644 --- a/java/org/apache/coyote/ActionCode.java +++ b/java/org/apache/coyote/ActionCode.java @@ -265,5 +265,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 1c568b05aa..9563c2b592 100644 --- a/java/org/apache/coyote/ajp/AjpProcessor.java +++ b/java/org/apache/coyote/ajp/AjpProcessor.java @@ -1057,6 +1057,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 76fdb0d126..07381f1b50 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 dac9e8e419..7d1b2e0431 100644 --- a/java/org/apache/coyote/http11/Http11Processor.java +++ b/java/org/apache/coyote/http11/Http11Processor.java @@ -1242,6 +1242,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 fdc8c4b160..c99a1691b7 100644 --- a/java/org/apache/coyote/http2/StreamProcessor.java +++ b/java/org/apache/coyote/http2/StreamProcessor.java @@ -268,6 +268,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..c1fad26110 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(StandardCharsets.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