Commit Graph

4 Commits

Author SHA1 Message Date
James Anderson 24d3dfcf00 fix(pages): harden cookie parsing (#1947)
* fix(pages): harden cookie parsing

* fix(pages): preserve cookie object compatibility

* fix(shims): distinguish edge cookie parser

* refactor(cookies): dedupe header parsing
2026-06-13 00:57:45 +01:00
Nathan Nguyen 2737a38d46 fix(pages-router): support stream proxying in API routes (#1902)
* fix(pages-router): support stream proxying in API routes

* fix(pages-router): address review feedback on stream proxying PR

- Narrow auto-end condition to only skip ending when handler returns res
  (the destination of req.pipe(...).pipe(res)), preventing hangs from
  non-stream return values.
- Replace full-body buffering with a PassThrough-backed PagesResponseStream
  that resolves on first write, enabling real streaming responses.
- Add error handling to reject responsePromise when the response stream
  errors, preventing silent request hangs.
- Add regression tests: non-stream return auto-end, multi-chunk write,
  and response stream error handling.

* fix(pages-router): avoid hanging responses and PassThrough unhandled errors

- Narrow auto-end condition to  to prevent non-stream
  return values from hanging requests.
- Replace PassThrough + Readable.toWeb() with a direct ReadableStream controller
  in PagesResponseStream to eliminate Controller is already closed unhandled
  errors during stream proxying.
- Add destroy() override to error/close the stream body when the Node Writable
  is destroyed after streaming starts.
- Add rejectResponse callback to PagesResponseStream constructor for proper
  error propagation.
- Add regression tests for auto-end, multi-chunk streaming, and destroy error
  handling.

* fix(pages-router): destroy Node side when Fetch body is cancelled

- Reject writes in _write() when streamEnded or destroyed to prevent
  buffering chunks after cancellation.
- In the ReadableStream cancel handler, clear bufferedChunks and destroy
  the Node-compatible PagesResponseStream so upstream writers receive
  an error instead of silently accumulating data.
- Add regression test proving writes after body cancellation fail with
  an error and do not accumulate in the bridge.
- Rename multi-chunk test to be more accurate about what it verifies.

* fix(pages-router): settle response when res.destroy() is called without error

- In destroy(), if the response has not been resolved yet, resolve the
  response promise (with a closed stream) when no error is provided, or
  reject it when an error is provided. This prevents the request from
  hanging forever when a handler calls res.destroy() before any body
  write and returns the res object.
- Add regression test proving res.destroy() without error resolves to an
  empty 200 response instead of hanging.

* chore: trigger CI run for flaky E2E test validation

* fix(pages-router): track pipe event and fix request body cancellation

- Add `res.once("pipe", ...)` listener to detect when a handler pipes into the response without returning the writable. Skipping auto-end by return-value only missed the real-world pattern:

    (req, res) => { req.pipe(upstream).pipe(res); } // returns undefined

  Next.js uses the pipe event signal, not the handler return value.

- Fix request body cancellation: the async generator finally block now calls `reader.cancel()` before `releaseLock()`, restoring the pre-PR behavior where early return from `for await (chunk of req)` cancelled the underlying stream instead of just releasing the lock.

- Add regression tests for both pipe-without-return and pipe-with-return.

* fix(pages-router): drop handlerResult === res auto-end signal, rely on pipe event only

The `handlerResult === res` check was too broad — chainable helpers like
`return res.status(202)` return `this` without implying streaming, and would
cause a hang by skipping auto-end. Now that `res.once("pipe", ...)` provides
the real stream signal, the return-value check is redundant and harmful.

Removes:
- handlerResult variable and === res comparison
- comment about returning the writable

Adds:
- regression test for `return res.status(202)` not hanging

* fix(pages-router): address stream response review

* fix(pages-router): close IncomingMessage parity gaps in API route streams

- Yield Buffer chunks (zero-copy views) from the request readable and opt
  out of objectMode so req matches IncomingMessage byte-stream semantics:
  per-chunk chunk.toString("utf8") decodes instead of comma-joining byte
  values, and setEncoding()/read(n) behave byte-wise.
- Honour config.api.externalResolver: Next.js never auto-ends a response,
  and externalResolver: true is the documented contract for handlers that
  respond after the handler promise settles (e.g. proxy middleware that
  attaches its pipe asynchronously). Skip the auto-end safety net for such
  routes so they are not truncated to an empty response.

---------

Co-authored-by: James <james@eli.cx>
2026-06-11 23:29:58 +00:00
James Anderson a8b3a5e394 fix(pages-router): make req async-iterable for bodyParser: false (#1479) (#1678)
Next.js Pages Router API handlers using `export const config = { api:
{ bodyParser: false } }` drain the raw bytes by iterating `req` directly
(`for await (const chunk of req)`), matching Node's IncomingMessage
contract. vinext's Workers-path req was a plain object with `body` set
to the underlying ReadableStream, so the idiom threw with 500 — breaking
webhook handlers and the `middleware-fetches-with-body` deploy suite.

Surface an `[Symbol.asyncIterator]` on the synthetic `PagesReqResRequest`
that proxies the underlying Web ReadableStream reader (with a `return()`
cleanup so `break` propagates a cancel). Leave `req.body` undefined when
`bodyParser: false`, matching Next.js (`parse-body.ts` only assigns
`req.body` when `bodyParser !== false`).

Ports the relevant assertion from
test/e2e/middleware-fetches-with-body/index.test.ts in the Next.js repo.
2026-06-03 23:07:19 +01:00
James Anderson a0f4e26655 fix(security): honor pages API bodyParser config (F-PROD-4) (#1617)
* fix(security): honor pages API bodyParser config (F-PROD-4)

Pages Router API routes export configuration like:

    export const config = { api: { bodyParser: false } };
    export const config = { api: { bodyParser: { sizeLimit: '4mb' } } };

vinext silently ignored this, hard-coding a 1 MB limit and always
parsing the body. That created a security downgrade for webhook
handlers (Stripe, GitHub, Slack, etc.) that depend on
'bodyParser: false' to read the raw request stream for HMAC
signature verification. By the time the user handler ran, the body
had already been consumed and parsed into 'req.body', so HMAC
verification would either fail closed (no signature match) or fail
open (handlers that fall back to an empty body for unsupported
content types).

Legitimate large uploads were also rejected with 413 even when the
route declared 'sizeLimit: "4mb"' or higher.

This change introduces a shared helper that resolves the route
module's 'config.api.bodyParser' option and patches both code paths:

  - Node/dev path:   packages/vinext/src/server/api-handler.ts
  - Workers/prod:    packages/vinext/src/server/pages-api-route.ts
                     (+ packages/vinext/src/server/pages-node-compat.ts)

Behaviour now matches Next.js
(packages/next/src/server/api-utils/node/api-resolver.ts):

  - bodyParser: false               -> skip parsing entirely. Node
                                       handlers receive the raw
                                       IncomingMessage stream;
                                       Workers handlers receive
                                       request.body as a
                                       ReadableStream<Uint8Array>
                                       on 'req.body'.
  - bodyParser: { sizeLimit: '4mb' } -> parse with the user-supplied
                                        limit instead of the
                                        hard-coded 1 MB.
  - bodyParser: true / undefined     -> existing 1 MB default behaviour.

The sizeLimit parser accepts the same suffix grammar Next.js does
('500b', '100kb', '4mb', '1gb'), implemented inline rather than
pulling the 'bytes' package.

Tests cover both Node and Workers paths plus parser-level unit
tests, including:

  - bodyParser: false leaves the stream intact for HMAC verification
  - sizeLimit '4mb' accepts a 2 MB POST that used to 413
  - sizeLimit '4mb' rejects a 5 MB POST with 413
  - sizeLimit '100kb' rejects a 200 KB POST with 413
  - No config preserves the 1 MB default

config.api.responseLimit is not in scope for this PR and remains a
follow-up.

Reference: Finding F-PROD-4 in SECURITY-AUDIT-2026-05.md

* chore(security): drop unused ResolvedBodyParserConfig export to satisfy knip
2026-05-27 16:25:32 +01:00