Repository navigation
feat: add experimental GraphQL incremental delivery (G5) - #1596
Merged
jlouvel merged 6 commits intoNov 1, 2026
Merged
Conversation
7 tasks
Contributor
There was a problem hiding this comment.
🟡 Changes recommended
Critical multipart handling flaws allow forged parts and can falsely signal successful stream termination after a writer failure.
6 open issues
Leave multipart stream incomplete when writing a part fails · New Reject boundary tokens at the start of content · New Reject invalid demand without blocking on part reads · New Preserve null extension values when copying the map · New Verify the closing delimiter before signaling completion · New Close the reader on completion, failure, and cancellation · New
What changed in this PR
Adds experimental, opt-in GraphQL @defer delivery over multipart/mixed, building on streaming multipart support in Restlet core.
Changes:
- Adds a streaming multipart writer, bounded reader, and media-type constant.
- Integrates incremental responses into the GraphQL server and client, reusing shared result-queue accounting.
- Adds multipart and incremental-delivery tests and updates the changelog.
| File | Description |
|---|---|
org.restlet/src/test/java/org/restlet/representation/MultipartStreamTestCase.java |
Tests multipart framing, limits, validation, and streaming. |
org.restlet/src/main/java/org/restlet/representation/MultipartStreamRepresentation.java |
Writes and flushes multipart parts as produced. |
org.restlet/src/main/java/org/restlet/representation/MultipartStreamReader.java |
Reads streamed parts with size limits. |
org.restlet/src/main/java/org/restlet/data/MediaType.java |
Adds MULTIPART_MIXED. |
org.restlet.ext.graphql/src/test/java/org/restlet/ext/graphql/GraphqlIncrementalTestCase.java |
Tests incremental delivery, fallback, cancellation, and failures. |
org.restlet.ext.graphql/src/main/java/org/restlet/ext/graphql/internal/GraphqlResultQueue.java |
Supports serialization of additional result types. |
org.restlet.ext.graphql/src/main/java/org/restlet/ext/graphql/internal/GraphqlIncrementalStream.java |
Streams initial and delayed GraphQL results. |
org.restlet.ext.graphql/src/main/java/org/restlet/ext/graphql/GraphqlServerAdapter.java |
Enables negotiated, opt-in incremental execution. |
org.restlet.ext.graphql/src/main/java/org/restlet/ext/graphql/GraphqlIncrementalPublisher.java |
Converts multipart responses into incremental results. |
org.restlet.ext.graphql/src/main/java/org/restlet/ext/graphql/GraphqlClientAdapter.java |
Adds opt-in multipart response handling. |
changes.md |
Documents multipart APIs and incremental GraphQL support. |
Review effort: Balanced
jlouvel
force-pushed
the
feat/graphql-g5-incremental
branch
2 times, most recently
from
October 31, 2026 07:32
1e79749 to
417e97d
Compare
jlouvel
force-pushed
the
feat/graphql-g5-incremental
branch
from
November 1, 2026 17:10
417e97d to
dd693ba
Compare
Add protocol-neutral streaming multipart/* support (RFC 2046), the counterpart of EventStreamRepresentation and EventStreamReader for protocols whose responses are made of parts produced over time, such as GraphQL incremental delivery: - MultipartStreamRepresentation: a PartWriter sends parts through a PartSink; each part is flushed followed by the next delimiter, so that readers can process it at once, and the close delimiter is written when the writer returns. Parts whose content contains the delimiter are rejected, as they would forge parts (protocols can use short fixed boundaries such as "-"). The other parameters of the media type are kept. - MultipartStreamReader: returns parts as they arrive, as MultiPartRepresentation.Part, ignoring the preamble, the epilogue and transport padding, with a maximum part size and a Knuth-Morris-Pratt delimiter search. - MediaType.MULTIPART_MIXED. Refs #1587 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Jerome Louvel <374450+jlouvel@users.noreply.github.com>
- Drop the old boundary from the media type name, not only from its parameters, so the Content-Type advertises a single boundary. - Reject part content starting with the boundary, which would form a delimiter after the header line break and forge parts. - Validate the boundary and size limit before acquiring the entity stream, which would otherwise be left open. - Look up the boundary parameter case-insensitively. - Avoid overflowing a Long.MAX_VALUE part size limit. - Don't count the opening delimiter in the preamble size. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Jerome Louvel <374450+jlouvel@users.noreply.github.com>
Writing the close delimiter after a writer failure made a truncated stream look complete to readers. Only write it when the writer returns. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Jerome Louvel <374450+jlouvel@users.noreply.github.com>
@defer results over multipart/mixed, on the streaming multipart support of core. Experimental and off by default: the GraphQL incremental delivery RFC is not finalized, and graphql-java 26.1 implements @defer only (no @stream). - Server: setIncrementalDeliveryEnabled(true). When a query or mutation accepts multipart/mixed, graphql-java's incremental support is enabled for that execution. An IncrementalExecutionResult is streamed as parts (boundary "-", deferSpec=20220824, graphql-java's format): the initial result, then each DelayedIncrementalPartialResult, requested one at a time and bounded by maxQueuedBytes. A failure aborts the response, so that clients can't mistake a partial result for a complete one. Without multipart/mixed, @defer is executed inline as before. - Client: setIncrementalDeliveryEnabled(true) accepts multipart/mixed. execute() returns graphql-java's IncrementalExecutionResult, with its incremental payloads rebuilt as DeferPayload and StreamPayload, and an item publisher that reads the following parts on demand (single subscriber, released on completion, failure or cancellation, parts bounded by maxResponseBytes). - GraphqlResultQueue now serializes any result type. Refs #1588 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Jerome Louvel <374450+jlouvel@users.noreply.github.com>
Signal non-positive demand without waiting for the next part, keep null extension values, require the close delimiter after the last result, and close the multipart reader on completion, failure and cancellation, including cancellation before any demand. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A POST answered with 406 Not Acceptable left its body unread, so the Netty server closed the connection after the response, sometimes after the client had already reused it for its next request (1001 Communication Error, seen in GraphqlIncrementalTestCase). Discard bodies within the request size limit before answering. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
jlouvel
force-pushed
the
feat/graphql-g5-incremental
branch
from
November 1, 2026 20:09
dd693ba to
cfdefaf
Compare
jlouvel
marked this pull request as ready for review
November 1, 2026 20:10
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.





Closes #1588. Part of #1581.
Summary
@deferresults overmultipart/mixed, experimental and off by default on both sides. The GraphQL incremental delivery RFC is not finalized, and graphql-java 26.1 implements@deferonly (Directives.DeferDirective); there is no@stream. Schemas must declare the directive.Server
multipart/mixed, graphql-java's incremental support is enabled for that execution only (GraphQL.unusualConfiguration(input).incrementalSupport()). AnIncrementalExecutionResultis streamed withMultipartStreamRepresentation: the initial result, then eachDelayedIncrementalPartialResult, asapplication/jsonparts (boundary-,deferSpec=20220824, graphql-java's format).maxQueuedBytes. A failure aborts the response rather than ending it cleanly, so that clients can't mistake a partial result for a complete one.multipart/mixedinAccept, or when disabled,@deferis executed inline as before. Results that are not incremental are plain JSON.Client
execute()also acceptsmultipart/mixedand returns graphql-java's ownIncrementalExecutionResult, with incremental payloads rebuilt asDeferPayload/StreamPayload(data or items, path, label, errors, extensions).maxResponseBytes, and a truncated stream is aGraphqlTransportException.GraphqlResultQueue(internal) now serializes any result type, so SSE, WebSocket and incremental delivery share the same back-pressure and byte accounting.Validation
./mvnw -B verify -P default,test -pl org.restlet.ext.graphql(with core from this branch): 63 tests, 0 failures; 91.2% instruction coverage (gate 80%); Spotless, SpotBugs, Javadoc,-Werrorand dependency convergence pass.New
GraphqlIncrementalTestCase(7 tests): wire format, with the initial part read while the deferred field is still blocked; inline execution withoutmultipart/mixedor when disabled (406when onlymultipart/mixedis acceptable); client publisher withDeferPayloaddata and label, single subscriber; deferred field errors as payload errors; plain results stay plain; cancellation and invalid demand; a server-side failure aborting the response and surfacing as aGraphqlTransportException.🤖 Generated with Claude Code