* 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>
Two parity gaps with Next.js were causing edge runtime API routes to fail
with 500 errors:
1. Edge API handlers received a plain `Request` object, so accessing
`req.nextUrl.searchParams` (the idiomatic pattern in Next.js edge API
routes) threw at runtime. Next.js wraps the request in `NextRequest`
before invoking the handler (NextRequestHint in
next/src/server/web/adapter.ts).
2. Only `export const config = { runtime: 'edge' }` was recognised as an
edge runtime declaration. Next.js also accepts a bare
`export const runtime = 'edge'` and resolves the effective runtime as
`config.runtime ?? config.config?.runtime`
(next/src/build/analysis/get-page-static-info.ts).
Both fixes are applied in dev (`server/api-handler.ts`) and prod
(`server/pages-api-route.ts`) so the two paths stay in parity.
Refs #1338
* fix(pages-api): execute edge API routes with Fetch Request
Pages API routes with config.runtime set to edge were still executed through the Node-style req/res adapter. That gives user handlers a plain headers object and turns valid Edge API code that returns a Response into a 500.
Detect edge and experimental-edge API route modules before Node body parsing, pass the Fetch Request through, and forward the returned Response. The dev Node bridge mirrors the generated Web Request path and preserves Set-Cookie response headers.
Regression coverage ports the relevant Next.js edge-async-local-storage contract for Fetch Request execution, concurrent AsyncLocalStorage isolation, and the dev bridge.
* fix(pages-api): stream edge API responses in dev
* fix(pages-api): handle closed dev edge streams
* fix(pages-api): stream dev edge request bodies
* fix(pages-api): validate dev edge forwarded protocol
* refactor: extract pages api route runtime
* refactor: share Pages ISR cache runtime (#684)
* fix: address Pages API route review feedback
- Replace fragile error.message string matches with typed PagesBodyParseError
(statusCode: 413) thrown from pages-node-compat.ts and api-handler.ts; the
existing instanceof catch in handlePagesApiRoute handles both 400 and 413
- Fix res.setHeader('set-cookie') to replace instead of append on repeated
calls, matching Node.js ServerResponse semantics
- Extract shared getMediaType/isJsonMediaType helpers and the body-parse error
class into pages-media-type.ts to eliminate drift between api-handler.ts and
pages-node-compat.ts
- Add unit tests for 404 (null match), missing default export (500), redirect
(1-arg and 2-arg), writeHead header lowercasing/array joining, setHeader/
getHeader round-trip, and set-cookie replace-not-append parity
---------
Co-authored-by: James <james@eli.cx>