Skip to content

feat: add a streaming multipart representation and reader - #1595

Closed
jlouvel wants to merge 0 commit into
3.0from
feat/streaming-multipart
Closed

jlouvel wants to merge 0 commit into
3.0from
feat/streaming-multipart

Conversation

@jlouvel

@jlouvel jlouvel commented Oct 30, 2026

Copy link
Copy Markdown
Collaborator

Closes #1587. Part of #1581 (prerequisite of GraphQL incremental delivery), but protocol-neutral.

Summary

MultiPartRepresentation parses or generates a known list of parts. Protocols whose responses are made of parts produced over time, such as GraphQL incremental delivery (@defer), need what EventStreamRepresentation and EventStreamReader provide for SSE:

// Server
response.setEntity(new MultipartStreamRepresentation(sink -> {
    sink.send(headers, firstPart);   // flushed with its trailing delimiter
    sink.send(headers, nextPart);    // later, from the same call thread
}));                                  // close delimiter written on return

// Client
try (MultipartStreamReader reader = new MultipartStreamReader(response.getEntity(), maxPartSize)) {
    MultiPartRepresentation.Part part;
    while ((part = reader.readPart()) != null) { ... }   // as parts arrive
}
  • MultipartStreamRepresentation (multipart/mixed by default, any multipart/* type): the PartWriter runs on the connector's call thread. Each part is written followed by the next delimiter (\r\n--boundary) and flushed, so readers can process it at once without waiting for the following part. This is the framing used by GraphQL incremental delivery implementations. The close delimiter is written when the writer returns. Boundaries are generated unless given; the other parameters of the media type are kept (unlike MultiPartRepresentation.setBoundary, which replaces them).
  • Forged parts are rejected: PartSink.send throws IllegalArgumentException when the content contains the delimiter. Protocols may use short fixed boundaries such as -, so data must not be able to forge parts.
  • MultipartStreamReader: an Iterator<MultiPartRepresentation.Part> and AutoCloseable that returns each part once its closing delimiter arrives. The preamble, epilogue and transport padding are ignored, and a leading delimiter without line break is accepted. Delimiters are found with a Knuth-Morris-Pratt search, so content resembling the delimiter is preserved. Headers plus content are bounded by maxPartSize (an IOException, never truncation), and so is the preamble. Part content is a ByteArrayRepresentation typed from the part's Content-Type.
  • MediaType.MULTIPART_MIXED.

Validation

  • ./mvnw -B verify -P default,test -pl org.restlet: 2,221 tests, 0 failures; 82.3% instruction coverage (gate 80%); SpotBugs at 8 findings (ratchet 9, unchanged); Spotless and Javadoc pass. Re-ran the static checks and the new test after the last change (media type parameters).
  • New MultipartStreamTestCase (6 tests): exact wire format of parts and of an empty stream; round trip of binary content and of content resembling the delimiter; preamble, epilogue and padding; malformed and oversized streams (no delimiter, bad delimiter line, truncated headers or content, invalid header, part, header line and preamble limits); validation (non-multipart type, invalid boundary, header injection, delimiter in content, send after close, media type parameters); an HTTP round trip where the first part is read while the server is still waiting to write the second.

🤖 Generated with Claude Code

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 Changes recommended

Multipart boundary handling can advertise a stale boundary and permit injected MIME parts, while reader validation and size-limit defects can leak streams or reject valid input.

6 open issues
What changed in this PR

Adds protocol-neutral streaming multipart support to Restlet core, providing a foundation for GraphQL incremental delivery without depending on GraphQL.

Changes:

  • Adds a multipart writer that flushes each part with its trailing delimiter.
  • Adds an iterator-based reader with bounded part sizes and multipart framing support.
  • Introduces MediaType.MULTIPART_MIXED, tests, and a changelog entry.
File Description
org.restlet/​src/​test/​java/​org/​restlet/​representation/​MultipartStreamTestCase.java Tests framing, validation, size limits, and HTTP streaming.
org.restlet/​src/​main/​java/​org/​restlet/​representation/​MultipartStreamRepresentation.java Produces and flushes multipart responses over time.
org.restlet/​src/​main/​java/​org/​restlet/​representation/​MultipartStreamReader.java Reads arriving parts with configurable size limits.
org.restlet/​src/​main/​java/​org/​restlet/​data/​MediaType.java Adds the MULTIPART_MIXED constant.
changes.md Documents the new streaming APIs and media type.

Review effort: Balanced

@jlouvel
jlouvel force-pushed the feat/streaming-multipart branch from 1ecffee to c76a762 Compare November 1, 2026 14:23
@jlouvel jlouvel closed this Nov 2, 2026
@jlouvel
jlouvel force-pushed the feat/streaming-multipart branch from c76a762 to 5bb754a Compare November 2, 2026 02:04
@jlouvel

jlouvel commented Nov 2, 2026

Copy link
Copy Markdown
Collaborator Author

Rebased onto 3.0 to resolve the changes.md conflict: its three commits were already on 3.0 as 7593588, 4760183 and d91cd7e (landed with the GraphQL incremental delivery stack, #1581), so the rebase dropped them all and GitHub closed this PR with no changes left. Nothing from it is missing on 3.0, including the review fixes and the #1587 changes.md entry.

@sonarqubecloud

sonarqubecloud Bot commented Nov 2, 2026

Copy link
Copy Markdown

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.

2 participants