Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement #8057 103 Early Hint #8058

Merged
merged 11 commits into from Jun 1, 2022
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"),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's add also HttpHeader.Link from RFC 8288.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added in javadoc


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)
*/
gregw marked this conversation as resolved.
Show resolved Hide resolved
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