You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Commit e7fb1c2
Browse filesBrowse the repository at this point in the historyBrowse files
feat: Separate decompression cap for streamed bodies
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.
Copy file name to clipboardExpand all lines: CLAUDE.md
+1-1Lines changed: 1 addition & 1 deletion
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -41,7 +41,7 @@ Key abstractions:
41
41
-`com.retailsvc.http.validate.DefaultValidator` — single class using `switch` pattern-match over `Schema` subtypes. Validation failures produce RFC 9457 `application/problem+json` 400 responses.
42
42
-`com.retailsvc.http.internal.Router` — two indexes: exact path map and templated path list. Resolves `operationId` + extracted path variables for each request.
43
43
-`TypeMapper` — per-media-type request parsing and response writing; registered via `Builder.bodyMapper(...)`, with `GsonTypeMapper` auto-registered when Gson is on the classpath.
44
-
-`com.retailsvc.http.Request` — an immutable record-like carrier built from primitives (body bytes, path parameters, raw query string, a header lookup function), never the `HttpExchange`. `bytes()` returns the decoded body, `parsed()` the object produced by the `TypeMapper`. A handler that implements `StreamingRequestHandler` instead gets the body unread: `RequestPreparationFilter` routes first, validates parameters, peeks one byte, and binds `Request.streaming(...)`, whose `bodyStream()` decodes lazily (`internal/DecodingInputStream`) under the same size cap, or not at all when the handler's `decodeContent()` is false; `bytes()` / `parsed()` throw. `internal/StreamingLimit` optionally caps concurrent streaming requests server-wide (503 + `Retry-After`). `internal/RequestContentType` matches `Content-Type` against `requestBody.content` for both paths.
44
+
-`com.retailsvc.http.Request` — an immutable record-like carrier built from primitives (body bytes, path parameters, raw query string, a header lookup function), never the `HttpExchange`. `bytes()` returns the decoded body, `parsed()` the object produced by the `TypeMapper`. A handler that implements `StreamingRequestHandler` instead gets the body unread: `RequestPreparationFilter` routes first, validates parameters, peeks one byte, and binds `Request.streaming(...)`, whose `bodyStream()` decodes lazily (`internal/DecodingInputStream`) under its own `long`size cap (`maxDecompressedStreamingRequestBytes`, defaulting to the buffered one), or not at all when the handler's `decodeContent()` is false; `bytes()` / `parsed()` throw. `internal/StreamingLimit` optionally caps concurrent streaming requests server-wide (503 + `Retry-After`). `internal/RequestContentType` matches `Content-Type` against `requestBody.content` for both paths.
45
45
-`com.retailsvc.http.ContentCoding` — a pluggable HTTP content coding. gzip is built in (`internal/GzipCoding`); callers register others on the builder, held per direction in `internal/ContentCodings`. `RequestBodyReader` decodes requests under the size cap and `ResponseRenderer` codes responses. See the README's "Content encoding" section for the policy.
Copy file name to clipboardExpand all lines: docs/superpowers/plans/2026-10-08-streaming-request-bodies.md
+11-1Lines changed: 11 additions & 1 deletion
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -4,7 +4,7 @@
4
4
5
5
**Goal:** Let a handler opt into reading the request body as it arrives, via a `StreamingRequestHandler` marker type, so memory per request no longer grows with the body — while routing, parameter validation, security, interceptors and error rendering keep working.
6
6
7
-
**Architecture:**`RequestPreparationFilter` routes before reading the body. For an operation whose handler is a `StreamingRequestHandler`, it opens the body through a new `RequestBodyReader.stream(...)` instead of `read(...)`, validates parameters, peeks one byte to apply the `required` / `Content-Type` checks, and binds a `Request` built with `Request.streaming(...)`. Coded bodies are decoded lazily by an internal `DecodingInputStream` that enforces `maxDecompressedRequestBytes`as the handler reads. `ExtrasRouter` takes the same split for extra routes. Buffered operations keep their exact behaviour and order.
7
+
**Architecture:**`RequestPreparationFilter` routes before reading the body. For an operation whose handler is a `StreamingRequestHandler`, it opens the body through a new `RequestBodyReader.stream(...)` instead of `read(...)`, validates parameters, peeks one byte to apply the `required` / `Content-Type` checks, and binds a `Request` built with `Request.streaming(...)`. Coded bodies are decoded lazily by an internal `DecodingInputStream` that enforces the decompression cap as the handler reads (`maxDecompressedStreamingRequestBytes`, defaulting to `maxDecompressedRequestBytes`; Task 15). `ExtrasRouter` takes the same split for extra routes. Buffered operations keep their exact behaviour and order.
8
8
9
9
**Additions (Tasks 9–12), review fixes (Task 13):** Content-Type matching with media-type ranges and a missing-header default, `Request.headers()`, raw (undecoded) streaming bodies, and a server-wide cap on concurrent streaming requests. See the spec's "Additions" section.
10
10
@@ -186,3 +186,13 @@ Two independent reviews (Opus, Fable) returned SHIP WITH FIXES.
186
186
-[x]**Step 3:** SonarLint sweep (`sonar_analyze_file`) over the changed main and test files; fix new findings. The `java:S9391` hit in `OpenApiServer.validateHandlerWiring` predates this change.
187
187
-[x]**Step 4:** Note `StreamingRequestHandler` in `CLAUDE.md`'s architecture section.
188
188
-[x]**Step 5:** Commit on `feat/streaming-request-bodies`: `feat: Support streaming request bodies` (signed).
189
+
190
+
## Task 15: Streaming decompression cap
191
+
192
+
Spec: "Streaming decompression cap". Lifts the 2 GiB ceiling on decoded streams without touching the buffered path.
193
+
194
+
-[x]**Step 1:**`RequestBodyReader`: three-argument constructor `(maxDecompressedBytes, maxStreamedDecompressedBytes, decoders)`, both validated positive; the two-argument constructor delegates with the same cap. `stream` passes the streamed cap to `DecodingInputStream`; `read` is unchanged.
195
+
-[x]**Step 2:**`Builder.maxDecompressedStreamingRequestBytes(long)` — any positive `long`; unset falls back to `maxDecompressedRequestBytes` at `build()`. Javadoc on both setters.
196
+
-[x]**Step 3:** Tests. Unit: streamed body held to its own cap, buffered body keeps its cap when the streamed one is higher, constructor rejects a non-positive streamed cap; builder accepts `Integer.MAX_VALUE + 1` and `Long.MAX_VALUE`, rejects 0 and -1. IT: a gzip body past the buffered cap streams when the streaming cap is higher; one past the streaming cap is 413.
197
+
-[x]**Step 4:** README "Raising the cap for streamed bodies", cross-links from "Content encoding", the streaming bullets and "Raw bodies"; spec section and constraint; this task.
198
+
-[x]**Step 5:**`pre-commit run`, `mvn verify`, SonarLint on changed files; commit `feat: Separate decompression cap for streamed bodies` (signed) and push to PR #131.
0 commit comments