Skip to content

Commit

Permalink
Allow applications to trigger sending of 103 early hints (#764)
Browse files Browse the repository at this point in the history
* Allow applications to trigger sending of 103 early hints

* Add unit tests
  • Loading branch information
ChristopherSchultz authored and markt-asf committed Nov 6, 2024
1 parent dd0d83b commit 6386e07
Show file tree
Hide file tree
Showing 4 changed files with 150 additions and 12 deletions.
40 changes: 33 additions & 7 deletions java/org/apache/catalina/connector/Response.java
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,8 @@ public class Response implements HttpServletResponse {

private static final MediaTypeCache MEDIA_TYPE_CACHE = new MediaTypeCache(100);

protected static final int SC_EARLY_HINTS = 103;

/**
* Coyote response.
*/
Expand Down Expand Up @@ -1051,12 +1053,32 @@ public void sendEarlyHints() {
}


/**
* {@inheritDoc}
* <p>
* Calling <code>sendError</code> with a status code of 103 differs from the usual
* behavior. Sending 103 will trigger the container to send a "103 Early Hints" informational response including all
* current headers. The application can continue to use the request and response after calling sendError with a 103
* status code, including triggering a more typical response of any type.
* <p>
* Starting with Tomcat 12, applications should use {@link #sendEarlyHints}.
*/
@Override
public void sendError(int status) throws IOException {
sendError(status, null);
}


/**
* {@inheritDoc}
* <p>
* Calling <code>sendError</code> with a status code of 103 differs from the usual
* behavior. Sending 103 will trigger the container to send a "103 Early Hints" informational response including all
* current headers. The application can continue to use the request and response after calling sendError with a 103
* status code, including triggering a more typical response of any type.
* <p>
* Starting with Tomcat 12, applications should use {@link #sendEarlyHints}.
*/
@Override
public void sendError(int status, String message) throws IOException {

Expand All @@ -1069,16 +1091,20 @@ public void sendError(int status, String message) throws IOException {
return;
}

setError();
if (SC_EARLY_HINTS == status) {
sendEarlyHints();
} else {
setError();

getCoyoteResponse().setStatus(status);
getCoyoteResponse().setMessage(message);
getCoyoteResponse().setStatus(status);
getCoyoteResponse().setMessage(message);

// Clear any data content that has been buffered
resetBuffer();
// Clear any data content that has been buffered
resetBuffer();

// Cause the response to be finished (from the application perspective)
setSuspended(true);
// Cause the response to be finished (from the application perspective)
setSuspended(true);
}
}


Expand Down
36 changes: 32 additions & 4 deletions java/org/apache/catalina/connector/ResponseFacade.java
Original file line number Diff line number Diff line change
Expand Up @@ -249,19 +249,47 @@ public void sendEarlyHints() {
response.sendEarlyHints();
}

/**
* {@inheritDoc}
* <p>
* Calling <code>sendError</code> with a status code of 103 differs from the usual
* behavior. Sending 103 will trigger the container to send a "103 Early Hints" informational response including all
* current headers. The application can continue to use the request and response after calling sendError with a 103
* status code, including triggering a more typical response of any type.
* <p>
* Starting with Tomcat 12, applications should use {@link #sendEarlyHints}.
*/
@Override
public void sendError(int sc, String msg) throws IOException {
checkCommitted("coyoteResponse.sendError.ise");
response.setAppCommitted(true);
response.sendError(sc, msg);
if (Response.SC_EARLY_HINTS == sc) {
sendEarlyHints();
} else {
response.setAppCommitted(true);
response.sendError(sc, msg);
}
}


/**
* {@inheritDoc}
* <p>
* Calling <code>sendError</code> with a status code of 103 differs from the usual
* behavior. Sending 103 will trigger the container to send a "103 Early Hints" informational response including all
* current headers. The application can continue to use the request and response after calling sendError with a 103
* status code, including triggering a more typical response of any type.
* <p>
* Starting with Tomcat 12, applications should use {@link #sendEarlyHints}.
*/
@Override
public void sendError(int sc) throws IOException {
checkCommitted("coyoteResponse.sendError.ise");
response.setAppCommitted(true);
response.sendError(sc);
if (Response.SC_EARLY_HINTS == sc) {
sendEarlyHints();
} else {
response.setAppCommitted(true);
response.sendError(sc);
}
}


Expand Down
81 changes: 80 additions & 1 deletion test/org/apache/coyote/http11/TestHttp11Processor.java
Original file line number Diff line number Diff line change
Expand Up @@ -1946,16 +1946,95 @@ public void testEarlyHints() throws Exception {
Assert.assertEquals(HttpServletResponse.SC_OK, client.getStatusCode());
}

@Test
public void testEarlyHintsSendError() throws Exception {
Tomcat tomcat = getTomcatInstance();

// No file system docBase required
Context ctx = getProgrammaticRootContext();

// Add servlet
Tomcat.addServlet(ctx, "EarlyHintsServlet", new EarlyHintsServlet(true, null));
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());
}


@Test
public void testEarlyHintsSendErrorWithMessage() throws Exception {
Tomcat tomcat = getTomcatInstance();

// No file system docBase required
Context ctx = getProgrammaticRootContext();

// Add servlet
Tomcat.addServlet(ctx, "EarlyHintsServlet", new EarlyHintsServlet(true, "ignored"));
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;

private final boolean useSendError;
private final String errorString;

EarlyHintsServlet() {
this(false, null);
}

EarlyHintsServlet(boolean useSendError, String errorString) {
this.useSendError = useSendError;
this.errorString = errorString;
}
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
resp.addHeader("Link", "</style.css>; rel=preload; as=style");

((ResponseFacade) resp).sendEarlyHints();
if (useSendError) {
if (null == errorString) {
resp.sendError(103);
} else {
resp.sendError(103, errorString);
}
} else {
((ResponseFacade) resp).sendEarlyHints();
}

resp.setCharacterEncoding(StandardCharsets.UTF_8);
resp.setContentType("text/plain");
Expand Down
5 changes: 5 additions & 0 deletions webapps/docs/changelog.xml
Original file line number Diff line number Diff line change
Expand Up @@ -246,6 +246,11 @@
as a number of libraries and JRE features depend on this being non-null
even when a SecurityManager is not is use. (markt)
</fix>
<add>
All applications to send an early hints informational response by
calling <code>HttpServletResponse.sendError()</code> with a status code
of 103. (schultz)
</add>
</changelog>
</subsection>
<subsection name="Coyote">
Expand Down

0 comments on commit 6386e07

Please sign in to comment.