The dev edge API bridge (`createEdgeApiRequest` in `api-handler.ts`) was
reading `X-Forwarded-Proto` without the `trustProxy` gate that the rest of
the prod server uses. A client that can reach the dev server directly
could send `X-Forwarded-Proto: https` and trick edge handlers that gate
Secure-cookie issuance on `request.url.startsWith("https")` (or any other
`request.url.protocol` check) into believing the request was
TLS-terminated.
Same issue applied to `X-Forwarded-Host` in the same function: the raw
header value was used to build the request URL, opening a host-header
poisoning vector identical to the one `prod-server.resolveHost` already
guards against.
This commit:
* Extracts `resolveRequestProtocol`, `resolveRequestHost`, `trustProxy`,
and `trustedHosts` into a new shared module
`packages/vinext/src/server/proxy-trust.ts`. The helpers accept both
Node `IncomingMessage` and Web `Headers` so the same trust policy
applies in every server flavor.
* Updates `prod-server.ts` to delegate to the shared module
(re-exporting `resolveHost`, `trustedHosts`, and `trustProxy` to keep
the existing public surface and the tests that mutate `trustedHosts`
working).
* Updates `createEdgeApiRequest` to use the new helpers so dev edge API
routes honour `X-Forwarded-Proto` / `X-Forwarded-Host` only when
`VINEXT_TRUST_PROXY=1` / `VINEXT_TRUSTED_HOSTS` is configured.
Tests:
* New `tests/api-handler-trust-proxy.test.ts` covers the default
(untrusted) behaviour, `VINEXT_TRUST_PROXY=1`, and the
`VINEXT_TRUSTED_HOSTS` allow-list (including the implicit
`trustProxy` enablement, case-insensitive matching, and
comma-separated values).
* Updated the existing "uses the first x-forwarded-proto value" test to
reflect the new (correct) default of ignoring forged proxy headers.
Reference: Finding F-PROD-7 in SECURITY-AUDIT-2026-05.md.
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: migrate ssrLoadModule to moduleRunner.import
Replace all server.ssrLoadModule() calls in dev-server.ts and api-handler.ts
with runner.import() via the ModuleImporter interface, following the Vite
Module Runner migration guide. Remove ssrFixStacktrace calls which are not
needed with Module Runner APIs. Update call sites in index.ts to pass the
existing getPagesRunner() lazy factory as the runner argument. Update tests
to use ModuleImporter-shaped mocks instead of the old ViteDevServer mock.
* refactor: address bonk review comments
- Fix misleading stack trace comments: ssrFixStacktrace is not applicable
to ModuleRunner (not that it rewrites automatically)
- Update ALS-ARCHITECTURE.md to reference runner.import() instead of
server.ssrLoadModule()
- Add importModule() typed helper to instrumentation.ts to centralise
the Record<string, any> cast, removing 12 inline eslint-disable comments
- Remove dead ssrLoadModule mock from pages-router ISR test (createSSRHandler
no longer calls server.ssrLoadModule)
* chore: migrate to vite plus
* Disable typeAware and typeCheck
* Update CI
* Fix CI
* Fix test
* Clean
* Run test with vp
* Try revert
* react: false In test
* Fix test
* Revert "Try revert"
This reverts commit 009da10473.
* Update
* Update
* Try revert ci changes
* revert
* Run vp migrate
* Disable typeAware and typeCheck for now
* Better resolve for test
* Use vp dev instead of vite
* Update expect
* Fix NormalizeManifestModuleId
* Try increase timeout
* Update to use vp
* Try new check
* Bring back npx vp
* Migrate CI
* Make next-intl resolvable
* Update
* Update
* Update
* Fix Pages API parsing parity
* Fix Pages API body parsing parity
* Fix empty form body parity
* Regenerate entry-templates snapshots after merge
* fix: propagate statusText through prod server sendCompressed non-compressed path
The sendCompressed function had a statusText parameter but only used it in
the compressed response path. The non-compressed else branch called
res.writeHead directly without forwarding statusText, so short error
responses (like 'Invalid JSON' at 12 bytes, well below COMPRESS_THRESHOLD)
lost their custom reason phrase and fell back to the default 'Bad Request'.
Fix: replace the direct res.writeHead call in the else branch with the
writeHead closure that already handles the statusText conditional.
Also set statusText on the ApiBodyParseError response in the pages server
entry template so the value is present for prod-server to forward.
---------
Co-authored-by: James <james@eli.cx>
* Fix Buffer handling in Pages API res
* fix: skip reporting handled Pages API parse errors
* address review: blank line nit, content-length parity for Buffer, fix duplicate Content-Length in sendCompressed
* fix: set content-length for Buffer in production res.send() to match dev parity
---------
Co-authored-by: James <james@eli.cx>
* perf: pre-split route patterns and hoist URL split out of match loop
Route matching runs on every request and iterates all routes until a
match is found. Previously, matchPattern() re-split both the URL and
the pattern string on every iteration:
for (const route of routes) {
// url.split("/") — same URL, re-split every iteration
// pattern.split("/") — pattern never pre-computed
matchPattern(normalizedUrl, route.pattern);
}
For a 50-route app on a 404, that's 50 URL splits + 50 pattern splits
+ 50 Object.create(null) + 100 .filter(Boolean) calls.
Fix:
- Add `patternParts: string[]` to Route and AppRoute interfaces,
computed once at scan time
- Split the URL once in matchRoute/matchAppRoute before the loop
- Pass pre-split arrays to matchPattern instead of raw strings
- Apply the same optimization to generated production entries
(pages-server-entry.ts, app-rsc-entry.ts)
- Fix intercepting route lookup to use the new array-based API
* update entry-templates snapshots
---------
Co-authored-by: James <james@eli.cx>
* add oxfmt formatter: config, scripts, CI, editor setup, docs
* rebuild lockfile
* fix: add Format to required checks list, remove dead ignore pattern
* run fmt
* add format to agents.md again