Skip to content

http-transport: safe cors defaults, request body size cap - #270

Merged
denny-il merged 2 commits into
mainfrom
dev/security-defaults
Jul 15, 2026
Merged

denny-il merged 2 commits into
mainfrom
dev/security-defaults

Conversation

@denny-il

@denny-il denny-il commented Jul 15, 2026 •

Copy link
Copy Markdown
Contributor

Fixes the first two items of #214 (the WS-token-in-URL item needs coordinated transport/protocol work and stays open).

  • cors: true no longer reflects arbitrary origins with credentials: the defaults sent Access-Control-Allow-Credentials: true while reflecting the request origin verbatim, letting any website make credentialed requests against a cookie-authed API. Credentials are now granted only when the config supplies an explicit origin allowlist (string array, object form, or a vetting function whose returned allowlist actually contains the requesting origin — a returned list is matched, not trusted). The params type became a union that makes origin: true + allowCredentials a compile error while still accepting composed true | string[] values; a JS caller passing the vulnerable combo has it ignored at runtime. Origin-dependent responses now also emit Vary: Origin (merged with existing Vary values) so shared caches can't serve one origin's CORS response to another.
  • Request body size cap: the Node runtime buffered non-blob bodies unboundedly (Buffer.concat(await bodyStream.toArray())) with no backpressure — a single large request could exhaust memory. A new maxRequestBodySize option (default 128 MiB, matching Bun's native default so all runtimes behave identically) is enforced incrementally on every body path: declared oversized Content-Length is rejected before reading, the buffering loop and a byte-counting transform on blob/undecodable streams abort with 413 Payload Too Large the moment the running total exceeds the cap, and stream errors propagate through an awaited pipeline so an oversized upload yields a catchable 413 instead of an uncaught stream error. The option is also wired into Bun.serve (runtime-specific setting wins).

Tests cover: credentialed vs credential-less config shapes (through the public type, with compile-time fixtures), the vetting-function allowlist match and mismatch, Vary merging, and 413s for oversized buffered/blob/undecodable/declared bodies on both the direct handler and the real uWS adapter — asserting the server survives and serves the next request.

Summary by CodeRabbit

  • New Features
    • Added configurable request body size limits, defaulting to 128 MiB.
    • Oversized requests are rejected with HTTP 413 responses.
    • Improved streaming protection to avoid buffering oversized uploads.
    • Enhanced CORS handling, including safer credential rules and Vary: Origin responses.
  • Bug Fixes
    • Preserved multiple Vary response header values.
    • Ensured servers continue handling requests after rejected uploads.
  • Tests
    • Added coverage for request size limits, streaming uploads, and CORS behavior.

@coderabbitai

coderabbitai Bot commented Jul 15, 2026 •

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The HTTP transport adds configurable request body size limits with HTTP 413 handling across Node, Bun, and the server layer. CORS handling now restricts credentials to explicitly allowed origins and preserves Vary headers. Tests cover both behaviors.

Changes

HTTP transport hardening

Layer / File(s) Summary
Transport limits and error contracts
packages/http-transport/src/constants.ts, packages/http-transport/src/types.ts, packages/http-transport/src/utils.ts
Adds the 128 MiB default, exposes maxRequestBodySize options, constrains credentialed CORS types, and introduces PayloadTooLargeError.
Request body size enforcement
packages/http-transport/src/server.ts, packages/http-transport/src/runtimes/*
Rejects oversized bodies using declared lengths or streaming byte counts, returns HTTP 413, and configures equivalent limits for Node and Bun.
Credential-aware CORS handling
packages/http-transport/src/server.ts
Applies credentials only to explicitly allowed origins, adds Vary: Origin, and merges existing Vary response values.
Transport behavior tests and fixtures
packages/http-transport/tests/*, packages/http-transport/vitest.config.ts
Adds test helpers and coverage for body limits, runtime recovery, CORS behavior, type constraints, and Vitest discovery.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant HttpTransportServer
  participant BodySizeGuard
  participant RPCHandler
  Client->>HttpTransportServer: Send request body
  HttpTransportServer->>BodySizeGuard: Stream body through configured limit
  BodySizeGuard->>RPCHandler: Forward body within limit
  BodySizeGuard-->>HttpTransportServer: Raise PayloadTooLargeError over limit
  HttpTransportServer-->>Client: Return HTTP 413
Loading

Possibly related issues

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the two main changes: safer CORS defaults and a request body size cap.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch dev/security-defaults

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
packages/http-transport/src/server.ts (1)

433-455: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Guard against null from the CORS callback. typeof result === 'object' also matches null, so a null return will throw here and surface as a generic 500 instead of skipping CORS headers.

Proposed fix
-      } else if (typeof result === 'object') {
+      } else if (result && typeof result === 'object') {
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/http-transport/src/server.ts` around lines 433 - 455, Update the
CORS callback handling in the `#corsOptions` function branch to exclude null
before treating result as an object. A null callback result must skip CORS
header generation without throwing, while preserving the existing boolean and
non-null object behavior.
🧹 Nitpick comments (2)
packages/http-transport/src/server.ts (1)

178-215: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Buffered branch skips the early declared-size rejection used by the blob branch.

The blob/cannotDecode branch rejects immediately when Content-Length declares an oversized body (Lines 184-187). The buffered/decodable branch (Lines 200-211) has no equivalent early check — it only rejects once bytes accumulate past the cap, doing unneeded work for requests that are already known to be oversized. Also, a non-numeric Content-Length yields NaN (Line 182), which silently bypasses the early check entirely (NaN > cap is always false); worth guarding with Number.isNaN.

♻️ Suggested consolidation
+        const contentLength = request.headers.get('content-length')
+        const declaredSize = contentLength ? Number.parseInt(contentLength, 10) : undefined
+        if (
+          typeof declaredSize === 'number' &&
+          !Number.isNaN(declaredSize) &&
+          declaredSize > this.#maxRequestBodySize
+        ) {
+          throw new PayloadTooLargeError()
+        }
         if (isBlob || cannotDecode) {
-          const type = contentType || 'application/octet-stream'
-          const contentLength = request.headers.get('content-length')
-          const size = contentLength
-            ? Number.parseInt(contentLength, 10)
-            : undefined
-          // Declared size over the cap: reject before reading anything
-          if (size !== undefined && size > this.#maxRequestBodySize) {
-            throw new PayloadTooLargeError()
-          }
+          const type = contentType || 'application/octet-stream'
+          const size = Number.isNaN(declaredSize as number) ? undefined : declaredSize
           const clientStream = new ProtocolClientStream(-1, { size, type })
           ...
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/http-transport/src/server.ts` around lines 178 - 215, Consolidate
the Content-Length parsing and validation before the isBlob/cannotDecode branch
so every request path, including the buffered decoder flow, rejects a declared
body larger than `#maxRequestBodySize` before reading it. Treat a non-numeric
Content-Length as invalid using Number.isNaN rather than allowing NaN to bypass
the size check, while preserving the existing streaming received-size guard.
packages/http-transport/src/runtimes/node.ts (1)

76-99: 🚀 Performance & Scalability | 🔵 Trivial

Capped uploads still keep the socket busy. Later chunks are dropped in userland once capped is set, but the request isn’t aborted, so oversized bodies can keep consuming bandwidth until the client finishes sending them. If that waste matters, this needs a transport-level abort/pause path; res.close() alone won’t reliably stop in-flight chunks.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/http-transport/src/runtimes/node.ts` around lines 76 - 99, The
oversized-body path in the ReadableStream start handler must stop the underlying
uWS request, not merely set capped and discard later chunks. Update the
maxBodySize overflow handling around res.onDataV2 to invoke the transport-level
abort or pause mechanism that reliably halts in-flight upload delivery, while
preserving PayloadTooLargeError propagation and avoiding reliance on res.close()
alone.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@packages/http-transport/src/server.ts`:
- Around line 433-455: Update the CORS callback handling in the `#corsOptions`
function branch to exclude null before treating result as an object. A null
callback result must skip CORS header generation without throwing, while
preserving the existing boolean and non-null object behavior.

---

Nitpick comments:
In `@packages/http-transport/src/runtimes/node.ts`:
- Around line 76-99: The oversized-body path in the ReadableStream start handler
must stop the underlying uWS request, not merely set capped and discard later
chunks. Update the maxBodySize overflow handling around res.onDataV2 to invoke
the transport-level abort or pause mechanism that reliably halts in-flight
upload delivery, while preserving PayloadTooLargeError propagation and avoiding
reliance on res.close() alone.

In `@packages/http-transport/src/server.ts`:
- Around line 178-215: Consolidate the Content-Length parsing and validation
before the isBlob/cannotDecode branch so every request path, including the
buffered decoder flow, rejects a declared body larger than `#maxRequestBodySize`
before reading it. Treat a non-numeric Content-Length as invalid using
Number.isNaN rather than allowing NaN to bypass the size check, while preserving
the existing streaming received-size guard.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: e7586892-1c55-457a-819d-538842607241

📥 Commits

Reviewing files that changed from the base of the PR and between e258b3b and 10e3952.

📒 Files selected for processing (10)
  • packages/http-transport/src/constants.ts
  • packages/http-transport/src/runtimes/bun.ts
  • packages/http-transport/src/runtimes/node.ts
  • packages/http-transport/src/server.ts
  • packages/http-transport/src/types.ts
  • packages/http-transport/src/utils.ts
  • packages/http-transport/tests/_helpers/test-utils.ts
  • packages/http-transport/tests/body-limit.spec.ts
  • packages/http-transport/tests/cors.spec.ts
  • packages/http-transport/vitest.config.ts

@denny-il
denny-il merged commit 1375bd0 into main Jul 15, 2026
6 checks passed
@denny-il
denny-il deleted the dev/security-defaults branch July 15, 2026 07:01
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