Skip to content

Commit

Permalink
Implement #8057 103 Early Hint (#8058)
Browse files Browse the repository at this point in the history
Co-authored-by: Ludovic Orban <lorban@bitronix.be>
Co-authored-by: Simone Bordet <simone.bordet@gmail.com>
  • Loading branch information
3 people committed Jun 1, 2022
1 parent 2632748 commit 7a1c165
Show file tree
Hide file tree
Showing 17 changed files with 320 additions and 43 deletions.
Expand Up @@ -291,6 +291,7 @@ public void resetResponse()
{
responseState = State.PENDING;
responseFailure = null;
response.clearHeaders();
}
}

Expand Down
Expand Up @@ -411,9 +411,8 @@ protected boolean responseSuccess(HttpExchange exchange)
ResponseNotifier notifier = getHttpDestination().getResponseNotifier();
notifier.notifySuccess(listeners, response);

// Special case for 100 Continue that cannot
// be handled by the ContinueProtocolHandler.
if (exchange.getResponse().getStatus() == HttpStatus.CONTINUE_100)
// Interim responses do not terminate the exchange.
if (HttpStatus.isInterim(exchange.getResponse().getStatus()))
return true;

// Mark atomically the response as terminated, with
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
Expand Up @@ -187,6 +187,7 @@ private void process()
}
else if (read == 0)
{
assert networkBuffer.isEmpty();
releaseNetworkBuffer();
fillInterested();
return;
Expand Down Expand Up @@ -245,18 +246,21 @@ private boolean parse()
this.method = null;
if (getHttpChannel().isTunnel(method, status))
return true;
}

if (networkBuffer.isEmpty())
return false;
if (networkBuffer.isEmpty())
return false;

if (complete)
{
if (LOG.isDebugEnabled())
LOG.debug("Discarding unexpected content after response: {}", networkBuffer);
networkBuffer.clear();
if (!HttpStatus.isInformational(status))
{
if (LOG.isDebugEnabled())
LOG.debug("Discarding unexpected content after response {}: {}", status, networkBuffer);
networkBuffer.clear();
}
return false;
}

if (networkBuffer.isEmpty())
return false;
}
}

Expand Down Expand Up @@ -372,7 +376,7 @@ public boolean messageComplete()
}

int status = exchange.getResponse().getStatus();
if (status != HttpStatus.CONTINUE_100)
if (!HttpStatus.isInterim(status))
{
inMessages.increment();
complete = true;
Expand Down
18 changes: 12 additions & 6 deletions jetty-http/src/main/java/org/eclipse/jetty/http/HttpGenerator.java
Expand Up @@ -385,15 +385,21 @@ public Result generateResponse(MetaData.Response info, boolean head, ByteBuffer

// Handle 1xx and no content responses
int status = info.getStatus();
if (status >= 100 && status < 200)
if (HttpStatus.isInformational(status))
{
_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 @@ -88,6 +88,7 @@ public enum HttpHeader
AGE("Age"),
ALT_SVC("Alt-Svc"),
ETAG("ETag"),
LINK("Link"),
LOCATION("Location"),
PROXY_AUTHENTICATE("Proxy-Authenticate"),
RETRY_AFTER("Retry-After"),
Expand Down
13 changes: 13 additions & 0 deletions jetty-http/src/main/java/org/eclipse/jetty/http/HttpStatus.java
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 Expand Up @@ -339,6 +341,17 @@ public static boolean isInformational(int code)
return ((100 <= code) && (code <= 199));
}

/**
* Tests whether the status code is informational but not {@code 101 Switching Protocols}.
*
* @param code the code to test
* @return whether the status code is informational but not {@code 101 Switching Protocols}
*/
public static boolean isInterim(int code)
{
return isInformational(code) && code != HttpStatus.SWITCHING_PROTOCOLS_101;
}

/**
* Simple test against an code to determine if it falls into the
* <code>Success</code> message category as defined in the <a
Expand Down
Expand Up @@ -120,8 +120,7 @@ void onHeaders(Stream stream, HeadersFrame frame)
if (responseHeaders(exchange))
{
int status = response.getStatus();
boolean informational = HttpStatus.isInformational(status) && status != HttpStatus.SWITCHING_PROTOCOLS_101;
if (frame.isEndStream() || informational)
if (frame.isEndStream() || HttpStatus.isInterim(status))
responseSuccess(exchange);
}
else
Expand Down
Expand Up @@ -98,8 +98,7 @@ public void sendHeaders(MetaData.Request request, MetaData.Response response, By
boolean isHeadRequest = HttpMethod.HEAD.is(request.getMethod());
boolean hasContent = BufferUtil.hasContent(content) && !isHeadRequest;
int status = response.getStatus();
boolean interimResponse = status == HttpStatus.CONTINUE_100 || status == HttpStatus.PROCESSING_102;
if (interimResponse)
if (HttpStatus.isInterim(status))
{
// Must not commit interim responses.
if (hasContent)
Expand Down
Expand Up @@ -52,9 +52,11 @@ public void onResponse(HeadersFrame frame)
MetaData.Response response = (MetaData.Response)frame.getMetaData();
boolean valid;
if (response.getStatus() == HttpStatus.CONTINUE_100)
valid = validateAndUpdate(EnumSet.of(FrameState.INITIAL), FrameState.CONTINUE);
valid = validateAndUpdate(EnumSet.of(FrameState.INITIAL), FrameState.INFORMATIONAL);
else if (response.getStatus() == HttpStatus.EARLY_HINT_103)
valid = validateAndUpdate(EnumSet.of(FrameState.INITIAL, FrameState.HEADER, FrameState.INFORMATIONAL), FrameState.INFORMATIONAL);
else
valid = validateAndUpdate(EnumSet.of(FrameState.INITIAL, FrameState.CONTINUE), FrameState.HEADER);
valid = validateAndUpdate(EnumSet.of(FrameState.INITIAL, FrameState.INFORMATIONAL), FrameState.HEADER);
if (valid)
{
notIdle();
Expand Down
Expand Up @@ -315,7 +315,7 @@ public String toString()

protected enum FrameState
{
INITIAL, CONTINUE, HEADER, DATA, TRAILER, FAILED
INITIAL, INFORMATIONAL, HEADER, DATA, TRAILER, FAILED
}

private enum CloseState
Expand Down
Expand Up @@ -486,18 +486,18 @@ public void onHeaders(long streamId, HeadersFrame frame)
else if (metaData.isResponse())
{
MetaData.Response response = (MetaData.Response)metaData;
if (response.getStatus() != HttpStatus.CONTINUE_100)
if (HttpStatus.isInformational(response.getStatus()))
{
// Expect DATA frames now.
parserDataMode = true;
parser.setDataMode(true);
if (LOG.isDebugEnabled())
LOG.debug("switching to parserDataMode=true for response {} on {}", metaData, this);
LOG.debug("staying in parserDataMode=false for response {} on {}", metaData, this);
}
else
{
// Expect DATA frames now.
parserDataMode = true;
parser.setDataMode(true);
if (LOG.isDebugEnabled())
LOG.debug("staying in parserDataMode=false for response {} on {}", metaData, this);
LOG.debug("switching to parserDataMode=true for response {} on {}", metaData, this);
}
}
else
Expand Down
Expand Up @@ -89,8 +89,7 @@ public void onResponse(Stream.Client stream, HeadersFrame frame)
if (responseHeaders(exchange))
{
int status = response.getStatus();
boolean informational = HttpStatus.isInformational(status) && status != HttpStatus.SWITCHING_PROTOCOLS_101;
if (frame.isLast() || informational)
if (frame.isLast() || HttpStatus.isInterim(status))
responseSuccess(exchange);
else
stream.demand();
Expand Down
Expand Up @@ -71,8 +71,7 @@ private void sendHeaders(MetaData.Request request, MetaData.Response response, B
boolean isHeadRequest = HttpMethod.HEAD.is(request.getMethod());
boolean hasContent = BufferUtil.hasContent(content) && !isHeadRequest;
int status = response.getStatus();
boolean interimResponse = status == HttpStatus.CONTINUE_100 || status == HttpStatus.PROCESSING_102;
if (interimResponse)
if (HttpStatus.isInterim(status))
{
// Must not commit interim responses.
if (hasContent)
Expand Down
Expand Up @@ -1044,10 +1044,10 @@ protected boolean sendResponse(MetaData.Response response, ByteBuffer content, b
_combinedListener.onResponseBegin(_request);
_request.onResponseCommit();

// wrap callback to process 100 responses
// wrap callback to process informational responses
final int status = response.getStatus();
final Callback committed = (status < HttpStatus.OK_200 && status >= HttpStatus.CONTINUE_100)
? new Send100Callback(callback)
final Callback committed = HttpStatus.isInformational(status)
? new Send1XXCallback(callback)
: new SendCallback(callback, content, true, complete);

// committing write
Expand Down Expand Up @@ -1477,9 +1477,9 @@ public void failed(Throwable th)
}
}

private class Send100Callback extends SendCallback
private class Send1XXCallback extends SendCallback
{
private Send100Callback(Callback callback)
private Send1XXCallback(Callback callback)
{
super(callback, null, false, false);
}
Expand Down
25 changes: 22 additions & 3 deletions jetty-server/src/main/java/org/eclipse/jetty/server/Response.java
Expand Up @@ -470,6 +470,7 @@ public void sendError(int sc) throws IOException
* <p>In addition to the servlet standard handling, this method supports some additional codes:</p>
* <dl>
* <dt>102</dt><dd>Send a partial PROCESSING response and allow additional responses</dd>
* <dt>103</dt><dd>Send a partial EARLY_HINT response as per <a href="https://datatracker.ietf.org/doc/html/rfc8297">RFC8297</a></dd>
* <dt>-1</dt><dd>Abort the HttpChannel and close the connection/stream</dd>
* </dl>
* @param code The error code
Expand All @@ -490,6 +491,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 @@ -498,9 +502,8 @@ public void sendError(int code, String message) 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
* If the request had an 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
Expand All @@ -514,6 +517,22 @@ public void sendProcessing() throws IOException
}
}

/**
* Sends a 103 Early Hint response.
*
* Send a 103 response as per <a href="https://datatracker.ietf.org/doc/html/rfc8297">RFC8297</a>
* This method is called by sendError if it is passed 103.
*
* @throws IOException if unable to send the 103 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

0 comments on commit 7a1c165

Please sign in to comment.