Skip to content

Implement HTTP 103 Early Hints (RFC 8297) interim responses - #2298

Merged
carryel merged 8 commits into
eclipse-ee4j:mainfrom
BalusC:implement_sendEarlyHints
Jun 2, 2026
Merged

Implement HTTP 103 Early Hints (RFC 8297) interim responses#2298
carryel merged 8 commits into
eclipse-ee4j:mainfrom
BalusC:implement_sendEarlyHints

Conversation

@BalusC

@BalusC BalusC commented May 25, 2026

Copy link
Copy Markdown
Contributor

Add Response.sendEarlyHints() to send 103 Early Hints interim responses over HTTP/1.1, HTTP/2 and AJP. An interim response carries the headers currently set on the response (e.g. Link preload hints), does not commit the response, and may be sent multiple times before the final response. Inspired by Jetty 12 and Tomcat 12.

Prerequisite for a green build of eclipse-ee4j/mojarra#5748.

Core (modules/http)

  • New HttpStatus.EARLY_HINTS_103.
  • HttpResponsePacket holds a transient interimStatus separate from the final status; serializing an interim response leaves the final status untouched.
  • HttpCodecFilter serializes the interim status line and headers without marking the headers serialized, so they re-emit with the final response.
  • HttpServerFilter.encodeInitialLine refactored to share a single private helper with a (packet, status) overload — preserves CustomReasonPhrase on the final response only.
  • 100-Continue and 1xx interim responses now share a single interimStatus mechanism (removed setAcknowledgement/isAcknowledgement/acknowledged from HttpResponsePacket; the four parallel isAcknowledgement branches in HTTP/1.1, AJP and HTTP/2 codecs collapse into the interim branch). OutputBuffer.acknowledge() and writeInterimResponse() collapse into the latter.
  • HttpServerFilter no longer runs prepareResponse (which injects Date/Content-Type/Content-Length) on interim packets — those mutations belong to the final response.

HTTP/2 (modules/http2)

  • DefaultOutputSink emits the interim as a HEADERS frame (END_HEADERS, no END_STREAM) without committing the stream.
  • Http2ClientFilter accepts inbound interim 1xx responses instead of failing with PROTOCOL_ERROR.
  • DecoderUtils.finalizeKnownHeader had a missing return on case "expect": (and case "te":) causing every HTTP/2 Expect: 100-continue request to be rejected at decode time — fixed. HTTP/2 100-Continue now works end-to-end via the unified interim mechanism.
  • HttpServerFilter#prepareRequest uses the new Protocol#isAtLeast(HTTP_1_1) for the legacy-protocol check.

AJP (modules/http-ajp)

  • AjpHandlerFilter and AjpMessageUtils encode interim responses as SEND_HEADERS packets without committing — the existing 100-Continue mechanism generalised. Tomcat NO-OPs early hints over AJP; we implement because the AJP13 protocol does not forbid multiple SEND_HEADERS packets, and forwarding is the upstream connector's concern.

Servlet (modules/http-servlet)

  • HttpServletResponseImpl.sendEarlyHints() (Servlet 6.2). The Servlet 6.2 spec signature does not declare IOException; the (effectively unreachable) async write path's checked exception is logged and swallowed — matches glassfish web-core's ResponseFacade.
  • ServletHandler.AckActionImpl.acknowledge() no longer leaks status 100 onto the final response after the unification (regression test added).
  • jakarta.servlet-api bumped 6.1.0 → 6.2.0-M2.

Tests

  • EarlyHintsTest (HTTP/1.1), Http2EarlyHintsTest, AjpEarlyHintsTest — interim 103 carries Link, final response still has it.
  • Http2ContinueTest — HTTP/2 100-Continue end-to-end.
  • ServletHttpContinueTest regression for the auto-ack leak.
  • HttpContinueTest, BasicAjpTest.test100ContinuePost, ServletHttpContinueTest all still green after unification.

Add Response.sendEarlyHints() to send 103 Early Hints interim responses
over both HTTP/1.1 and HTTP/2. An interim response carries the headers
currently set on the response (e.g. Link preload hints), does not commit
the response, and may be sent multiple times before the final response.

Core (modules/http):
- Register HttpStatus.EARLY_HINTS_103.
- HttpResponsePacket holds a transient interimStatus, separate from the
  final status, so serializing an interim response leaves the final
  response's status untouched.
- HttpCodecFilter serializes the interim status line and headers without
  marking the headers serialized (new encodeMimeHeaders overload), so the
  same headers are re-emitted with the final response.
- OutputBuffer.writeInterimResponse() writes the interim header without
  committing.

HTTP/2 (modules/http2):
- DefaultOutputSink emits the interim response as a HEADERS frame
  (END_HEADERS, no END_STREAM) without committing the stream, reusing the
  interimStatus signal the same way the existing 100-Continue path spans
  both protocols. EncoderUtils.encodeUserHeaders gained a markSerialized
  flag so interim headers survive into the final response (HTTP/2 uses the
  same per-header serialized flag as HTTP/1.1).
- Http2ClientFilter now accepts inbound interim 1xx responses instead of
  failing with PROTOCOL_ERROR: a 1xx HEADERS block is decoded and
  delivered as a standalone interim response without advancing the
  stream's header/trailer state, so the final response is still parsed
  correctly. This also fixes inbound 100-Continue over HTTP/2.

http-server:
- Response.sendEarlyHints() sets the interim status and writes it, guarded
  to HTTP/1.1 and HTTP/2 only.

Tests: EarlyHintsTest (HTTP/1.1) and Http2EarlyHintsTest verify the 103
carries the Link header and that the same header is still present on the
final response; the full http2 suite and HttpContinueTest remain green.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Comment thread modules/http/src/main/java/org/glassfish/grizzly/http/HttpCodecFilter.java Outdated
@carryel

carryel commented May 27, 2026

Copy link
Copy Markdown
Contributor

Although Servlet spec 6.2 has not been released yet, how about adding it to HttpServletResponseImpl for the future?

Reference) https://github.com/jakartaee/servlet/pull/672/files

For example, it is as follows:

    /**
     * Sends a 103 response to the client using the current response headers. This method does not commit the response and
     * may be called multiple times before the response is committed. The current response headers may include some headers
     * that have been added automatcially by the container.
     * <p>
     * This method has no effect if called after the response has been committed.
     *
     * @since Servlet 6.2
     */
    public void sendEarlyHints() {
        if (response == null) {
            throw new IllegalStateException("Null response object");
        }
        return response.sendEarlyHints();
    }

@carryel

carryel commented May 27, 2026

Copy link
Copy Markdown
Contributor

I think the AJP Protocol also needs to be modified. It would be good if you referred to the section below and considered it in a similar manner.

if (httpResponsePacket.isAcknowledgement()) {
encodedBuffer.trim();
httpResponsePacket.acknowledged();
return encodedBuffer; // DO NOT MARK COMMITTED
}

if (httpResponsePacket.isAcknowledgement()) {
// If it's acknoledgment packet - don't encode the headers
// Serialize 0 num_headers
encodedBuffer = putShort(mm, encodedBuffer, 0);
} else {

@BalusC

BalusC commented May 27, 2026

Copy link
Copy Markdown
Contributor Author

Great feedback. Let me take a look.

@BalusC

BalusC commented May 27, 2026

Copy link
Copy Markdown
Contributor Author

AJP is going to be more work than anticipated because there is no foundation and it's even not implemented in Tomcat so it cannot be used as reference.

BalusC and others added 5 commits May 27, 2026 09:20
Mirrors the HTTP/1.1 path: AjpHandlerFilter routes interim packets
through a new isInterimResponse() branch (without committing the
response), and AjpMessageUtils encodes the interim status code with
the currently-set headers, skipping the auto-injection of
Content-Type/Content-Length/Content-Language which describe the final
response.

Tomcat NO-OPs early hints over AJP. We implement instead because the
AJP13 protocol does not forbid multiple SEND_HEADERS packets — grizzly
already emits multiple for 100-Continue — and forwarding is the
upstream connector's concern.

Addresses review comment in PR eclipse-ee4j#2298.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds the facade method to HttpServletResponseImpl delegating to the
underlying Response#sendEarlyHints(). The Servlet 6.2 spec signature
does not declare IOException, so the (effectively unreachable) async
write path's checked exception is logged at WARNING and swallowed —
matches glassfish web-core's ResponseFacade.sendEarlyHints.

Bumps jakarta.servlet-api 6.1.0 -> 6.2.0-M2.

Addresses review comment in PR eclipse-ee4j#2298.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds an encodeInitialLine(HttpPacket, HttpStatus, Buffer, MemoryManager)
overload on HttpCodecFilter (default delegates to the single-arg form),
overridden in HttpServerFilter to thread the status through a shared
private helper. The interim-response branch in HttpCodecFilter now
calls into this overload instead of inlining its own status-line
encoding, restoring CustomReasonPhrase support along the way (only for
the final status — interim always emits the registered reason phrase).

Addresses review comment in PR eclipse-ee4j#2298.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Deletes the acknowledgment-specific lifecycle from HttpResponsePacket
(setAcknowledgement, isAcknowledgement, acknowledged) and routes
100-Continue through the same interimStatus mechanism as 103 Early
Hints. The four parallel isAcknowledgement branches in HttpCodecFilter,
AjpHandlerFilter, AjpMessageUtils and DefaultOutputSink (HTTP/2) are
removed; the interim branch handles both.

Side-effects of the old acknowledged() that remained relevant (clearing
request.requiresAcknowledgement when the cleared interim was a 100
Continue) move into interimResponseSent. OutputBuffer's two identical
methods (acknowledge, writeInterimResponse) merge into the latter.

Fixes a latent bug exposed by the unification: HttpServerFilter was
calling prepareResponse (which mutates the response headers map with
Date/Content-Type/Content-Length) for interim responses too — those
mutations would leak into the interim packet on the wire and duplicate
on the final response. prepareResponse is now skipped for interim.

Also fixes ServletHandler.AckActionImpl.acknowledge() which was
setting the final response status to 100 before calling
sendAcknowledgement — previously masked by acknowledged() clearing
httpStatus, now surfaces because interimResponseSent does not.
Regression test added.

Addresses review comment in PR eclipse-ee4j#2298.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
DecoderUtils#finalizeKnownHeader had case "expect" missing return,
falling through into case "connection" which throws PROTOCOL_ERROR —
so every HTTP/2 request carrying Expect: 100-continue was rejected at
decode time. Same fall-through fixed on case "te". HTTP/2 100-Continue
now flows end-to-end via the app-level Response#sendAcknowledgement
path and the unified interim mechanism added in the previous commit.

HttpServerFilter#prepareRequest now uses Protocol#isAtLeast(HTTP_1_1)
for the legacy-protocol check and tightens the chunked-ack branch to
HTTP/1.1 specifically. The HTTP/2 case is left to the app-level flow.

Addresses review comment in PR eclipse-ee4j#2298.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@BalusC

BalusC commented May 27, 2026

Copy link
Copy Markdown
Contributor Author

@carryel: all your feedback has been addressed. PR description is also updated.

Comment thread pom.xml Outdated
@carryel
carryel merged commit d3ba2b3 into eclipse-ee4j:main Jun 2, 2026
3 checks passed
@BalusC
BalusC deleted the implement_sendEarlyHints branch June 2, 2026 10:56
@BalusC

BalusC commented Jun 2, 2026

Copy link
Copy Markdown
Contributor Author

Thank you for merging. What's the approximate release date for 5.0.2?

@carryel

carryel commented Jun 4, 2026

Copy link
Copy Markdown
Contributor

What's the approximate release date for 5.0.2?

As far as I know, Grizzly does not release updates regularly.

I believe that if there is a need or if a sufficient number of features are gathered, it will be possible to release updates at any time upon request.

@dmatej dmatej added this to the 5.0.2 milestone Jun 4, 2026
@OndroMih

OndroMih commented Jun 4, 2026

Copy link
Copy Markdown
Contributor

We will release Grizzly 5.0.2 soon, there's also an important fix pending

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants