Config matchers (redirects, rewrites, headers) now receive the decoded,
normalized pathname so that percent-encoded request paths like /%61bout
correctly match config rules defined for /about.
Both dev and prod servers rebuild the working URL from the decoded
pathname + original query string, ensuring all downstream consumers
operate on canonical paths.
Also adds unit and integration tests for percent-encoded path handling
in config matchers across App Router and Pages Router (dev + prod).
Strip all x-middleware-* prefixed headers from responses in both the
App Router and Pages Router dev servers before sending to clients.
These headers are internal routing signals (x-middleware-next,
x-middleware-rewrite, x-middleware-override-headers, etc.) and should
not be visible to end users.
The prod server already stripped these correctly. This brings the dev
servers into parity.
Ensures the pathname is decoded exactly once (via decodeURIComponent +
normalizePath) at the request entry point, and all downstream consumers
(middleware, config matchers, route matching) receive the same canonical
path. Previously, some code paths decoded the pathname a second time,
which could cause inconsistent matching between middleware and routing.
Also removes redundant decodeURIComponent calls from matchRoute and
matchConfigPattern catch-all branches that are no longer needed since
the caller now guarantees a pre-decoded pathname.
* fix: fetch cache key generation — handle all body types, include all headers minus blocklist
- Make buildFetchCacheKey async to support stream/blob body serialization
- Handle all BodyInit types: string, Uint8Array, ReadableStream, FormData, Blob, URLSearchParams
- Switch header strategy from allowlist (3 auth headers) to blocklist (traceparent/tracestate), matching Next.js
- Include RequestInit fields (mode, redirect, credentials, etc.) in cache key
- SHA-256 hash the cache key for compact, deterministic storage
- Fix spent stream bug: restore _ogBody in stripNextFromInit so originalFetch gets usable body
- Add 19 new tests covering body types, header inclusion, body restoration, and URLSearchParams
* Improve fetch cache key generation
Replace simple auth-header extraction with a unified header collector, add a header blocklist and cache version prefix, and introduce async cache key building. serializeBody now handles various body types (Uint8Array, streams, URLSearchParams, FormData, Blob, string), preserves the original body on init._ogBody, and body content is included in the cache key. The cache key is generated by normalizing URL, method, headers and init options and hashing them with SHA-256. buildFetchCacheKey is now async and awaited where used, and stripNextFromInit restores the preserved original body. Also add hasAuthHeaders and move auth header list into a constant.
* PR #111 approved. 52 tests pass, 3 fixes correct.
Co-authored-by: threepointone <threepointone@users.noreply.github.com>
---------
Co-authored-by: ask-bonk[bot] <ask-bonk[bot]@users.noreply.github.com>
Co-authored-by: threepointone <threepointone@users.noreply.github.com>
Syncs the checked-in build artifact with current source. Notable
changes: middleware runner now wraps decodeURIComponent in try/catch,
jsx-dev-runtime switched to jsx-runtime for production build.
The dev server's __sanitizeDestination only collapsed leading '//' but
ignored leading backslashes (e.g. '\evil.com'). The prod version in
config-matchers.ts already used /^[\/]+/ to handle both. Align the
dev codegen to match.
Wrap all decodeURIComponent calls on user-controlled URL paths in
try/catch to return 400 Bad Request instead of throwing an uncaught
URIError that crashes the Node process.
A single request with a malformed percent-encoded path (e.g. /%E0%A4%A)
could terminate the entire server process. This affected all server
entry points: prod server (App + Pages Router), dev server middleware,
Cloudflare Worker entry, and generated RSC/middleware handlers.
Fixes:
- prod-server.ts: App Router and Pages Router request handlers
- app-router-entry.ts: Cloudflare Worker entry
- app-dev-server.ts: Generated RSC entry handler
- index.ts: Dev server connect middleware + generated middleware runner
+ generated NEXT_LOCALE cookie parser
- middleware.ts: Pages Router dev middleware runner
Includes 11 regression tests across app-router, pages-router, and
features test suites.
* fix: close security gaps in init.ts shell exec and app-dev-server external proxy
Gap 1 (GHSA-w7vc-6jjg-498h): migrate init.ts from execSync to execFileSync
with argument array, matching the pattern already applied in deploy.ts and
cli.ts. Inputs are hardcoded package names so practical risk is near zero,
but this eliminates the last instance of the flagged pattern.
Gap 2 (GHSA-pf96-3m7r-pv3j): harden the inline __proxyExternalRequest in
the generated RSC entry (app-dev-server.ts) to strip Cookie, Authorization,
x-api-key, proxy-authorization, and all x-middleware-* headers before
forwarding to external rewrite targets. Also adds a 30-second
AbortSignal.timeout. This brings the dev-server code path into parity with
the already-fixed proxyExternalRequest in config-matchers.ts.
Also fixes a missed GSP redirect path in dev-server.ts where backslash
normalization was not applied (GHSA-79rw-r8hc-qg8v — the GSSP path was
already committed).
Includes an integration test that verifies credential stripping end-to-end
via a mock HTTP server and a conditional external rewrite in next.config.ts.
* fix: unify isExternalUrl to detect all URL schemes and protocol-relative URLs
All four copies of isExternalUrl (config-matchers.ts, router.ts,
navigation.ts, app-dev-server.ts inline) now use the same RFC 3986
scheme regex: /^[a-z][a-z0-9+.-]*:/i.test(url) || url.startsWith('//')
Previously:
- config-matchers.ts and app-dev-server.ts only checked http:// and
https:// (missing // protocol-relative)
- router.ts and navigation.ts checked http://, https://, and // but
missed exotic schemes (data:, javascript:, blob:, ftp:, etc.)
Now all copies are consistent and detect any URL scheme per RFC 3986
plus protocol-relative URLs.
Adds test cases for exotic schemes, protocol-relative, hash-only, and
bare string inputs.
* fix: add shell: true to execFileSync for Windows .cmd file compat
On Windows, package managers (pnpm, npm, yarn) are .cmd batch files.
execFileSync without shell: true cannot execute them. Adding shell: true
lets the OS shell resolve .cmd files while still using the argument array
form for safer argument passing.
* Sanitize redirects/headers and use execFileSync
Replace execSync calls with execFileSync (pass argv arrays) to avoid shell interpolation and adjust package-install logic to split package manager and args. Sanitize redirect destinations in SSR paths to prevent protocol-relative open-redirects and use sanitizeDestinationLocal for Next.js redirects. Ensure middleware receives a cleaned pathname by stripping internal .rsc suffix before creating NextRequest and add a test for this behavior. Strip sensitive headers (cookie, authorization and x-middleware-*) from proxied external requests and add a test. Document cache key hazards for auth headers in fetch-cache, add a security note for dangerouslySetInnerHTML in head SSR, and set the Draft Mode cookie to include Secure in production (with tests for production/dev behavior).
* Rename middleware header to x-custom-middleware
Change middleware header name from x-middleware-test to x-custom-middleware across fixtures and tests, and clarify server comment about stripping x-middleware-* headers from responses (reserved for internal routing signals). Files updated: packages/vinext/src/server/prod-server.ts (comment tweak), tests/fixtures/pages-basic/middleware.ts (header set), and tests/pages-router.test.ts (expectations and test titles/comments). This ensures tests reflect the new public header name while preserving internal x-middleware-* semantics.
* Normalize leading slashes and strip headers
Normalize redirect/rewrite destinations by collapsing any leading slashes or backslashes to a single "/" to avoid protocol-relative redirects and handle browser-backslash behavior. Strip additional sensitive headers (x-api-key, proxy-authorization) from proxied requests and ensure all x-middleware-* internal headers are removed from responses (broadened from x-middleware-request-*). Update dev/server sanitization logic accordingly. Update tests and fixture middleware to use shortened x-mw-* header names and add tests covering backslash normalization.
Refactor chained .replace() to single-pass tokenizers in pattern matching:
- matchConfigPattern in config-matchers.ts and index.ts
- escapeHeaderSource (new) replaces inline chained escaping in matchHeaders/applyHeaders
- matchMiddlewarePattern in prod server entry template
- matchPattern in middleware.ts
Also:
- escapeHeaderSource correctly handles :param(constraint) patterns
- Fix decodeURIComponent parity gap in index.ts matchConfigPattern
- Fix redundant dot in middleware.ts tokenizer char class
- Add lgtm suppression comments for false positives in test files
- Add unit tests for escapeHeaderSource
- Add normalizePath utility that collapses consecutive slashes,
resolves . and .. segments, and ensures a leading slash
- Apply decodeURIComponent + normalizePath before middleware matching
and route matching in all entry points (dev server, prod server,
Cloudflare Worker entry, codegen entries)
- Extract shared middleware codegen module (middleware-codegen.ts) as
single source of truth for pattern matching logic, replacing
hand-copied inline implementations in index.ts and app-dev-server.ts
- Fix default middleware matcher to return true when no matcher is
configured, matching Next.js behavior
- Fix regex pattern corruption in middleware matcher: patterns containing
parentheses or backslashes are now passed to RegExp directly without
dot-escaping
- Add Content-Security-Policy and X-Content-Type-Options headers to image responses
- Validate Content-Type allowlist (reject SVG, HTML, non-image types)
- Bound width parameter to configured deviceSizes/imageSizes with 3840px max
- Add deviceSizes and imageSizes to NextConfig type
- Apply to all image paths: Workers, Node.js prod server (App + Pages Router)
- Fix benchmarks limit param NaN handling
* fix: add Vary: RSC, Accept header to prevent CDN cache poisoning
Add Vary: RSC, Accept to all App Router RSC stream and HTML SSR
responses to prevent CDN cache poisoning. Without this header, a CDN
or reverse proxy may cache an RSC stream response and serve it to a
browser expecting HTML (or vice versa), breaking the page.
Affected response types in app-dev-server.ts:
- Main RSC stream responses (client-side navigation)
- HTML SSR responses (all cache control variants)
- Not-found/forbidden/unauthorized fallback pages (RSC and HTML)
- Error boundary pages (RSC and HTML)
- Server action redirect and re-render responses
- Intercepting route RSC stream responses
Fix Vary header merging in prod-server.ts:
- sendWebResponse: merge Accept-Encoding with existing Vary values
instead of overwriting (preserves RSC, Accept from upstream)
- sendCompressed: same merge behavior for buffered responses
Scope analysis (intentionally NOT changed):
- Pages Router: no RSC/HTML content negotiation on same URL
- Route handler responses: content type is handler-controlled
- Metadata routes: serve single content type per URL
- Static assets: no content negotiation
- Error/redirect responses: not cached or content-negotiated
- Worker entry (app-router-entry.ts): passes through RSC handler
responses which already include the Vary header
Closes GHSA-jgqv-mvrh-hv36
Related: GHSA-r2fc-ccr8-96c4 (Next.js equivalent)
* remove advisory reference from code comment
---------
Co-authored-by: ask-bonk[bot] <ask-bonk[bot]@users.noreply.github.com>
* feat(cli): load .env files with Next.js precedence
* fix(dotenv): address review feedback on .env loading
- Document Vite double-loading interaction in loadDotenv JSDoc
- Fix escaped dollar sign bug ($100 was not unescaped when $ wasn't
followed by a valid variable name)
- Add type cast for parseEnv return value
- Expand test coverage: loadedEnv assertions, production mode
precedence, ${VAR} expansion, escaped dollars, circular refs,
missing files, empty result
- Add getDotenvFiles unit tests for all three modes
- Add 'Prefer Node.js Built-in APIs' guideline to AGENTS.md
Co-authored-by: liuxiaopai-ai <liuxiaopai-ai@users.noreply.github.com>
* test(dotenv): add cross-file expansion and multi-line value tests
Address review suggestions: test that .env can reference vars from
higher-priority .env.development.local, and that parseEnv handles
quoted multi-line values correctly.
---------
Co-authored-by: root <root@localhost.localdomain>
Co-authored-by: liuxiaopai-ai <liuxiaopai-ai@users.noreply.github.com>
Fork PRs don't have access to repository secrets (CLOUDFLARE_API_TOKEN,
CLOUDFLARE_ACCOUNT_ID), so deploy/smoke-test/comment steps always fail.
Add a fork detection condition to skip these steps gracefully. The build
steps still run, so example builds are still validated for fork PRs.
* Skip deploy previews for fork PRs that lack Cloudflare secrets
Fork PRs don't have access to repository secrets (CLOUDFLARE_API_TOKEN,
CLOUDFLARE_ACCOUNT_ID), so deploy/smoke-test/comment steps always fail.
Add a fork detection condition to skip these steps gracefully. The build
steps still run, so example builds are still validated for fork PRs.
* Add AI-generated release summary + Google Chat notification to publish workflow
* fix: replace glob npm package with native node:fs/promises glob
Migrate pagesRouter, apiRouter, and appRouter from the third-party 'glob'
package to Node.js built-in glob (node:fs/promises), available since
Node 22. This removes a dependency and fixes path handling issues on
Windows where dynamic route segments (e.g. [slug]) were incorrectly
parsed due to premature path separator normalization.
Changes:
- Use for-await with glob() from node:fs/promises
- Use exclude callbacks instead of post-filtering
- Normalize pagePath/routePath with replaceAll('\\', '/') in output
- Remove glob dependency from package.json
* fix: address review feedback on native glob migration
- Fix excludePrivate over-exclusion: the native glob exclude callback
fires at every directory depth, not just the root. Replace with
post-filtering on the first path segment to match the old glob
package's root-relative ignore behavior.
- Remove redundant .replaceAll('\\', '/') normalization from
pagePath/routePath in app-router.ts — consumers already normalize
backslashes, and this was inconsistent with pages-router.ts.
- Add engines field (node >= 22) since fs.glob requires Node 22+.
* refactor: use glob pattern arrays instead of callbacks for exclude
- app-router: replace hasSlotSegment callback with ['**/@*'] pattern
- pages-router: replace excludePrivate callback with ['api', '**/_*'] patterns
- Simplify iteration: process glob results directly instead of collecting into intermediate arrays
---------
Co-authored-by: Steve Faulkner <sfaulkner@cloudflare.com>
- Add version bump input (patch/minor/major) to publish workflow
- Gate publish on full CI suite (lint, typecheck, vitest, e2e)
- Use npm OIDC trusted publishing instead of long-lived NPM_TOKEN
- Tag releases in git after publish
- Make ci.yml callable via workflow_call
- Fix repository URL format in package.json for OIDC validation
* fix: install react-server-dom-webpack during init and deploy for App Router
vinext init was missing react-server-dom-webpack from its dependency list,
causing 'Could not resolve react-server-dom-webpack/server.edge' errors on
fresh create-next-app projects. Additionally, react-server-dom-webpack@19.2.4
requires react@^19.2.4, but create-next-app ships react@19.2.3 -- the version
mismatch caused npm to nest rsdw inside vinext/node_modules where Vite can't
resolve it.
Fix: add react-server-dom-webpack to getInitDeps for App Router, detect when
the installed React version is too old, and upgrade react/react-dom as regular
dependencies before installing dev deps (avoiding ERESOLVE peer-dep conflicts
and keeping react in 'dependencies' rather than 'devDependencies').
Also applies the same fix to vinext deploy for parity, and adds a CI job that
scaffolds a real create-next-app project, runs vinext init, and verifies the
dev server returns HTTP 200.
Closes#22
* refactor: use createRequire for package resolution instead of direct node_modules reads
Address review feedback (PR #18 pattern): replace fs.existsSync(node_modules/...)
with createRequire-based resolution, which handles hoisting, pnpm symlinks,
monorepos, and Yarn PnP correctly.
- deploy.ts: extract isPackageResolvable() helper using createRequire, use it
in getMissingDeps for react-server-dom-webpack detection
- init.ts: refactor getReactUpgradeDeps() to use createRequire + findPackageVersion
walk-up pattern instead of reading node_modules/react/package.json directly
- tests: add isPackageResolvable unit tests, use dependency injection for
getMissingDeps rsdw test (Vitest's createRequire intercepts resolution)
* ci: add Windows to create-next-app smoke test
Run the create-next-app compatibility test on both ubuntu-latest and
windows-latest. Replace bash-specific server health check (curl, kill,
backgrounding) with a cross-platform Node script using child_process
and http. Use runner.temp instead of /tmp for portable paths.
* fix: use pathToFileURL for dynamic import() calls on Windows
On Windows, Node's ESM loader requires file:// URLs for absolute paths.
Bare paths like D:\... are rejected with ERR_UNSUPPORTED_ESM_URL_SCHEME.
Wrap all dynamic import() calls that receive absolute paths from
require.resolve() or path.join() with pathToFileURL().href:
- index.ts: RSC plugin import, RSC transforms import, PostCSS config
loading, PostCSS plugin resolution (5 call sites)
- cli.ts: Vite module import from project root
* fix: pathToFileURL for cache-runtime codegen and test imports
Additional Windows fixes found via PR #43 review:
- index.ts: use pathToFileURL for runtimeModulePath in 'use cache'
transform code generation (2 locations) — these embed absolute paths
into generated import() statements via JSON.stringify
- pages-router.test.ts: wrap 9 import(serverEntryPath) calls with
pathToFileURL so tests work on Windows
* feat(deploy): add --env support for wrangler deploy
Adds vinext deploy --env <name> and threads it through to wrangler deploy --env <name>. Keeps --preview working as shorthand for preview environment, adds unit tests for wrangler arg construction, and updates README/help examples. Refs #33.
* fix(deploy): use execFileSync and util.parseArgs for --env support
Build on #44 by liuxiaopai-ai — adds --env flag for wrangler deploy.
Security: switch from execSync(shell string) to execFileSync(binary, args)
to eliminate command injection via unsanitized --env/--name values.
Replace hand-rolled flag parsing (parseStringFlag, parseNumericFlag,
rawArgs.includes) with Node.js built-in util.parseArgs. This gives us
strict mode (throws on unknown flags), automatic --flag=value handling,
and proper error messages for missing values.
Also: buildWranglerDeployArgs now returns { args, env } directly instead
of re-parsing the args array for the log message.
---------
Co-authored-by: root <root@localhost.localdomain>
Updated the production server to handle binary responses correctly by using Buffer.from with arrayBuffer instead of text(). Added a test to ensure binary data integrity and created a new fixture for testing binary API responses.
* docs: update README — add blog link, lead with agent skill, fix examples, remove stale benchmarks
* docs: add LICENSE, sync package README from root, add bug reporting guidance
- Add MIT LICENSE to repo root
- Copy root README into package at prepack time instead of maintaining two copies
- Add 'Reporting bugs' section encouraging AI-assisted diagnosis
* fix: update LICENSE copyright to Cloudflare, Inc.
- Install vinext itself as a devDependency during init (#11)
- Remove explicit rsc() from generated configs since vinext auto-registers it (#12)
- Widen react/react-dom peer dep to >=19.2.0 for create-next-app compat (#13)
Co-authored-by: Fred K. Schott <fschott@cloudflare.com>
When vinext is installed from npm, the .ts source files aren't in the
package but .js.map files reference them, causing Vite to log many
'points to missing source files' warnings.
Adding inlineSources: true to tsconfig embeds the original TypeScript
source content directly in the .js.map files so no separate .ts files
are needed.
Fixes#2