Skip to content

feat: Support streaming request bodies - #131

Merged
thced merged 4 commits into
masterfrom
feat/streaming-request-bodies
Oct 8, 2026
Merged

thced merged 4 commits into
masterfrom
feat/streaming-request-bodies

Conversation

@thced

@thced thced commented Oct 8, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Request bodies were always buffered in memory (readAllBytes). This adds opt-in streaming, so a handler can take large uploads without holding them in memory.

  • StreamingRequestHandler — a handler implementing it gets the body unread via request.bodyStream(). The Content-Encoding (gzip built in) is decoded lazily under a size cap; StreamingRequestHandler.raw(...) skips decoding and hands over the bytes as sent. bytes() / parsed() / asPojo() throw on a streaming request.
  • Still checked before the handler runs: routing, parameter validation, security, a required body being present (one-byte peek) and the Content-Type matching requestBody.content. The body schema is not validated (it can't be without buffering).
  • Builder.maxConcurrentStreamingRequests(n[, retryAfter]) — optional server-wide cap; excess streaming requests get 503 problem+json with Retry-After. The slot is taken after security and the peek, so slow or unauthenticated clients never hold one. Buffered requests are not counted. After-response hooks see the 503.
  • Builder.maxDecompressedStreamingRequestBytes(long) — a separate cap on the decoded size of a streamed body; any positive long, so past the 2 GiB that bounds buffered bodies (maxDecompressedRequestBytes, held in a byte[]). Defaults to maxDecompressedRequestBytes.
  • Content-Type matching (both paths): media ranges type/* and */* in requestBody.content; a missing Content-Type is treated as application/json if declared, else application/octet-stream (RFC 9110 §8.3).
  • Request.headers() — all request headers, case-insensitive.
  • Streaming extra routes (extraRoute with a StreamingRequestHandler) are supported and share the concurrency cap.
  • A streaming handler on an operation without a requestBody fails at boot.

Additive only; existing handlers behave as before. One 400 detail string for a missing Content-Type now names application/octet-stream when JSON isn't declared.

Design and plan: docs/superpowers/specs/2026-10-08-streaming-request-bodies-design.md, docs/superpowers/plans/2026-10-08-streaming-request-bodies.md. README has a new "Streaming request bodies" section.

Known limits (documented)

  • A buffered 404/405 is still answered after reading the full body (unchanged from master).
  • Clients sending Expect: 100-continue are answered 100 by the JDK before filters run, so a shed client may still send its body.

Test plan

  • mvn verify — 758 unit + 128 IT green (incl. 36 in StreamingRequestBodyIT: 8 MiB past the cap, gzip, 413/400/415, separate streaming cap, validation, security, cap + Retry-After, slot release on failures, stalled clients holding no slot)
  • pre-commit hooks pass
  • SonarLint clean on changed files

thced added 4 commits October 8, 2026 14:25
Handlers implementing StreamingRequestHandler receive the request body
unread as an InputStream, decoded lazily under the existing size cap, or
raw via StreamingRequestHandler.raw(...). Parameters, security and body
presence are still checked before the handler runs.

Also: Content-Type media ranges (type/*, */*) and a missing Content-Type
are matched against requestBody.content; Request.headers() lists all
headers; Builder.maxConcurrentStreamingRequests(...) caps concurrent
streaming requests with a 503 + Retry-After.
Builder.maxDecompressedStreamingRequestBytes(long) caps the decoded size
of a body read by a StreamingRequestHandler with any positive long, so
decoded streams are no longer held to the 2 GiB that bounds a buffered
body. Unset, it defaults to maxDecompressedRequestBytes.
Javadoc and README now name maxDecompressedStreamingRequestBytes where
streamed bodies are concerned. Adds an IT holding a streaming extra
route to the streaming cap, with the setters in reverse order.
@sonarqubecloud

sonarqubecloud Bot commented Oct 8, 2026

Copy link
Copy Markdown

@thced
thced merged commit 4d7d661 into master Oct 8, 2026
5 checks passed
@thced
thced deleted the feat/streaming-request-bodies branch October 8, 2026 12:51
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.

1 participant