Skip to content

Commit

Permalink
Implement #8057 103 Early Hint
Browse files Browse the repository at this point in the history
Implemented 103 early hint, but is failing for HTTP>=2
  • Loading branch information
gregw committed May 30, 2022
1 parent 8b06eb0 commit a3bbbc0
Show file tree
Hide file tree
Showing 6 changed files with 134 additions and 18 deletions.
Expand Up @@ -291,6 +291,7 @@ public void resetResponse()
{
responseState = State.PENDING;
responseFailure = null;
response.clearHeaders();
}
}

Expand Down
Expand Up @@ -87,6 +87,11 @@ public HttpFields getHeaders()
return headers.asImmutable();
}

public void clearHeaders()
{
headers.clear();
}

public HttpResponse addHeader(HttpField header)
{
headers.add(header);
Expand Down
16 changes: 11 additions & 5 deletions jetty-http/src/main/java/org/eclipse/jetty/http/HttpGenerator.java
Expand Up @@ -388,12 +388,18 @@ public Result generateResponse(MetaData.Response info, boolean head, ByteBuffer
if (status >= 100 && status < 200)
{
_noContentResponse = true;

if (status != HttpStatus.SWITCHING_PROTOCOLS_101)
switch(status)
{
header.put(HttpTokens.CRLF);
_state = State.COMPLETING_1XX;
return Result.FLUSH;
case HttpStatus.SWITCHING_PROTOCOLS_101:
break;
case HttpStatus.EARLY_HINT_103:
generateHeaders(header, content, last);
_state = State.COMPLETING_1XX;
return Result.FLUSH;
default:
header.put(HttpTokens.CRLF);
_state = State.COMPLETING_1XX;
return Result.FLUSH;
}
}
else if (status == HttpStatus.NO_CONTENT_204 || status == HttpStatus.NOT_MODIFIED_304)
Expand Down
Expand Up @@ -25,6 +25,7 @@ public class HttpStatus
public static final int CONTINUE_100 = 100;
public static final int SWITCHING_PROTOCOLS_101 = 101;
public static final int PROCESSING_102 = 102;
public static final int EARLY_HINT_103 = 103;

public static final int OK_200 = 200;
public static final int CREATED_201 = 201;
Expand Down Expand Up @@ -103,6 +104,7 @@ public enum Code
CONTINUE(CONTINUE_100, "Continue"),
SWITCHING_PROTOCOLS(SWITCHING_PROTOCOLS_101, "Switching Protocols"),
PROCESSING(PROCESSING_102, "Processing"),
EARLY_HINT(EARLY_HINT_103, "Early Hint"),

OK(OK_200, "OK"),
CREATED(CREATED_201, "Created"),
Expand Down
20 changes: 20 additions & 0 deletions jetty-server/src/main/java/org/eclipse/jetty/server/Response.java
Expand Up @@ -490,6 +490,9 @@ public void sendError(int code, String message) throws IOException
case HttpStatus.PROCESSING_102:
sendProcessing();
break;
case HttpStatus.EARLY_HINT_103:
sendEarlyHint();
break;
default:
_channel.getState().sendError(code, message);
break;
Expand All @@ -514,6 +517,23 @@ public void sendProcessing() throws IOException
}
}

/**
* Sends a 102-Processing response.
* If the connection is an HTTP connection, the version is 1.1 and the
* request has a Expect header starting with 102, then a 102 response is
* sent. This indicates that the request still be processed and real response
* can still be sent. This method is called by sendError if it is passed 102.
*
* @throws IOException if unable to send the 102 response
* @see javax.servlet.http.HttpServletResponse#sendError(int)
*/
public void sendEarlyHint() throws IOException
{
if (!isCommitted())
_channel.sendResponse(new MetaData.Response(_channel.getRequest().getHttpVersion(), HttpStatus.EARLY_HINT_103,
_channel.getResponse()._fields.asImmutable()), null, true);
}

/**
* Sends a response with one of the 300 series redirection codes.
*
Expand Down
Expand Up @@ -14,6 +14,8 @@
package org.eclipse.jetty.http.client;

import java.io.IOException;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;
Expand Down Expand Up @@ -44,6 +46,7 @@

import static org.eclipse.jetty.http.client.Transport.FCGI;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.contains;
import static org.hamcrest.Matchers.is;
import static org.junit.jupiter.api.Assertions.assertTrue;

Expand Down Expand Up @@ -127,8 +130,8 @@ public void test102Processing(Transport transport) throws Exception
public void handle(String target, Request jettyRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException
{
jettyRequest.setHandled(true);
response.sendError(102);
response.sendError(102);
response.sendError(HttpStatus.PROCESSING_102);
response.sendError(HttpStatus.PROCESSING_102);
response.setStatus(200);
response.getOutputStream().print("OK");
}
Expand All @@ -147,23 +150,14 @@ public String getName()
@Override
public boolean accept(org.eclipse.jetty.client.api.Request request, Response response)
{
System.err.println("accept: " + request);
System.err.println(response.getStatus());

return response.getStatus() == 102;
return response.getStatus() == HttpStatus.PROCESSING_102;
}

@Override
public Response.Listener getResponseListener()
{
return new Response.Listener()
{
@Override
public void onBegin(Response response)
{
System.err.println("onBegin " + response);
Response.Listener.super.onBegin(response);
}
@Override
public void onSuccess(Response response)
{
Expand Down Expand Up @@ -195,7 +189,6 @@ public void onSuccess(Response response)
@Override
public void onComplete(Result result)
{
System.err.println("COMPLETE " + result.getResponse());
response.set(result.getResponse());
complete.countDown();
}
Expand All @@ -209,7 +202,96 @@ public void onComplete(Result result)
assertTrue(complete.await(10, TimeUnit.SECONDS));
assertThat(response.get().getStatus(), is(200));
assertThat(listener.getContentAsString(), is("OK"));
}

@ParameterizedTest
@ArgumentsSource(TransportProvider.class)
public void test103EarlyHint(Transport transport) throws Exception
{
init(transport);
scenario.start(new AbstractHandler()
{
@Override
public void handle(String target, Request jettyRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException
{
jettyRequest.setHandled(true);
response.setHeader("Hint", "one");
response.sendError(HttpStatus.EARLY_HINT_103);
response.setHeader("Hint", "two");
response.sendError(HttpStatus.EARLY_HINT_103);
response.setHeader("Hint", "three");
response.setStatus(200);
response.getOutputStream().print("OK");
}
});
long idleTimeout = 10000;
scenario.setRequestIdleTimeout(idleTimeout);

List<String> hints = new CopyOnWriteArrayList<>();
scenario.client.getProtocolHandlers().put(new ProtocolHandler()
{
@Override
public String getName()
{
return "EarlyHint";
}

@Override
public boolean accept(org.eclipse.jetty.client.api.Request request, Response response)
{
return response.getStatus() == HttpStatus.EARLY_HINT_103;
}

@Override
public Response.Listener getResponseListener()
{
return new Response.Listener()
{
@Override
public void onSuccess(Response response)
{
org.eclipse.jetty.client.api.Request request = response.getRequest();
HttpConversation conversation = ((HttpRequest)request).getConversation();
// Reset the conversation listeners, since we are going to receive another response code
conversation.updateResponseListeners(null);

HttpExchange exchange = conversation.getExchanges().peekLast();
if (exchange != null && response.getStatus() == HttpStatus.EARLY_HINT_103)
{
// All good, continue.
hints.add(response.getHeaders().get("Hint"));
exchange.resetResponse();
exchange.proceed(null);
}
else
{
throw new IllegalStateException("should not have accepted");
}
}
};
}
});

CountDownLatch complete = new CountDownLatch(1);
AtomicReference<Response> response = new AtomicReference<>();
BufferingResponseListener listener = new BufferingResponseListener()
{
@Override
public void onComplete(Result result)
{
hints.add(result.getResponse().getHeaders().get("Hint"));
response.set(result.getResponse());
complete.countDown();
}
};
scenario.client.newRequest(scenario.newURI())
.method("GET")
.timeout(10, TimeUnit.SECONDS)
.send(listener);

assertTrue(complete.await(10, TimeUnit.SECONDS));
assertThat(response.get().getStatus(), is(200));
assertThat(listener.getContentAsString(), is("OK"));
assertThat(hints, contains("one", "two", "three"));
}
}

0 comments on commit a3bbbc0

Please sign in to comment.