cdk-nextjs 0.7.7 → 0.7.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. package/.jsii +2 -2
  2. package/docs/adapter-runtime.md +1 -1
  3. package/docs/harness-coverage.md +153 -73
  4. package/lib/adapter/adapter.mjs +12 -3
  5. package/lib/adapter/aws-cache-store.d.ts +7 -0
  6. package/lib/adapter/aws-cache-store.js +12 -1
  7. package/lib/adapter/aws-cache-store.test.js +32 -1
  8. package/lib/adapter/cache-handler.d.ts +5 -3
  9. package/lib/adapter/cache-handler.js +16 -10
  10. package/lib/adapter/cache-handler.mjs +103 -26
  11. package/lib/adapter/cache-handler.test.js +44 -1
  12. package/lib/adapter/cache-utils.d.ts +22 -0
  13. package/lib/adapter/cache-utils.js +44 -1
  14. package/lib/adapter/function-groups.js +17 -4
  15. package/lib/adapter/function-groups.test.js +33 -1
  16. package/lib/adapter/memory-cache-handler.d.ts +5 -1
  17. package/lib/adapter/memory-cache-handler.js +6 -2
  18. package/lib/adapter/s3-cache-handler.d.ts +12 -1
  19. package/lib/adapter/s3-cache-handler.js +30 -8
  20. package/lib/adapter/s3-cache-handler.test.js +84 -2
  21. package/lib/adapter/use-cache-common.d.ts +13 -0
  22. package/lib/adapter/use-cache-common.js +22 -6
  23. package/lib/adapter/use-cache-common.test.js +37 -1
  24. package/lib/adapter/use-cache-default-handler.mjs +33 -10
  25. package/lib/adapter/use-cache-remote-handler.mjs +33 -12
  26. package/lib/nextjs-api.js +1 -1
  27. package/lib/nextjs-build/nextjs-build.js +1 -1
  28. package/lib/nextjs-cache.js +1 -1
  29. package/lib/nextjs-compute/nextjs-containers.js +1 -1
  30. package/lib/nextjs-compute/nextjs-functions.js +1 -1
  31. package/lib/nextjs-distribution.js +1 -1
  32. package/lib/nextjs-post-deploy.js +1 -1
  33. package/lib/nextjs-static-assets.js +1 -1
  34. package/lib/root-constructs/nextjs-base-construct.js +1 -1
  35. package/lib/root-constructs/nextjs-global-containers.js +1 -1
  36. package/lib/root-constructs/nextjs-global-functions.js +1 -1
  37. package/lib/root-constructs/nextjs-regional-containers.js +1 -1
  38. package/lib/root-constructs/nextjs-regional-functions.js +1 -1
  39. package/lib/runtime/core.js +3 -1
  40. package/lib/runtime/image-utils.d.ts +8 -9
  41. package/lib/runtime/image-utils.js +16 -1
  42. package/lib/runtime/image-utils.test.js +13 -1
  43. package/lib/runtime/image.js +9 -3
  44. package/lib/runtime/lambda.mjs +50 -15
  45. package/lib/runtime/server.mjs +50 -15
  46. package/lib/runtime/tag-manifest.d.ts +25 -0
  47. package/lib/runtime/tag-manifest.js +37 -2
  48. package/lib/runtime/tag-manifest.test.js +27 -1
  49. package/package.json +3 -3
  50. package/scripts/e2e-deploy.sh +7 -2
  51. package/scripts/e2e-harness/README.md +72 -35
  52. package/scripts/e2e-harness/screen.mjs +148 -7
  53. package/scripts/playwright-install-deps.sh +40 -0
package/.jsii CHANGED
@@ -19875,6 +19875,6 @@
19875
19875
  "symbolId": "src/nextjs-build/nextjs-build:PublicDirEntry"
19876
19876
  }
19877
19877
  },
19878
- "version": "0.7.7",
19879
- "fingerprint": "wUvmDyqHWggvpq7Zle9jMzULe3GdaY7X42JRhFzqYxs="
19878
+ "version": "0.7.8",
19879
+ "fingerprint": "mbu9XA0j6aJlQ7uoPmGT3EFe6wEYliezchQ6jC93mWw="
19880
19880
  }
@@ -5,7 +5,7 @@ build time, what runs per request, where it knowingly differs from `next start`,
5
5
  and what each deployment type cannot do. The reasoning behind individual
6
6
  decisions lives in the code comments next to them; this is the map.
7
7
 
8
- Written against Next.js 16.3.8. `@next/routing` is exact-pinned to the `next`
8
+ Written against Next.js 16.4.0. `@next/routing` is exact-pinned to the `next`
9
9
  version and bumped with it.
10
10
 
11
11
  ## Build time: `onBuildComplete`
@@ -29,40 +29,47 @@ as a regression test once fixed.
29
29
 
30
30
  ## Status
31
31
 
32
- As of 2026-10-05, against next.js 16.3.8. The screening funnel lives in the
32
+ As of 2026-10-07, against next.js 16.4.0. The screening funnel lives in the
33
33
  manifest's `screening` block, regenerated by
34
34
  `node scripts/e2e-harness/screen.mjs --next ../next.js --write` (`--check` to
35
35
  verify, which every e2e-harness run does in shard 1).
36
36
 
37
- | Of next.js's 1136 e2e files | Count |
38
- | ----------------------------- | ----- |
39
- | edge-free (the hard ceiling) | 933 |
40
- | candidates after every screen | **1** |
41
- | deployed and screened | 472 |
42
- | in `rules.include` | 427 |
43
- | plus, per-case, in `suites` | 10 |
44
-
45
- The candidate pool is one file, `app-dir/nsec-105-opengraph-image-dynamicparams`,
46
- new in 16.3.8 and not yet deployed. Every other file is included, included
47
- per-case, or disqualified by a screen, and every disqualification that is a
48
- cdk-nextjs decision has a verdict below. The job now is keeping that true as next.js adds files and as
49
- each scheduled run re-runs what is already included.
50
-
51
- | Verdict | Files |
52
- | ------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
53
- | pass | 427 whole files, plus the passing cases of the 10 `suites` files |
54
- | fixed | the defects in the table below |
55
- | bug | none open |
56
- | upstream | 2 — `rewrites-destination-query-array`, `incremental-cache-path-traversal` |
57
- | unsupported | 4 whole files (`prerender-encoding`, `middleware-fetches-with-any-http-method`, `revalidate-dynamic`, `proxy-readable-toweb`), plus the skipped cases of `trailingslash`, `revalidate-path-with-rewrites` and the 6 `invalid-static-asset-404-*` files, and 203 files the edge screen disqualifies without deploying |
58
- | CDN-inherent | the 4 skipped cases of `dynamic-route-interpolation` |
59
- | architectural | the 2 skipped `resume-data-cache` cases |
60
- | no signal | 49 — 2 gated by next.js, 29 `skipDeployment` or stubbed in deploy mode, 18 `next-config-ts-native-ts` files that cannot be built here |
37
+ | Of next.js's 1213 e2e files | Count |
38
+ | ----------------------------- | ------- |
39
+ | edge-free (the hard ceiling) | 1009 |
40
+ | candidates after every screen | **165** |
41
+ | deployed and screened | 472 |
42
+ | in `rules.include` | 421 |
43
+ | plus, per-case, in `suites` | 14 |
44
+
45
+ 16.4 reopened the candidate pool. next.js replaced most `skipDeployment: true`
46
+ calls with `// @force-gate !deploy` pragmas, and in doing so dropped the deploy
47
+ exclusion from about 110 files that had carried it, so those now run in deploy
48
+ mode upstream. With about 30 files new in 16.4 and some 25 whose deploy
49
+ exclusion now covers only some of their suites, that makes 165 candidates, none
50
+ deployed yet. One more is `app-dir/treeshake-mw`: its `middleware.ts` opts into
51
+ the Node runtime, and the middleware screen used to count every `middleware.*`
52
+ as edge. Two included files, `prefetch-app-shell-cached-gsp` and
53
+ `prefetch-app-shell-global-eager`, no longer exist upstream and left
54
+ `rules.include`. Every disqualification that is a cdk-nextjs decision has a
55
+ verdict below. The job now is working through the candidates, and keeping the
56
+ included files passing on each scheduled run.
57
+
58
+ | Verdict | Files |
59
+ | ------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
60
+ | pass | 421 whole files, plus the passing cases of the 14 `suites` files |
61
+ | fixed | the defects in the table below |
62
+ | bug | none open |
63
+ | upstream | 2 — `rewrites-destination-query-array`, `incremental-cache-path-traversal` |
64
+ | unsupported | 4 whole files (`prerender-encoding`, `middleware-fetches-with-any-http-method`, `revalidate-dynamic`, `proxy-readable-toweb`), plus the skipped cases of `trailingslash`, `revalidate-path-with-rewrites`, `partial-fallback-shell-upgrade`, `cached-navigations` and the 6 `invalid-static-asset-404-*` files, and 203 files the edge screen disqualifies without deploying |
65
+ | CDN-inherent | the 4 skipped cases of `dynamic-route-interpolation` |
66
+ | architectural | the 2 skipped `resume-data-cache` cases |
67
+ | no signal | 49 — 2 gated by next.js (plus 3 cases next.js runs as `.failing` in deploy mode), 29 `skipDeployment` or stubbed in deploy mode, 18 `next-config-ts-native-ts` files that cannot be built here |
61
68
 
62
69
  ### Which types the harness proves
63
70
 
64
- The harness runs on all four types: the schedule on `NextjsGlobalFunctions`, the
65
- other three by `workflow_dispatch` (`nextjs_type`) or by hand, with
71
+ The harness runs on all four types, on the schedule and by `workflow_dispatch`
72
+ (`nextjs_type`, `all` for every type) or by hand, with
66
73
  `NextjsRegionalFunctions` behind `scripts/e2e-harness/stage-proxy.mjs`, which puts
67
74
  back the stage prefix the harness would otherwise drop. The findings whose
68
75
  plumbing differs per deployment type are also back-filled into
@@ -116,6 +123,13 @@ manifest's `rules.excludeByType["regional-functions"]`:
116
123
  | `app-dir/action-forward-loop` | fails | Action forwarding fetches the request's own origin (`createForwardedActionResponse`, from `initURL`) — `127.0.0.1`. Same cause. |
117
124
  | `hydration` | 1 / 3 | **Defect, fixed:** API Gateway collapses `//` to `/` in `event.path`, so Next.js never redirected it and the page failed to hydrate at `//`. `apiGatewayRequestPath` now restores the slashes. |
118
125
 
126
+ `app-dir/asset-prefix` joined them on 2026-10-08, for the same unserved
127
+ `assetPrefix`. It had passed until then by timing: its bundles case fetches
128
+ inside an un-awaited `forEach`, so the 404s reject after the case ends and jest
129
+ charges them to whichever case is running, usually none. That run they landed
130
+ in `rewrites that do not start with assetPrefix should still work`. Excluding
131
+ the bundles case removes the stray rejections too.
132
+
119
133
  ## Passing — in `rules.include`
120
134
 
121
135
  The list is the manifest's `rules.include`; it is not duplicated here. Notes worth
@@ -151,15 +165,19 @@ A `suites` file is included with the cases in its `failed` array skipped
151
165
  Use this instead of dropping a whole file when the residual failures have a
152
166
  verdict below.
153
167
 
154
- | File | Cases | Skipped, and why |
155
- | ------------------------------------------------------------------ | ---------- | ----------------------------------------------------------------------------------------------- |
156
- | `app-dir/trailingslash` | 6 / 8 | 2 `should revalidate a page with generated static params` — unsupported (async invalidation) |
157
- | `app-dir/resume-data-cache` | 3 / 5 | 2 `should have consistent data between static and dynamic renders` — architectural |
158
- | `dynamic-route-interpolation` | 3 / 7 | 4 requesting unencoded `[`/`]` — CDN-inherent |
159
- | `app-dir/revalidate-path-with-rewrites` | 1 / 2 | `static page` — unsupported (async invalidation) |
160
- | `invalid-static-asset-404-app` (+ `-asset-prefix`, `-base-path`) | 2 / 3 each | `should return 404 with plain text when fetching invalid asset path` — S3's XML 404, unsupported |
161
- | `invalid-static-asset-404-pages` (+ `-asset-prefix`, `-base-path`) | 2 / 3 each | Same case, same reason |
162
- | `next-form/default/app-dir` | 13 / 14 | `flakey`: `should soft-navigate on submit and show the prefetched loading state` — timing |
168
+ | File | Cases | Skipped, and why |
169
+ | ------------------------------------------------------------------ | ---------- | -------------------------------------------------------------------------------------------------------------- |
170
+ | `app-dir/trailingslash` | 6 / 8 | 2 `should revalidate a page with generated static params` — unsupported (async invalidation) |
171
+ | `app-dir/resume-data-cache` | 3 / 5 | 2 `should have consistent data between static and dynamic renders` — architectural |
172
+ | `dynamic-route-interpolation` | 3 / 7 | 4 requesting unencoded `[`/`]` — CDN-inherent |
173
+ | `app-dir/revalidate-path-with-rewrites` | 1 / 2 | `static page` — unsupported (async invalidation) |
174
+ | `app-dir/partial-fallback-shell-upgrade` | 10 / 13 | 3 waiting for `x-vercel-cache: HIT` — unsupported (Vercel's response header) |
175
+ | `invalid-static-asset-404-app` (+ `-asset-prefix`, `-base-path`) | 2 / 3 each | `should return 404 with plain text when fetching invalid asset path` — S3's XML 404, unsupported |
176
+ | `invalid-static-asset-404-pages` (+ `-asset-prefix`, `-base-path`) | 2 / 3 each | Same case, same reason |
177
+ | `next-form/default/app-dir` | 13 / 14 | `flakey`: `should soft-navigate on submit and show the prefetched loading state` — timing |
178
+ | `app-dir/metadata-streaming-parallel-routes` | 4 / 6 | 2 `should only bundle the selected metadata resolution implementation` — no signal (`@gate !deploy` inversion) |
179
+ | `app-dir/segment-cache/metadata` | 2 / 3 | `requests only the head when the page segments are cached` — no signal (`@gate !deploy` inversion) |
180
+ | `app-dir/segment-cache/cached-navigations` | 19 / 20 | `caches a fully static on-demand param for repeated navigations` — unsupported (`next start` serving) |
163
181
 
164
182
  The `next-form` case submits right after the page loads and waits for the loading
165
183
  state the router prefetched; where the prefetch has not landed, `#loading` never
@@ -178,43 +196,44 @@ defects came from the harness and 3 from the four-type suite.
178
196
  "4-type spec" names the `examples/e2e-tests/src/*.test.ts` file guarding it on all
179
197
  four deployment types; `—` means harness or unit tests only.
180
198
 
181
- | # | Symptom (as a test sees it) | Root cause | Fix / guard | 4-type spec |
182
- | --- | ------------------------------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------- |
183
- | 1 | Every POST with a body (server actions) → `403 InvalidSignatureException` | `patchFetchInClientJs` ran inside `runNextBuild()`, so `skipBuild: true` shipped an unpatched client with no `x-amz-content-sha256` | Call moved to the `NextjsBuild` constructor, marker-guarded against double wrap. The harness is the `skipBuild` guard | `request-methods` (in-page POST ok, raw POST 403) |
184
- | 2 | RSC responses labeled `text/html`; navigation duration `NaN`; action `redirect()` with no head | Seeded the HTML prerender's `initialHeaders` (`content-type`, `vary`, `x-nextjs-prerender`/`-postponed`) into `APP_PAGE` entries | `appPageCacheHeaders`, `src/adapter/cache-utils.ts` | `rsc-navigation` |
185
- | 3 | Empty streamed response: Function URL drops the prelude (bare `200 octet-stream`), API Gateway `502` | `RESPONSE_STREAM` with zero bytes after the prelude delimiter | `ResponseSink.padEmptyBody` (one space byte), `src/runtime/http/sink.test.ts` | `response-bodies`, `request-methods`, `server-action-redirect` |
186
- | 4 | Client navigation to `/` → 404, `x-nextjs-cache: HIT`, empty body | `groupPrerenders` rebuilt `${route}.rsc`; the root's payload is `/index.rsc` | `groupPrerenders`, `src/adapter/cache-utils.ts` | `rsc-navigation` |
187
- | 5 | `[id]/[id2]`: second param is the first's value plus `2` | `@next/routing` substitutes `$nxtP*` placeholders in insertion order, so a prefixing name is mangled | `repairRouteParamQuery`, `src/runtime/dispatch.ts` | `routing-params` |
188
- | 6 | `trailingSlash: true` — every canonical `/a/` URL 404s | Output pathnames never carry the slash; `@next/routing` matches exactly | `withTrailingSlashVariants`, `Dispatcher.normalizePathname` | — |
189
- | 7 | `next build` fails: "encountered uncached or runtime data during prerendering" (`cacheComponents` + handler) | Build-time cache handler was write-only; the final prerender pass re-issued fetches | `LocalFileCacheHandler.get`; `cache-handler.test.ts` | — |
190
- | 8 | `revalidateTag` never reaches a build-time prerender; tagged entries evicted right after storing | Handler read a `tags` array only runtime `set` writes, not `x-next-cache-tags`; mapping rows stamped `revalidatedAt = now` | `entryTags`, `checkIfRevalidated`, per-tag marker row — `src/adapter/s3-cache-handler.ts` | — |
191
- | 9 | Origin re-renders after `revalidatePath`, browser still gets the old page (`x-cache: Hit from cloudfront`) | Invalidation paths came only from tag mapping rows, which build-time prerenders lack | `implicitTagPath`/`invalidationVariants`; the build's tag manifest (s3-cache-handler) | — |
192
- | 10 | Fixture _n+1_ serves fixture _n_'s pages (another app's `/_not-found`) | With `NEXT_DEPLOYMENT_ID` set, next's `BUILD_ID` is the constant `build-TfctsWXpff2fKS`; our cache is partitioned by build id | `NextjsBuild.buildId` suffixes the manifest's `deploymentId` (`src/nextjs-build/nextjs-build.ts`) | — |
193
- | 11 | Unmatched optional catchall renders a segment `"undefined"` | `@next/routing` reports an unfilled param as a present key with value `undefined` | `repairRouteParamQuery` second pass; `dispatch.test.ts` | `routing-params` |
194
- | 12 | `robots.txt`, `sitemap.xml`, `manifest.webmanifest` served as `application/octet-stream` | Static metadata staged as `<route>.body`; `send` types by extension | `setBodyFileContentType`, `src/runtime/static-files.ts` | `metadata-routes` |
195
- | 13 | Root-params app (no `app/layout.tsx`) 404s at every URL | next gates the `dynamicRoutes` rule on draft cookies; concrete prerenders were not registered | `addPrerenderPathnames`, `src/adapter/build-outputs.ts` | `draft-mode`, `cookies` (cookie half only) |
196
- | 14 | PPR fallback shell rendered per request (`(runtime)` where `(buildtime)` expected) | Seeding loop skipped every prerender group whose pathname contains `[` | Prerender-group loop in `writeInitCache`, `src/adapter/init-cache.ts`; `init-cache.test.ts` | — |
197
- | 15 | Custom `pages/404` replaced by the built-in 404 | `statusTarget` tried `/_error` before an invocable `/404` | `statusTarget`; `dispatch.test.ts` "prefers an invocable custom /404 over /\_error" | — |
198
- | 16 | Path-style `assetPrefix` — every bundle 404s | No CloudFront behavior for `<prefix>/_next/static*`; Next applies `assetPrefix` on top of `basePath` | `relativeAssetPrefix`, `NextjsDistribution.addAssetPrefixBehavior` (CF function); regional types warn at synth | — |
199
- | 17 | Pages Router home page `/` 404s (many unrelated-looking symptoms) | `normalizePagePath("/")` is `/index`; nothing registered `/` | `routablePathnames`, `src/adapter/build-outputs.ts` | — |
200
- | 18 | Space in a `public/` filename fails synth: "Invalid CloudFront Distribution Cache Behavior Path Pattern" | Characters outside CloudFront's path-pattern alphabet threw | `toPathPattern` (`src/nextjs-distribution.ts`); `NextjsApi` warns and skips on Regional Functions. See 37 | `static-assets` (asserts 404 on Regional Functions) |
201
- | 19 | A throw answers plain-text 500, never the app's error page | No error-page ladder; one `failWith` wrote a bare 500 | `resolveErrorTarget` (dispatch.ts) + `NextjsRuntime.sendError`, `no-store` | `status-codes` (App Router side) |
202
- | 20 | Every i18n app answers `/` with a 308 to `/en-US` | `@next/routing` builds `/${locale}/`, which the trailing-slash 308 then matches | `Dispatcher.withRootLocale`, `normalizeRedirectLocation` | — |
203
- | 21 | Static `getStaticProps` data route 404s, so client navigation into it full-reloads | Without middleware, next emits no rule for it; the concrete prerender was skipped | `addPrerenderPathnames` registers concrete `/_next/data/…json` when no ungated rule matches | — |
204
- | 22 | Absolute `assetPrefix` with a path — every bundle 404s | The prefix's path was reduced to `""`, so no behavior was added | `assetPrefixPath`, `src/utils/base-path.ts` | — |
205
- | 24 | `/_next/image` 400s for a `public/` file whose name needs percent-encoding | `fetchFromS3` used the still-encoded href as the S3 key | Tolerant percent-decode in `fetchFromS3`, `src/runtime/image-utils.ts` | `static-assets` (asserts `image/webp`) |
206
- | 25 | A web worker never starts (assertion times out on placeholder text) | `patch-fetch.js` was prepended to `turbopack-worker-*.js` and read `window` | `patch-fetch.js` uses `globalThis`; worker-scope block in `patch-fetch.test.ts` | — |
207
- | 26 | Large init cache only partly seeded (`MISS`, postponed shell); deploy reported success | `BucketDeployment` Lambda hit `ENOSPC` in 512 MiB `/tmp`; `--hotswap` ignores a custom resource's `FAILED` | `sizeDeploymentLambda`, `src/nextjs-cache.ts` | — |
208
- | 27 | A 1 MB segment prefetch takes ~5s | Buffers serialized as JSON integer arrays and parsed with a reviver | Base64 in `serializeCacheValue`/`parseCacheValue` (old format still readable) | — |
209
- | 29 | A `beforeFiles` rewrite that matches its own output is applied twice | `RouteModule.prepare` re-runs `handleRewrites` on the already-rewritten `req.url` | `src/runtime/core.ts` hands `requestMeta.query` the resolved query | `config-routing` |
210
- | 30 | `//` → 500, `/api//json` → 404, instead of a 308 | `resolveRoutes` does not collapse repeated slashes; `new URL("//")` reads a host | Collapse + 308 in `src/runtime/core.ts`, ahead of any parsing | `url-normalization` |
211
- | 31 | On Global Functions, `//` paths → `403 InvalidSignatureException` | OAC signs the raw path, the Function URL canonicalizes before verifying | Viewer-request function, `createDynamicCloudFrontFunctionAssociations` | `url-normalization` (`FunctionGeneratedResponse`) |
212
- | 32 | Build-time `notFound: true` served as `200` with the 404 body | Seeded a `PAGES` entry holding `404.html`; the Pages HIT path ignores `value.status` | `writeInitCache` skips a `PAGES` prerender whose `initialStatus` ≠ 200; `init-cache.test.ts` | `status-codes` (App Router half) |
213
- | 33 | `res.revalidate()` fails (`{revalidated:false}`), reason reports `stale` | No `routerServerContext.revalidate` supplied: "missing internal router-server-methods" | `requestMeta.revalidate` in `src/runtime/core.ts`; `core.test.ts` | — |
214
- | 34 | Untagged `force-cache` fetch is never evicted by `revalidatePath`/`revalidateTag` | Fetch entries were checked against their stored tags, not the request's `ctx.tags` + `ctx.softTags` | `isFetchCacheGet` split in `S3CacheHandler.get` | — |
215
- | 35 | Catch-all capture containing `%2F` → 500 ("Requested and resolved page mismatch") | The `nxtP` query contract decodes twice and splits on `/` | `outOfBandRouteParams` (dispatch.ts) passes `requestMeta.params`. Verified offline; the file is upstream | — |
216
- | 36 | Regional Functions: every redirect drops the stage prefix, following it → 403 | API Gateway strips the stage before invoking the Lambda | `src/runtime/api-gateway-path.ts` uses `requestContext.path` when `basePath` starts with the stage | `url-normalization` (where it was found) |
217
- | 39 | `cached-navigations`: a cached navigation shows params or lacks `'use cache'` content in its static stage | `refreshTags` awaited the revalidation log `Query`, which Next awaits in the `'use cache'` lookup, past the stage timer | `tagMethods` starts the refresh unawaited (`use-cache-common.ts`); `use-cache-handlers.test.ts` | — |
199
+ | # | Symptom (as a test sees it) | Root cause | Fix / guard | 4-type spec |
200
+ | --- | ------------------------------------------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------- |
201
+ | 1 | Every POST with a body (server actions) → `403 InvalidSignatureException` | `patchFetchInClientJs` ran inside `runNextBuild()`, so `skipBuild: true` shipped an unpatched client with no `x-amz-content-sha256` | Call moved to the `NextjsBuild` constructor, marker-guarded against double wrap. The harness is the `skipBuild` guard | `request-methods` (in-page POST ok, raw POST 403) |
202
+ | 2 | RSC responses labeled `text/html`; navigation duration `NaN`; action `redirect()` with no head | Seeded the HTML prerender's `initialHeaders` (`content-type`, `vary`, `x-nextjs-prerender`/`-postponed`) into `APP_PAGE` entries | `appPageCacheHeaders`, `src/adapter/cache-utils.ts` | `rsc-navigation` |
203
+ | 3 | Empty streamed response: Function URL drops the prelude (bare `200 octet-stream`), API Gateway `502` | `RESPONSE_STREAM` with zero bytes after the prelude delimiter | `ResponseSink.padEmptyBody` (one space byte), `src/runtime/http/sink.test.ts` | `response-bodies`, `request-methods`, `server-action-redirect` |
204
+ | 4 | Client navigation to `/` → 404, `x-nextjs-cache: HIT`, empty body | `groupPrerenders` rebuilt `${route}.rsc`; the root's payload is `/index.rsc` | `groupPrerenders`, `src/adapter/cache-utils.ts` | `rsc-navigation` |
205
+ | 5 | `[id]/[id2]`: second param is the first's value plus `2` | `@next/routing` substitutes `$nxtP*` placeholders in insertion order, so a prefixing name is mangled | `repairRouteParamQuery`, `src/runtime/dispatch.ts` | `routing-params` |
206
+ | 6 | `trailingSlash: true` — every canonical `/a/` URL 404s | Output pathnames never carry the slash; `@next/routing` matches exactly | `withTrailingSlashVariants`, `Dispatcher.normalizePathname` | — |
207
+ | 7 | `next build` fails: "encountered uncached or runtime data during prerendering" (`cacheComponents` + handler) | Build-time cache handler was write-only; the final prerender pass re-issued fetches | `LocalFileCacheHandler.get`; `cache-handler.test.ts` | — |
208
+ | 8 | `revalidateTag` never reaches a build-time prerender; tagged entries evicted right after storing | Handler read a `tags` array only runtime `set` writes, not `x-next-cache-tags`; mapping rows stamped `revalidatedAt = now` | `entryTags`, `checkIfRevalidated`, per-tag marker row — `src/adapter/s3-cache-handler.ts` | — |
209
+ | 9 | Origin re-renders after `revalidatePath`, browser still gets the old page (`x-cache: Hit from cloudfront`) | Invalidation paths came only from tag mapping rows, which build-time prerenders lack | `implicitTagPath`/`invalidationVariants`; the build's tag manifest (s3-cache-handler) | — |
210
+ | 10 | Fixture _n+1_ serves fixture _n_'s pages (another app's `/_not-found`) | With `NEXT_DEPLOYMENT_ID` set, next's `BUILD_ID` is the constant `build-TfctsWXpff2fKS`; our cache is partitioned by build id | `NextjsBuild.buildId` suffixes the manifest's `deploymentId` (`src/nextjs-build/nextjs-build.ts`) | — |
211
+ | 11 | Unmatched optional catchall renders a segment `"undefined"` | `@next/routing` reports an unfilled param as a present key with value `undefined` | `repairRouteParamQuery` second pass; `dispatch.test.ts` | `routing-params` |
212
+ | 12 | `robots.txt`, `sitemap.xml`, `manifest.webmanifest` served as `application/octet-stream` | Static metadata staged as `<route>.body`; `send` types by extension | `setBodyFileContentType`, `src/runtime/static-files.ts` | `metadata-routes` |
213
+ | 13 | Root-params app (no `app/layout.tsx`) 404s at every URL | next gates the `dynamicRoutes` rule on draft cookies; concrete prerenders were not registered | `addPrerenderPathnames`, `src/adapter/build-outputs.ts` | `draft-mode`, `cookies` (cookie half only) |
214
+ | 14 | PPR fallback shell rendered per request (`(runtime)` where `(buildtime)` expected) | Seeding loop skipped every prerender group whose pathname contains `[` | Prerender-group loop in `writeInitCache`, `src/adapter/init-cache.ts`; `init-cache.test.ts` | — |
215
+ | 15 | Custom `pages/404` replaced by the built-in 404 | `statusTarget` tried `/_error` before an invocable `/404` | `statusTarget`; `dispatch.test.ts` "prefers an invocable custom /404 over /\_error" | — |
216
+ | 16 | Path-style `assetPrefix` — every bundle 404s | No CloudFront behavior for `<prefix>/_next/static*`; Next applies `assetPrefix` on top of `basePath` | `relativeAssetPrefix`, `NextjsDistribution.addAssetPrefixBehavior` (CF function); regional types warn at synth | — |
217
+ | 17 | Pages Router home page `/` 404s (many unrelated-looking symptoms) | `normalizePagePath("/")` is `/index`; nothing registered `/` | `routablePathnames`, `src/adapter/build-outputs.ts` | — |
218
+ | 18 | Space in a `public/` filename fails synth: "Invalid CloudFront Distribution Cache Behavior Path Pattern" | Characters outside CloudFront's path-pattern alphabet threw | `toPathPattern` (`src/nextjs-distribution.ts`); `NextjsApi` warns and skips on Regional Functions. See 37 | `static-assets` (asserts 404 on Regional Functions) |
219
+ | 19 | A throw answers plain-text 500, never the app's error page | No error-page ladder; one `failWith` wrote a bare 500 | `resolveErrorTarget` (dispatch.ts) + `NextjsRuntime.sendError`, `no-store` | `status-codes` (App Router side) |
220
+ | 20 | Every i18n app answers `/` with a 308 to `/en-US` | `@next/routing` builds `/${locale}/`, which the trailing-slash 308 then matches | `Dispatcher.withRootLocale`, `normalizeRedirectLocation` | — |
221
+ | 21 | Static `getStaticProps` data route 404s, so client navigation into it full-reloads | Without middleware, next emits no rule for it; the concrete prerender was skipped | `addPrerenderPathnames` registers concrete `/_next/data/…json` when no ungated rule matches | — |
222
+ | 22 | Absolute `assetPrefix` with a path — every bundle 404s | The prefix's path was reduced to `""`, so no behavior was added | `assetPrefixPath`, `src/utils/base-path.ts` | — |
223
+ | 24 | `/_next/image` 400s for a `public/` file whose name needs percent-encoding | `fetchFromS3` used the still-encoded href as the S3 key | Tolerant percent-decode in `fetchFromS3`, `src/runtime/image-utils.ts` | `static-assets` (asserts `image/webp`) |
224
+ | 25 | A web worker never starts (assertion times out on placeholder text) | `patch-fetch.js` was prepended to `turbopack-worker-*.js` and read `window` | `patch-fetch.js` uses `globalThis`; worker-scope block in `patch-fetch.test.ts` | — |
225
+ | 26 | Large init cache only partly seeded (`MISS`, postponed shell); deploy reported success | `BucketDeployment` Lambda hit `ENOSPC` in 512 MiB `/tmp`; `--hotswap` ignores a custom resource's `FAILED` | `sizeDeploymentLambda`, `src/nextjs-cache.ts` | — |
226
+ | 27 | A 1 MB segment prefetch takes ~5s | Buffers serialized as JSON integer arrays and parsed with a reviver | Base64 in `serializeCacheValue`/`parseCacheValue` (old format still readable) | — |
227
+ | 29 | A `beforeFiles` rewrite that matches its own output is applied twice | `RouteModule.prepare` re-runs `handleRewrites` on the already-rewritten `req.url` | `src/runtime/core.ts` hands `requestMeta.query` the resolved query | `config-routing` |
228
+ | 30 | `//` → 500, `/api//json` → 404, instead of a 308 | `resolveRoutes` does not collapse repeated slashes; `new URL("//")` reads a host | Collapse + 308 in `src/runtime/core.ts`, ahead of any parsing | `url-normalization` |
229
+ | 31 | On Global Functions, `//` paths → `403 InvalidSignatureException` | OAC signs the raw path, the Function URL canonicalizes before verifying | Viewer-request function, `createDynamicCloudFrontFunctionAssociations` | `url-normalization` (`FunctionGeneratedResponse`) |
230
+ | 32 | Build-time `notFound: true` served as `200` with the 404 body | Seeded a `PAGES` entry holding `404.html`; the Pages HIT path ignores `value.status` | `writeInitCache` skips a `PAGES` prerender whose `initialStatus` ≠ 200; `init-cache.test.ts` | `status-codes` (App Router half) |
231
+ | 33 | `res.revalidate()` fails (`{revalidated:false}`), reason reports `stale` | No `routerServerContext.revalidate` supplied: "missing internal router-server-methods" | `requestMeta.revalidate` in `src/runtime/core.ts`; `core.test.ts` | — |
232
+ | 34 | Untagged `force-cache` fetch is never evicted by `revalidatePath`/`revalidateTag` | Fetch entries were checked against their stored tags, not the request's `ctx.tags` + `ctx.softTags` | `isFetchCacheGet` split in `S3CacheHandler.get` | — |
233
+ | 35 | Catch-all capture containing `%2F` → 500 ("Requested and resolved page mismatch") | The `nxtP` query contract decodes twice and splits on `/` | `outOfBandRouteParams` (dispatch.ts) passes `requestMeta.params`. Verified offline; the file is upstream | — |
234
+ | 36 | Regional Functions: every redirect drops the stage prefix, following it → 403 | API Gateway strips the stage before invoking the Lambda | `src/runtime/api-gateway-path.ts` uses `requestContext.path` when `basePath` starts with the stage | `url-normalization` (where it was found) |
235
+ | 39 | `cached-navigations`: a cached navigation shows params or lacks `'use cache'` content in its static stage | `refreshTags` awaited the revalidation log `Query`, which Next awaits in the `'use cache'` lookup, past the stage timer | `tagMethods` starts the refresh unawaited (`use-cache-common.ts`); `use-cache-handlers.test.ts` | — |
236
+ | 40 | `cached-navigations`: after a draft mode page load, a navigation reuses nothing from it | 16.4 ends a staged render's static stage on `Date.now()`, `new Date()`, `Math.random()` or a `crypto` random call in the request's context. `refreshTags` read `Date.now()` there, and a draft mode page is rendered whole at request time, so its static stage (`l`) was 0 bytes | Cache handler methods run outside the request (`outsideRender`, `use-cache-common.ts`; `outsideRequest`, `runtime/tag-manifest.ts`); `tag-manifest.test.ts`, `use-cache-common.test.ts` | — |
218
237
 
219
238
  Found by the four-type suite:
220
239
 
@@ -359,6 +378,47 @@ Making them pass would mean blocking every revalidation on an invalidation.
359
378
  attempt, on a case with no prerender to invalidate. That was defect 34 — the
360
379
  `dynamic page` case of `revalidate-path-with-rewrites` had passed once by luck.
361
380
 
381
+ ### `x-vercel-cache` is Vercel's header
382
+
383
+ `app-dir/partial-fallback-shell-upgrade`, 3 of 13 (the other 10 via `suites`),
384
+ all new in 16.4 and all in the `partialPrefetching disabled` describe. Under
385
+ `isNextDeploy` each polls for `x-vercel-cache: HIT`, the header Vercel's CDN adds
386
+ to a response it served from cache. Nothing in cdk-nextjs emits it, so the poll
387
+ times out on `null` before the case's own checks (the shell's `data-rendered-at`
388
+ reused across params and across a revalidation) run. Those checks are the
389
+ coverage being given up; the passing `prefix` cases cover shell sharing and
390
+ revalidation for a fallback shell, but not for an on-demand shell (no
391
+ `generateStaticParams`). Emitting the header would mean claiming a CDN hit
392
+ cdk-nextjs can't vouch for. Failed both attempts on Global Functions in
393
+ the 2026-10-07 run, with no deploy error.
394
+
395
+ ### An on-demand param's RSC navigation is a live render, not a prerender
396
+
397
+ `app-dir/segment-cache/cached-navigations` (1 of 20, via `suites`), new in 16.4:
398
+ `caches a fully static on-demand param for repeated navigations`. Its
399
+ `isNextDeploy` branch navigates to `/fully-static-params/t4` (not in
400
+ `generateStaticParams`) and expects a repeat visit a minute later to make no
401
+ request, because "the platform serves a completed static prerender". On Vercel,
402
+ Next.js runs in minimal mode: an RSC navigation without postponed state is not a
403
+ dynamic RSC request (`isDynamicRSCRequest` in `app-page-runtime.js`), so the
404
+ function prerenders `t4` and the edge caches the result, which starts with the
405
+ `#` "complete" marker byte (`prependIsPartialByteToChunks`). cdk-nextjs runs
406
+ Next.js the way `next start` does, where the same request is a dynamic RSC
407
+ request: a live render, without a marker, which the client reads as partial
408
+ (`stripIsPartialByte`) and so fetches again. The response we sent was exactly
409
+ that: `cache-control: private, no-store`, no marker, `"l"` set to its 2666-byte
410
+ static stage. The case's own non-`isNextDeploy` branch asserts this request for
411
+ `next start`. Matching Vercel would mean minimal mode, with the runtime doing the
412
+ platform's prerender routing and caching. Not done.
413
+
414
+ The file's other case new in 16.4 that failed, `reuses cached page segment
415
+ across fallback params after a draft mode HTML load`, was a cdk-nextjs defect,
416
+ fixed (40 in the table). It passed offline because `e2e-offline.sh` has no
417
+ revalidation log, so `refreshTags` returned before reading the clock. And it
418
+ passed on Regional Containers only because that harness URL is plain HTTP: the
419
+ browser drops the `Secure` draft cookie there, so the page never loaded in
420
+ draft mode.
421
+
362
422
  ### A test's `env` reaches `next build`, not the deployed app
363
423
 
364
424
  Per-test `env` from `nextTestSetup({ env })` is set for `next build`, so values
@@ -421,6 +481,26 @@ payload — the problem next.js's `cdn-cache-busting` gates out itself.
421
481
  | `next-dynamic-lazy-compilation` | Returns early under `shouldUseTurbopack()`, and the harness sets `IS_TURBOPACK_TEST=1`. Webpack-only. 5.1s. |
422
482
  | `skipDeployment: true` files (manifest key `skipDeployment, 236 files`) | See "The `skipDeployment` traps" below. |
423
483
 
484
+ ### A `@gate !deploy` case that passes is reported as a failure
485
+
486
+ 16.4's case-level `// @gate !deploy` does not skip the case in deploy mode: it
487
+ runs it as jest's `.failing` (`test/lib/gate/runtime.ts`), on the assumption that
488
+ it cannot pass on Vercel. Where cdk-nextjs passes it, jest reports _"Failing test
489
+ passed even though it was supposed to fail"_ on every attempt and every type. So
490
+ the skip is in `suites`, and the case says nothing either way:
491
+
492
+ - `metadata-streaming-parallel-routes`, its bundle case (in both
493
+ `parallelRouteMetadata` describes): it reads `.next/server` from the test dir,
494
+ which Vercel's deploy harness doesn't expose and ours, building in place, does.
495
+ - `segment-cache/metadata`, its head-only case: on Vercel a cold ISR path is a
496
+ completed static prerender that sends the page body along with the head.
497
+ cdk-nextjs serves it the `next start` way (see "An on-demand param's RSC
498
+ navigation is a live render" above), so only the head is requested, which is
499
+ what the case asserts.
500
+
501
+ Found on the 2026-10-08 run. `screen.mjs` doesn't flag case-level `@gate`; a new
502
+ one surfaces as this error on the first run that includes it.
503
+
424
504
  ## No signal — the fixture cannot be built here
425
505
 
426
506
  ### `next-config-ts-native-ts/**` needs a `next build` flag the harness does not pass
@@ -1633,9 +1633,18 @@ function resolveFile(pathname, { fileOfTemplate: files, routing }) {
1633
1633
  return exact;
1634
1634
  }
1635
1635
  for (const route of routing.dynamicRoutes ?? []) {
1636
- if (new RegExp(route.sourceRegex, "i").test(pathname)) {
1637
- const template = route.destination?.split("?")[0];
1638
- return template === void 0 ? void 0 : files.get(template);
1636
+ const match = new RegExp(route.sourceRegex, "i").exec(pathname);
1637
+ if (match) {
1638
+ const template = route.destination?.split("?")[0].replace(
1639
+ /\$([1-9]\d*)/g,
1640
+ // A group that matched nothing (an absent `.rsc` suffix) is "", as
1641
+ // in `replaceDestination`; an index past the last group stays as is.
1642
+ (literal, index) => Number(index) < match.length ? match[Number(index)] ?? "" : literal
1643
+ );
1644
+ const file = template === void 0 ? void 0 : files.get(template);
1645
+ if (file !== void 0) {
1646
+ return file;
1647
+ }
1639
1648
  }
1640
1649
  }
1641
1650
  return void 0;
@@ -565,6 +565,13 @@ export declare class TrackedTagMarkers {
565
565
  * for ({@link behindInRender}). Rejects only as `render` does.
566
566
  */
567
567
  catchUp<T>(render: () => Promise<T>): Promise<T>;
568
+ /**
569
+ * Run `fn` outside the request's context (`outsideRequest`), keeping only
570
+ * whether the render was {@link catchUp caught up}: a cache handler method
571
+ * called from inside a staged render, whose clock reads and AWS SDK calls
572
+ * would otherwise end its static stage.
573
+ */
574
+ outsideRender<T>(fn: () => T): T;
568
575
  /** Whether `at` is more than the interval and the grace ago. */
569
576
  private overdue;
570
577
  /**