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

Reply via email to