cdk-nextjs 0.6.1 → 0.7.0

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 (266) hide show
  1. package/.jsii +2467 -2714
  2. package/.jsii.tabl.json +1 -1
  3. package/API.md +1725 -1909
  4. package/CLAUDE.md +2 -1
  5. package/README.md +429 -130
  6. package/assets/lambdas/post-deploy/post-deploy.lambda/index.js +152 -170
  7. package/assets/lambdas/redeploy-stage/redeploy-stage.lambda/index.js +875 -0
  8. package/docs/adapter-runtime.md +185 -0
  9. package/docs/breaking-changes.md +687 -4
  10. package/docs/caching-guide.md +156 -112
  11. package/docs/cdk-nextjs-cfn-exec-policy.json +2 -1
  12. package/docs/development-guide.md +1 -1
  13. package/docs/harness-coverage.md +598 -0
  14. package/docs/init-cache-deployment.md +32 -1
  15. package/docs/next-build-output-guide.md +30 -20
  16. package/examples/README.md +5 -1
  17. package/lib/adapter/__fixtures__/app-playground-base-path.json +4926 -0
  18. package/lib/adapter/__fixtures__/app-playground.json +4917 -0
  19. package/lib/adapter/__fixtures__/pages-i18n.json +1728 -0
  20. package/lib/adapter/adapter.mjs +1548 -185
  21. package/lib/adapter/adapter.test.js +106 -0
  22. package/lib/adapter/aws-cache-store.d.ts +489 -0
  23. package/lib/adapter/aws-cache-store.js +1004 -0
  24. package/lib/adapter/aws-cache-store.test.d.ts +1 -0
  25. package/lib/adapter/aws-cache-store.test.js +361 -0
  26. package/lib/adapter/build-outputs.d.ts +80 -0
  27. package/lib/adapter/build-outputs.js +852 -0
  28. package/lib/adapter/build-outputs.test.d.ts +1 -0
  29. package/lib/adapter/build-outputs.test.js +861 -0
  30. package/lib/adapter/cache-handler.d.ts +9 -2
  31. package/lib/adapter/cache-handler.js +72 -96
  32. package/lib/adapter/cache-handler.mjs +18956 -17774
  33. package/lib/adapter/cache-handler.test.js +263 -31
  34. package/lib/adapter/cache-kinds.d.ts +32 -0
  35. package/lib/adapter/cache-kinds.js +35 -0
  36. package/lib/adapter/cache-kinds.test.d.ts +1 -0
  37. package/lib/adapter/cache-kinds.test.js +69 -0
  38. package/lib/adapter/cache-utils.d.ts +90 -0
  39. package/lib/adapter/cache-utils.js +199 -4
  40. package/lib/adapter/cache-utils.test.d.ts +1 -0
  41. package/lib/adapter/cache-utils.test.js +179 -0
  42. package/lib/adapter/function-groups.d.ts +215 -0
  43. package/lib/adapter/function-groups.js +1099 -0
  44. package/lib/adapter/function-groups.test.d.ts +1 -0
  45. package/lib/adapter/function-groups.test.js +743 -0
  46. package/lib/adapter/init-cache.d.ts +6 -0
  47. package/lib/adapter/init-cache.js +245 -0
  48. package/lib/adapter/init-cache.test.d.ts +1 -0
  49. package/lib/adapter/init-cache.test.js +137 -0
  50. package/lib/adapter/local-file-cache-handler.d.ts +23 -0
  51. package/lib/adapter/local-file-cache-handler.js +52 -1
  52. package/lib/adapter/memory-cache-handler.d.ts +26 -17
  53. package/lib/adapter/memory-cache-handler.js +32 -50
  54. package/lib/adapter/memory-cache-handler.test.js +105 -85
  55. package/lib/adapter/s3-cache-handler.d.ts +179 -40
  56. package/lib/adapter/s3-cache-handler.js +807 -277
  57. package/lib/adapter/s3-cache-handler.test.js +1186 -31
  58. package/lib/adapter/use-cache-common.d.ts +90 -0
  59. package/lib/adapter/use-cache-common.js +250 -0
  60. package/lib/adapter/use-cache-common.test.d.ts +1 -0
  61. package/lib/adapter/use-cache-common.test.js +646 -0
  62. package/lib/adapter/use-cache-default-handler.d.ts +24 -0
  63. package/lib/adapter/use-cache-default-handler.js +90 -0
  64. package/lib/adapter/use-cache-default-handler.mjs +38645 -0
  65. package/lib/adapter/use-cache-handlers.test.d.ts +1 -0
  66. package/lib/adapter/use-cache-handlers.test.js +349 -0
  67. package/lib/adapter/use-cache-remote-handler.d.ts +26 -0
  68. package/lib/adapter/use-cache-remote-handler.js +186 -0
  69. package/lib/{image-optimization/handler.mjs → adapter/use-cache-remote-handler.mjs} +33572 -38798
  70. package/lib/generated-structs/OptionalNextjsApiProps.d.ts +59 -0
  71. package/lib/generated-structs/OptionalNextjsApiProps.js +3 -0
  72. package/lib/generated-structs/OptionalNextjsCacheProps.d.ts +15 -6
  73. package/lib/generated-structs/OptionalNextjsCacheProps.js +1 -1
  74. package/lib/generated-structs/OptionalNextjsContainersProps.d.ts +25 -11
  75. package/lib/generated-structs/OptionalNextjsContainersProps.js +1 -1
  76. package/lib/generated-structs/OptionalNextjsDistributionProps.d.ts +31 -9
  77. package/lib/generated-structs/OptionalNextjsDistributionProps.js +1 -1
  78. package/lib/generated-structs/OptionalNextjsPostDeployProps.d.ts +13 -0
  79. package/lib/generated-structs/OptionalNextjsPostDeployProps.js +1 -1
  80. package/lib/generated-structs/OptionalPostDeployCustomResourceProperties.d.ts +11 -2
  81. package/lib/generated-structs/OptionalPostDeployCustomResourceProperties.js +1 -1
  82. package/lib/index.d.ts +6 -7
  83. package/lib/index.js +2 -4
  84. package/lib/lambdas/post-deploy/post-deploy.lambda.js +3 -2
  85. package/lib/lambdas/post-deploy/prune-cache-bucket.d.ts +7 -3
  86. package/lib/lambdas/post-deploy/prune-cache-bucket.js +57 -71
  87. package/lib/lambdas/post-deploy/prune-cache-bucket.test.d.ts +1 -0
  88. package/lib/lambdas/post-deploy/prune-cache-bucket.test.js +114 -0
  89. package/lib/lambdas/post-deploy/prune-revalidation-table.d.ts +1 -0
  90. package/lib/lambdas/post-deploy/prune-revalidation-table.js +23 -31
  91. package/lib/lambdas/post-deploy/prune-revalidation-table.test.d.ts +1 -0
  92. package/lib/lambdas/post-deploy/prune-revalidation-table.test.js +71 -0
  93. package/lib/lambdas/post-deploy/prune-s3.d.ts +19 -3
  94. package/lib/lambdas/post-deploy/prune-s3.js +54 -15
  95. package/lib/lambdas/post-deploy/prune-s3.test.d.ts +1 -0
  96. package/lib/lambdas/post-deploy/prune-s3.test.js +255 -0
  97. package/lib/lambdas/redeploy-stage/redeploy-stage-function.d.ts +13 -0
  98. package/lib/lambdas/redeploy-stage/redeploy-stage-function.js +55 -0
  99. package/lib/lambdas/redeploy-stage/redeploy-stage.d.ts +24 -0
  100. package/lib/lambdas/redeploy-stage/redeploy-stage.js +68 -0
  101. package/lib/lambdas/redeploy-stage/redeploy-stage.lambda.d.ts +20 -0
  102. package/lib/lambdas/redeploy-stage/redeploy-stage.lambda.js +24 -0
  103. package/lib/lambdas/redeploy-stage/redeploy-stage.test.d.ts +1 -0
  104. package/lib/lambdas/redeploy-stage/redeploy-stage.test.js +93 -0
  105. package/lib/nextjs-api.d.ts +107 -18
  106. package/lib/nextjs-api.js +349 -39
  107. package/lib/nextjs-api.test.js +582 -1
  108. package/lib/nextjs-build/global-containers.Dockerfile +15 -10
  109. package/lib/nextjs-build/nextjs-build.d.ts +339 -63
  110. package/lib/nextjs-build/nextjs-build.js +684 -327
  111. package/lib/nextjs-build/nextjs-build.test.d.ts +1 -0
  112. package/lib/nextjs-build/nextjs-build.test.js +412 -0
  113. package/lib/nextjs-build/patch-fetch.js +1 -1
  114. package/lib/nextjs-build/patch-fetch.test.js +166 -11
  115. package/lib/nextjs-build/regional-containers.Dockerfile +17 -10
  116. package/lib/nextjs-cache.d.ts +82 -4
  117. package/lib/nextjs-cache.js +186 -10
  118. package/lib/nextjs-cache.test.js +176 -1
  119. package/lib/nextjs-compute/nextjs-compute-base-props.d.ts +21 -8
  120. package/lib/nextjs-compute/nextjs-compute-base-props.js +41 -1
  121. package/lib/nextjs-compute/nextjs-compute-base-props.test.d.ts +1 -0
  122. package/lib/nextjs-compute/nextjs-compute-base-props.test.js +44 -0
  123. package/lib/nextjs-compute/nextjs-containers.d.ts +19 -0
  124. package/lib/nextjs-compute/nextjs-containers.js +57 -20
  125. package/lib/nextjs-compute/nextjs-functions.d.ts +122 -7
  126. package/lib/nextjs-compute/nextjs-functions.js +113 -67
  127. package/lib/nextjs-compute/nextjs-functions.test.d.ts +1 -0
  128. package/lib/nextjs-compute/nextjs-functions.test.js +99 -0
  129. package/lib/nextjs-distribution.d.ts +194 -23
  130. package/lib/nextjs-distribution.js +535 -62
  131. package/lib/nextjs-distribution.test.d.ts +1 -0
  132. package/lib/nextjs-distribution.test.js +697 -0
  133. package/lib/nextjs-post-deploy.d.ts +23 -3
  134. package/lib/nextjs-post-deploy.js +14 -10
  135. package/lib/nextjs-post-deploy.test.d.ts +1 -0
  136. package/lib/nextjs-post-deploy.test.js +34 -0
  137. package/lib/nextjs-static-assets.d.ts +23 -4
  138. package/lib/nextjs-static-assets.js +29 -6
  139. package/lib/nextjs-static-assets.test.js +20 -1
  140. package/lib/root-constructs/nextjs-base-construct.d.ts +105 -45
  141. package/lib/root-constructs/nextjs-base-construct.js +144 -34
  142. package/lib/root-constructs/nextjs-global-containers.d.ts +18 -20
  143. package/lib/root-constructs/nextjs-global-containers.js +20 -70
  144. package/lib/root-constructs/nextjs-global-functions.d.ts +26 -28
  145. package/lib/root-constructs/nextjs-global-functions.js +52 -69
  146. package/lib/root-constructs/nextjs-regional-containers.d.ts +25 -5
  147. package/lib/root-constructs/nextjs-regional-containers.js +17 -15
  148. package/lib/root-constructs/nextjs-regional-functions.d.ts +22 -18
  149. package/lib/root-constructs/nextjs-regional-functions.js +8 -25
  150. package/lib/runtime/api-gateway-path.d.ts +34 -0
  151. package/lib/runtime/api-gateway-path.js +71 -0
  152. package/lib/runtime/api-gateway-path.test.d.ts +1 -0
  153. package/lib/runtime/api-gateway-path.test.js +65 -0
  154. package/lib/runtime/case-redirect.d.ts +22 -0
  155. package/lib/runtime/case-redirect.js +63 -0
  156. package/lib/runtime/case-redirect.test.d.ts +1 -0
  157. package/lib/runtime/case-redirect.test.js +32 -0
  158. package/lib/runtime/core.d.ts +234 -0
  159. package/lib/runtime/core.js +1213 -0
  160. package/lib/runtime/core.test.d.ts +1 -0
  161. package/lib/runtime/core.test.js +1678 -0
  162. package/lib/runtime/deployment-root.d.ts +13 -0
  163. package/lib/runtime/deployment-root.js +29 -0
  164. package/lib/runtime/dispatch.d.ts +331 -0
  165. package/lib/runtime/dispatch.js +751 -0
  166. package/lib/runtime/dispatch.test.d.ts +1 -0
  167. package/lib/runtime/dispatch.test.js +853 -0
  168. package/lib/runtime/entrypoints.d.ts +57 -0
  169. package/lib/runtime/entrypoints.js +115 -0
  170. package/lib/runtime/http/abort-signal.d.ts +6 -0
  171. package/lib/runtime/http/abort-signal.js +19 -0
  172. package/lib/runtime/http/abort-signal.test.d.ts +1 -0
  173. package/lib/runtime/http/abort-signal.test.js +49 -0
  174. package/lib/runtime/http/node-server.d.ts +38 -0
  175. package/lib/runtime/http/node-server.js +99 -0
  176. package/lib/runtime/http/node-server.test.d.ts +1 -0
  177. package/lib/runtime/http/node-server.test.js +96 -0
  178. package/lib/runtime/http/node-sink.d.ts +16 -0
  179. package/lib/runtime/http/node-sink.js +48 -0
  180. package/lib/runtime/http/node-sink.test.d.ts +1 -0
  181. package/lib/runtime/http/node-sink.test.js +66 -0
  182. package/lib/runtime/http/request.d.ts +62 -0
  183. package/lib/runtime/http/request.js +134 -0
  184. package/lib/runtime/http/request.test.d.ts +1 -0
  185. package/lib/runtime/http/request.test.js +103 -0
  186. package/lib/runtime/http/response.d.ts +77 -0
  187. package/lib/runtime/http/response.js +229 -0
  188. package/lib/runtime/http/response.test.d.ts +1 -0
  189. package/lib/runtime/http/response.test.js +178 -0
  190. package/lib/runtime/http/sink.d.ts +73 -0
  191. package/lib/runtime/http/sink.js +188 -0
  192. package/lib/runtime/http/sink.test.d.ts +1 -0
  193. package/lib/runtime/http/sink.test.js +257 -0
  194. package/lib/runtime/image-utils.d.ts +78 -0
  195. package/lib/runtime/image-utils.js +129 -0
  196. package/lib/runtime/image-utils.test.d.ts +1 -0
  197. package/lib/runtime/image-utils.test.js +237 -0
  198. package/lib/runtime/image.d.ts +76 -0
  199. package/lib/runtime/image.js +296 -0
  200. package/lib/runtime/image.test.d.ts +1 -0
  201. package/lib/runtime/image.test.js +405 -0
  202. package/lib/runtime/lambda-event.d.ts +4 -0
  203. package/lib/runtime/lambda-event.js +113 -0
  204. package/lib/runtime/lambda-event.test.d.ts +1 -0
  205. package/lib/runtime/lambda-event.test.js +119 -0
  206. package/lib/runtime/lambda.mjs +41595 -0
  207. package/lib/runtime/load-module.d.ts +8 -0
  208. package/lib/runtime/load-module.js +35 -0
  209. package/lib/runtime/manifest.d.ts +215 -0
  210. package/lib/runtime/manifest.js +118 -0
  211. package/lib/runtime/middleware.d.ts +43 -0
  212. package/lib/runtime/middleware.js +113 -0
  213. package/lib/runtime/middleware.test.d.ts +1 -0
  214. package/lib/runtime/middleware.test.js +259 -0
  215. package/lib/runtime/next-modules.d.ts +55 -0
  216. package/lib/runtime/next-modules.js +124 -0
  217. package/lib/runtime/next-modules.test.d.ts +1 -0
  218. package/lib/runtime/next-modules.test.js +34 -0
  219. package/lib/runtime/public-files.d.ts +32 -0
  220. package/lib/runtime/public-files.js +117 -0
  221. package/lib/runtime/public-files.test.d.ts +1 -0
  222. package/lib/runtime/public-files.test.js +97 -0
  223. package/lib/runtime/server.mjs +41588 -0
  224. package/lib/runtime/static-files.d.ts +50 -0
  225. package/lib/runtime/static-files.js +266 -0
  226. package/lib/runtime/static-files.test.d.ts +1 -0
  227. package/lib/runtime/static-files.test.js +118 -0
  228. package/lib/runtime/util.d.ts +28 -0
  229. package/lib/runtime/util.js +79 -0
  230. package/lib/utils/base-path.d.ts +123 -11
  231. package/lib/utils/base-path.js +276 -39
  232. package/lib/utils/base-path.test.js +275 -50
  233. package/lib/utils/get-architecture.d.ts +7 -8
  234. package/lib/utils/get-architecture.js +12 -13
  235. package/lib/utils/zip-directory.d.ts +14 -0
  236. package/lib/utils/zip-directory.js +121 -0
  237. package/lib/utils/zip-directory.test.d.ts +1 -0
  238. package/lib/utils/zip-directory.test.js +99 -0
  239. package/package.json +22 -3
  240. package/scripts/capture-adapter-fixture.mjs +325 -0
  241. package/scripts/e2e-deploy.sh +398 -0
  242. package/scripts/e2e-harness/README.md +707 -0
  243. package/scripts/e2e-harness/app.js +370 -0
  244. package/scripts/e2e-harness/common.sh +249 -0
  245. package/scripts/e2e-harness/screen.mjs +302 -0
  246. package/scripts/e2e-harness/stage-proxy.mjs +145 -0
  247. package/scripts/e2e-harness/type-filters.mjs +47 -0
  248. package/scripts/e2e-logs.sh +75 -0
  249. package/scripts/e2e-offline.sh +91 -0
  250. package/scripts/e2e-sweep.sh +370 -0
  251. package/scripts/e2e-warm.sh +114 -0
  252. package/scripts/zero-config-build.mjs +418 -0
  253. package/docs/1k-concurrent-users-executions.png +0 -0
  254. package/docs/plans/adapter-runtime-release.md +0 -362
  255. package/docs/plans/phase-0-image-function-opt-in.md +0 -295
  256. package/lib/generated-structs/OptionalDockerImageFunctionProps.d.ts +0 -425
  257. package/lib/generated-structs/OptionalDockerImageFunctionProps.js +0 -3
  258. package/lib/image-optimization/handler-utils.d.ts +0 -34
  259. package/lib/image-optimization/handler-utils.js +0 -83
  260. package/lib/image-optimization/handler-utils.test.js +0 -178
  261. package/lib/nextjs-build/functions.Dockerfile +0 -31
  262. package/lib/nextjs-compute/nextjs-image-function.d.ts +0 -53
  263. package/lib/nextjs-compute/nextjs-image-function.js +0 -59
  264. package/lib/utils/experimental-flags.d.ts +0 -24
  265. package/lib/utils/experimental-flags.js +0 -30
  266. /package/lib/{image-optimization/handler-utils.test.d.ts → adapter/adapter.test.d.ts} +0 -0
@@ -0,0 +1,598 @@
1
+ # Next.js compatibility harness — coverage record
2
+
3
+ What has been run from [vercel/next.js's own e2e suite][harness] against a real
4
+ cdk-nextjs deployment, and — for anything that fails — why, with a verdict on
5
+ whether the failure is acceptable or a bug to fix.
6
+
7
+ `test/deploy-tests-manifest.json` is the machine-readable half and the source of
8
+ truth for what runs: `rules.include` (whole files) and `suites` (a file minus
9
+ named cases). Nothing goes in either that has not been watched to pass. Its
10
+ `excluded-notes` carries a one-line verdict for everything deliberately left out,
11
+ pointing back here. See `scripts/e2e-harness/README.md` for how the harness works
12
+ and how to screen a new candidate.
13
+
14
+ ## Verdict key
15
+
16
+ | Verdict | Meaning |
17
+ | ----------------- | ---------------------------------------------------------------------------------------------- |
18
+ | **pass** | Runs and passes against a real deployment. In `rules.include`. |
19
+ | **fixed** | Failed, was root-caused to a cdk-nextjs defect, and the defect is fixed. |
20
+ | **bug** | Fails for a cdk-nextjs reason. Not acceptable. Fix it, then add the file. |
21
+ | **upstream** | Fails because of a bug in next.js or its fixture, not reachable from here. |
22
+ | **unsupported** | Fails because of a deliberate, documented product limitation or design choice. Acceptable. |
23
+ | **CDN-inherent** | Fails for a reason true of any CDN-fronted deployment. Acceptable. |
24
+ | **architectural** | Fails for a reason true of any distributed (multi-instance) cache handler. Acceptable. |
25
+ | **no signal** | next.js skips or gates the file itself, or the fixture cannot be built here. Nothing measured. |
26
+
27
+ "Acceptable" means understood and not worth fixing. Every **bug** should come back
28
+ as a regression test once fixed.
29
+
30
+ ## Status
31
+
32
+ As of 2026-09-23, against next.js 16.3.5. The screening funnel lives in the
33
+ manifest's `screening` block, regenerated by
34
+ `node scripts/e2e-harness/screen.mjs --next ../next.js --write` (`--check` to
35
+ verify, which every e2e-harness run does in shard 1).
36
+
37
+ | Of next.js's 1134 e2e files | Count |
38
+ | ----------------------------- | ----- |
39
+ | edge-free (the hard ceiling) | 931 |
40
+ | candidates after every screen | **0** |
41
+ | deployed and screened | 472 |
42
+ | in `rules.include` | 427 |
43
+ | plus, per-case, in `suites` | 10 |
44
+
45
+ The candidate pool is empty: every file is included, included per-case, or
46
+ disqualified by a screen, and every disqualification that is a cdk-nextjs decision
47
+ has a verdict below. The job now is keeping that true as next.js adds files and as
48
+ each scheduled run re-runs what is already included.
49
+
50
+ | Verdict | Files |
51
+ | ------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
52
+ | pass | 427 whole files, plus the passing cases of the 10 `suites` files |
53
+ | fixed | the defects in the table below |
54
+ | bug | none open |
55
+ | upstream | 2 — `rewrites-destination-query-array`, `incremental-cache-path-traversal` |
56
+ | 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 |
57
+ | CDN-inherent | the 4 skipped cases of `dynamic-route-interpolation` |
58
+ | architectural | the 2 skipped `resume-data-cache` cases |
59
+ | 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 |
60
+
61
+ ### Which types the harness proves
62
+
63
+ The harness runs on all four types: the schedule on `NextjsGlobalFunctions`, the
64
+ other three by `workflow_dispatch` (`nextjs_type`) or by hand, with
65
+ `NextjsRegionalFunctions` behind `scripts/e2e-harness/stage-proxy.mjs`, which puts
66
+ back the stage prefix the harness would otherwise drop. The findings whose
67
+ plumbing differs per deployment type are also back-filled into
68
+ `examples/e2e-tests` (run against `examples/app-playground` on all four types,
69
+ every commit) — the "4-type spec" column of the table under "Fixed". What stays
70
+ harness-only is what cannot coexist with the App Router fixture in one
71
+ `next.config.ts` (`trailingSlash`, `assetPrefix`, `i18n`, the Pages Router) or has
72
+ no per-type divergence (defect 25).
73
+
74
+ ### The 2026-09-29 runs, all four types
75
+
76
+ The whole manifest, 15 shards per type, after the fixes of 2026-09-28/29:
77
+
78
+ | Type | Result |
79
+ | -------------------------- | ----------------------------------------------------------------------------------- |
80
+ | `NextjsRegionalContainers` | 437 / 437, no retries |
81
+ | `NextjsRegionalFunctions` | 434 / 435 (2 files in `excludeByType`); the one failure the `next-form` flake below |
82
+ | `NextjsGlobalContainers` | 411 / 414; all 3 failures and 23 unrun files on one shard, one hung `cdk deploy` |
83
+
84
+ What those fixes were, in the order the runs found them: the Global types'
85
+ `NEXT_E2E_TEST_TIMEOUT` raised to 900000 for CloudFront updates; the Regional
86
+ Functions runtime keeping `//` (`hydration`); the harness URL's host lowercased and
87
+ the container health check moved to `/_next/static/` (it had been caught by
88
+ fixtures' dynamic routes); and Global Containers building absolute URLs with the
89
+ viewer's protocol (`action-forward-loop`: CloudFront reaches the ALB over HTTP
90
+ without a certificate, so `x-forwarded-proto` said `http`).
91
+
92
+ ### Running on `NextjsRegionalFunctions`
93
+
94
+ Run once, on 2026-09-24, behind the local stage proxy (`scripts/e2e-harness/README.md`,
95
+ "Running on `NextjsRegionalFunctions`"): 22 files picked for what differs on that
96
+ type, 17 green, no cdk-nextjs defect. The failures are what to expect on a re-run:
97
+
98
+ | File | Result | Why |
99
+ | --------------------------------------------------- | ------- | ------------------------------------------------------------------------------------------------------------------- |
100
+ | `app-dir/actions-streaming` | fails | The action `fetch`es `location.origin` from the server — `127.0.0.1` from inside the Lambda. Local proxy only. |
101
+ | `app-dir/app-basepath` | 10 / 13 | Action `redirect()` streamed by a server-side fetch of the app's own origin (`ECONNREFUSED 127.0.0.1`). Same cause. |
102
+ | `app-dir/redirect-rewrite-dynamic-basepath` | 1 / 2 | Same, surfacing as a 500 from the runtime's error path. |
103
+ | `invalid-static-asset-404-{app,pages}-asset-prefix` | fail | Nothing serves `assetPrefix` on the regional types; cdk-nextjs warns at synth (`isAssetPrefixUnserved`). |
104
+
105
+ Then in CI on 2026-09-28, the whole manifest (437 files) in 10 shards: 428 green,
106
+ 8 of them only on retry, mostly `basePath` fixtures (the ~90s stage settle in the
107
+ README). Beyond the table above, the same two causes failed four more files, and
108
+ one failure was a cdk-nextjs defect. Everything but `hydration` is now in the
109
+ manifest's `rules.excludeByType["regional-functions"]`:
110
+
111
+ | File | Result | Why |
112
+ | --------------------------------------------------------------------- | ------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
113
+ | `app-dir/asset-prefix-absolute`, `app-dir/asset-prefix-with-basepath` | fail | `assetPrefix` is unserved on the regional types, as above; each logs the synth warning. |
114
+ | `next-form/default/app-dir` | fails | The action `redirect()`s, streamed by a server-side fetch of the app's own origin. Same cause as `app-basepath`. |
115
+ | `app-dir/action-forward-loop` | fails | Action forwarding fetches the request's own origin (`createForwardedActionResponse`, from `initURL`) — `127.0.0.1`. Same cause. |
116
+ | `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. |
117
+
118
+ ## Passing — in `rules.include`
119
+
120
+ The list is the manifest's `rules.include`; it is not duplicated here. Notes worth
121
+ having when one of them goes red:
122
+
123
+ - **Retry passes.** `app-dir/router-autoscroll`, `app-dir/bfcache-regression` and
124
+ `app-dir/third-parties` each failed attempt 0 once on their own scroll or embed
125
+ assertions, with no cdk-nextjs error, and passed on retry 1. The harness runs
126
+ `--retries 1`. Suspect these first if a run goes red for no other reason.
127
+ - **Slowest file:** `instrumentation-client-hook`, ~677s.
128
+ - **Weakest coverage:** the three `chrome-devtools-workspace` files assert only
129
+ `status: 404` in deploy mode (the dev-only endpoint must not be reachable in
130
+ production). Real, but thin.
131
+ - **Collapsed families:** `app-dir/next-config-ts/*` (21 files) and
132
+ `app-dir/next-config-ts-native-mts/*` (18) — each a distinct `next build` whose
133
+ resolved config the adapter reads back out of `required-server-files.json`; and
134
+ `app-dir/scss/*` (29 files), which on our side all test one thing: the emitted
135
+ stylesheet reaching the browser from S3. The only Sass coverage in the project.
136
+ - **`handle-non-hoisted-swc-helpers`** is the one fixture pinning
137
+ `packageManager: npm`. Deploy mode sets `skipInstall`, so its non-hoisted
138
+ `node_modules` layout is never exercised — green means it builds and serves. See
139
+ "npm-pinned fixtures" under Debugging.
140
+ - **`deprecation-warnings`** was once red on a harness bug: `scripts/e2e-logs.sh`
141
+ printed more than next.js treats as build logs. Its output _is_ `next.cliOutput`.
142
+ - **`test-utils-tests/basic`** tests `nextTestSetup` itself — the harness contract,
143
+ not the adapter.
144
+
145
+ ## Passing in part — in `suites`
146
+
147
+ A `suites` file is included with the cases in its `failed` array skipped
148
+ (next.js's `test/get-test-filter.js` turns them into a negative
149
+ `--testNamePattern`, so names must be full jest names, `describe` title included).
150
+ Use this instead of dropping a whole file when the residual failures have a
151
+ verdict below.
152
+
153
+ | File | Cases | Skipped, and why |
154
+ | ------------------------------------------------------------------ | ---------- | ----------------------------------------------------------------------------------------------- |
155
+ | `app-dir/trailingslash` | 6 / 8 | 2 `should revalidate a page with generated static params` — unsupported (async invalidation) |
156
+ | `app-dir/resume-data-cache` | 3 / 5 | 2 `should have consistent data between static and dynamic renders` — architectural |
157
+ | `dynamic-route-interpolation` | 3 / 7 | 4 requesting unencoded `[`/`]` — CDN-inherent |
158
+ | `app-dir/revalidate-path-with-rewrites` | 1 / 2 | `static page` — unsupported (async invalidation) |
159
+ | `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 |
160
+ | `invalid-static-asset-404-pages` (+ `-asset-prefix`, `-base-path`) | 2 / 3 each | Same case, same reason |
161
+ | `next-form/default/app-dir` | 13 / 14 | `flakey`: `should soft-navigate on submit and show the prefetched loading state` — timing |
162
+
163
+ The `next-form` case submits right after the page loads and waits for the loading
164
+ state the router prefetched; where the prefetch has not landed, `#loading` never
165
+ renders. It failed both attempts on Regional Functions and on both Containers
166
+ types at least once in the 2026-09-28/29 runs, and passed on each of them in
167
+ another, with no cdk-nextjs error.
168
+
169
+ ## Fixed
170
+
171
+ `#` labels a row for the rest of this doc. **23**, **37** and **38** were found by
172
+ the four-type suite (`examples/e2e-tests`) rather than the harness: 23 is under
173
+ "Stale-while-revalidate after a tag revalidation", 37 and 38 follow the table.
174
+ There is no **28**: the number was skipped, not withdrawn. So 34 of the 37 fixed
175
+ defects came from the harness and 3 from the four-type suite.
176
+
177
+ "4-type spec" names the `examples/e2e-tests/src/*.test.ts` file guarding it on all
178
+ four deployment types; `—` means harness or unit tests only.
179
+
180
+ | # | Symptom (as a test sees it) | Root cause | Fix / guard | 4-type spec |
181
+ | --- | ------------------------------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------- |
182
+ | 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) |
183
+ | 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` |
184
+ | 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` |
185
+ | 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` |
186
+ | 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` |
187
+ | 6 | `trailingSlash: true` — every canonical `/a/` URL 404s | Output pathnames never carry the slash; `@next/routing` matches exactly | `withTrailingSlashVariants`, `Dispatcher.normalizePathname` | — |
188
+ | 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` | — |
189
+ | 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` | — |
190
+ | 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) | — |
191
+ | 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`) | — |
192
+ | 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` |
193
+ | 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` |
194
+ | 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) |
195
+ | 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` | — |
196
+ | 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" | — |
197
+ | 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 | — |
198
+ | 17 | Pages Router home page `/` 404s (many unrelated-looking symptoms) | `normalizePagePath("/")` is `/index`; nothing registered `/` | `routablePathnames`, `src/adapter/build-outputs.ts` | — |
199
+ | 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) |
200
+ | 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) |
201
+ | 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` | — |
202
+ | 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 | — |
203
+ | 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` | — |
204
+ | 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`) |
205
+ | 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` | — |
206
+ | 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` | — |
207
+ | 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) | — |
208
+ | 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` |
209
+ | 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` |
210
+ | 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`) |
211
+ | 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) |
212
+ | 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` | — |
213
+ | 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` | — |
214
+ | 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 | — |
215
+ | 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) |
216
+
217
+ Found by the four-type suite:
218
+
219
+ - **37** — defect 18's first fix wrote one `?` per _percent-encoded_ character.
220
+ CloudFront URL-decodes the path before matching and `?` matches one byte of the
221
+ decoded path, so `toPathPattern` writes one `?` per UTF-8 byte. A top-level
222
+ `public/` name with no character CloudFront can spell would become a pattern of
223
+ wildcards alone that captures unrelated app routes, so it gets **no behavior**
224
+ and a synth warning; its files reach the server, which reads them from S3.
225
+ Don't go back to per-encoded-character
226
+ patterns: they synthesize, deploy, and never match. The harness's
227
+ `next-image-legacy/unicode` could not catch it — the image optimizer reads S3
228
+ directly — `static-assets.test.ts` did.
229
+ - **38** — `NextjsRegionalContainers` served no `public/` file (the adapter API
230
+ lists `public/` in `staticFiles` only for `output: "export"`). The container
231
+ runtime now lists `public/` off disk at cold start the way `next start` does
232
+ (`src/runtime/public-files.ts`), ahead of app routes; the Lambda types have no
233
+ `public/` in the package and rely on the distribution routing it to S3.
234
+
235
+ ## Upstream — a fixture that cannot pass as written
236
+
237
+ ### `incremental-cache-path-traversal` asserts a 200 for a param shape that throws
238
+
239
+ 1 of 1. Filed as [vercel/next.js#99154](https://github.com/vercel/next.js/issues/99154).
240
+ The file requests `/_next/data/<buildId>/pages-cache/..%2F..%2Fserver-reference-manifest.json`
241
+ and, in its `isNextDeploy && isAdapterTest` branch, asserts status 200 _together
242
+ with_ `pageProps.rest` equal to `['..', '..', 'server-reference-manifest']`. That
243
+ array is exactly the shape `normalizePagePath` rejects (`Requested and resolved
244
+ page mismatch`), and `cacheKey` is passed to `handleResponse` regardless of minimal
245
+ mode, so the two assertions cannot both hold on 16.3.5. The runtime 500 behind it
246
+ is fixed (defect 35: we now answer 200, byte-identical to `next start`). Its
247
+ `describe.skip` is conditional on `__NEXT_CACHE_COMPONENTS`, which the harness does
248
+ not set, so the screen does not remove it. **Recheck:** when #99154 lands, run it
249
+ offline first (`scripts/e2e-offline.sh incremental-cache-path-traversal`).
250
+
251
+ ## Upstream — a bug in the adapter routing library
252
+
253
+ ### A rewrite destination's repeated query key is collapsed to its last value
254
+
255
+ `rewrites-destination-query-array` (1 of 1). Filed as
256
+ [vercel/next.js#99155](https://github.com/vercel/next.js/issues/99155). A rewrite
257
+ to `/?items=1&items=2` reaches the page as `items: "2"`, so
258
+ `props.query.items.join(',')` throws and we answer 500 where `next start` renders
259
+ `1,2`. The loss is two `URLSearchParams#set` calls (should be `append`) in
260
+ `@next/routing@16.3.5`:
261
+
262
+ ```
263
+ packages/next-routing/src/destination.ts:63 newUrl.searchParams.set(key, value)
264
+ packages/next-routing/src/resolve-routes.ts:312 mergedUrl.searchParams.set(key, value)
265
+ ```
266
+
267
+ `next start` uses its in-tree `router-server` and is unaffected. By the time
268
+ `resolveRoutes` returns one value is gone; our `formatTarget` round-trips repeated
269
+ keys fine. No patch point — the library is a published dependency of `next`.
270
+ **Recheck:** `scripts/e2e-offline.sh rewrites-destination-query-array` (500 vs 200
271
+ off the same build today); requeue when the fix ships.
272
+
273
+ ## Unsupported — a product limitation
274
+
275
+ ### `dynamicParams = false`
276
+
277
+ `app-dir/prerender-encoding` (1 of 1). Not an encoding bug — dispatch resolves
278
+ `/sticks%20%26%20stones` to `nxtPid = "sticks & stones"` correctly. With
279
+ `export const dynamicParams = false`, Next.js gates the route's
280
+ `routing.dynamicRoutes` entries behind the preview cookies (`__prerender_bypass`,
281
+ `__next_preview_data`), expecting the platform to serve concrete prerendered paths
282
+ off the CDN and 404 everything else. cdk-nextjs registers only dynamic templates
283
+ (`addPrerenderTemplates`, `src/adapter/build-outputs.ts`), so every param 404s —
284
+ `/plain` too. Removing the flag from a copy of the fixture makes both resolve.
285
+
286
+ Supporting it means routing concrete prerendered paths to the owning entrypoint
287
+ while still supplying the template's `nxtP*` params, which done naively makes
288
+ `/isr/1` resolve to itself and lose `nxtPid` (see `addPrerenderTemplates`'s
289
+ comment). A design change; do it if a user asks. Defects 13 and 21 register
290
+ concrete pathnames in narrower cases (gated-only, and data routes with no ungated
291
+ rule). Side note: an unencoded `&` in a path parses as a query separator
292
+ (`/a&b` → `{nxtPid: "a", b: ""}`), matching Vercel.
293
+
294
+ ## Unsupported — the edge runtime
295
+
296
+ cdk-nextjs refuses at build time to deploy any output whose runtime is not
297
+ `nodejs` (`assertNodeRuntimes`, `src/adapter/build-outputs.ts`): the edge runtime
298
+ is deprecated, and cdk-nextjs supports the Node-runtime `proxy.ts` instead of
299
+ legacy edge `middleware.ts`. The check throws during `next build`, so a fixture
300
+ with _any_ edge route or legacy middleware fails wholesale and `suites` cannot
301
+ rescue it. Both spellings count: `export const runtime = 'edge'` and
302
+ `export const config = { runtime: 'experimental-edge' }` — screening only the first
303
+ undercounts. 203 of 1134 files are disqualified this way.
304
+
305
+ | File | Why |
306
+ | ----------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- |
307
+ | `app-dir/app-static` | ~10 `*-edge` routes. Worth mining for individual cases to port: the densest ISR/caching coverage upstream. |
308
+ | `app-dir/actions/app-action` | Ships a legacy edge `middleware.js`. |
309
+ | `middleware-rewrites/test/index` | Ships a legacy edge `middleware.js`. |
310
+ | `middleware-fetches-with-any-http-method` | Writes its edge middleware inline via `nextTestSetup({ files })`, so `screen.mjs`'s on-disk grep missed it; failed in 7s on `assertNodeRuntimes`. |
311
+
312
+ The `middleware` screen's count is a floor: an inline-`files` fixture only shows up
313
+ as a ~7s build failure naming `assertNodeRuntimes`.
314
+
315
+ ## Unsupported — a design choice
316
+
317
+ ### A missing `/_next/static/*` is a 404 from S3, not from Next.js
318
+
319
+ The 6 `invalid-static-asset-404-*` files, 1 of 3 cases each (via `suites`). The
320
+ case wants a `404` with body exactly `Not Found` for a nonexistent static asset.
321
+ S3 behind Origin Access Control answers the `404` itself (the OAC principal has
322
+ `s3:ListBucket`, so S3 may say a key is absent) with its XML `NoSuchKey` body.
323
+ Matching the body would take a Lambda@Edge origin-response rewrite for one
324
+ assertion. Not done. Real-app cost: none beyond the body. The two passing cases
325
+ (valid asset → 200, invalid non-asset path → the app's 404 page) show Next.js's
326
+ own handling is intact.
327
+
328
+ ### A `POST` body from a non-browser client needs a payload hash
329
+
330
+ `proxy-readable-toweb` (2 of 2, excluded outright). Both cases `POST` JSON from
331
+ the jest process and get `403 InvalidSignatureException`. CloudFront's OAC signs
332
+ the origin request to a Lambda Function URL but supplies only the empty-body hash,
333
+ so the viewer must send `x-amz-content-sha256` (defect 1's root cause from the
334
+ other side). Measured: with the header the request reaches the origin; without it, 403. cdk-nextjs covers the browser (`src/nextjs-build/patch-fetch.js` in the client
335
+ bundle); `next.fetch` is plain Node `fetch` in an unmodified next.js checkout. The
336
+ README's Limitations states it: on `NextjsGlobalFunctions` a non-browser caller
337
+ must hash a `POST`/`PUT` body. The Node-runtime proxy itself is covered by four
338
+ `proxy-*` files in `rules.include`.
339
+
340
+ ### Revalidation re-requested about a second later
341
+
342
+ `app-dir/revalidate-dynamic` (2 of 2, excluded outright),
343
+ `app-dir/revalidate-path-with-rewrites` (`static page`) and `app-dir/trailingslash`
344
+ (2 of 8), the latter two via `suites`. Each loads a prerendered page, revalidates
345
+ via a route handler (which returns `revalidated: true`), refreshes, and expects new
346
+ content. The refresh is served from the edge — prerendered pages come back
347
+ `cache-control: s-maxage=31536000` and the second request reports
348
+ `x-cache: Hit from cloudfront`. cdk-nextjs does invalidate (defect 9, including
349
+ build-time prerenders), but `CreateInvalidation` is fire-and-forget with no bounded
350
+ completion time. The discriminator is how long the case waits: next.js's `retry()`
351
+ (`test/lib/next-test-utils.ts`) defaults to 3000ms; `segment-cache/prefetch-inlining`
352
+ passes 15000 and is green, these take the default or re-request immediately.
353
+ Vercel purges its CDN synchronously, which is why upstream does not gate these.
354
+ Making them pass would mean blocking every revalidation on an invalidation.
355
+
356
+ **Tell for a real regression instead:** the _same_ random value across every
357
+ attempt, on a case with no prerender to invalidate. That was defect 34 — the
358
+ `dynamic page` case of `revalidate-path-with-rewrites` had passed once by luck.
359
+
360
+ ### A test's `env` reaches `next build`, not the deployed app
361
+
362
+ Per-test `env` from `nextTestSetup({ env })` is set for `next build`, so values
363
+ inlined at build time work, but not on the deployed function or task: a
364
+ request-time read of a non-inlined `process.env` value returns `undefined`.
365
+ next-deploy.ts hands the deploy script the test's `env` merged into the runner's
366
+ whole environment, so the script can't tell them apart, and forwarding it all
367
+ would also forward the runner's AWS credentials. No file in the manifest hits this.
368
+
369
+ ## CDN-inherent — excluded, acceptable
370
+
371
+ ### CloudFront rejects unencoded `[` and `]` in a path
372
+
373
+ `dynamic-route-interpolation`, 4 of 7 (the other 3 via `suites`). The four request
374
+ literal brackets (`/blog/[slug]`, `/api/dynamic/[abc]`) and CloudFront answers
375
+ before the Lambda:
376
+
377
+ ```
378
+ $ curl -sg --path-as-is -D - -o /dev/null 'https://…/blog/[slug]'
379
+ HTTP/2 400
380
+ content-length: 0
381
+ x-cache: Error from cloudfront
382
+ ```
383
+
384
+ Percent-encoded (`/blog/%5Bslug%5D`) the same routes render correctly. `[`/`]`
385
+ are `gen-delims` and not legal in a path, so CloudFront is conformant.
386
+
387
+ ## Architectural — excluded, acceptable
388
+
389
+ ### Stale-while-revalidate after a tag revalidation
390
+
391
+ `app-dir/resume-data-cache`, 2 of 5 (the `it.each` "should have consistent data
392
+ between static and dynamic renders" cases; the other 3 via `suites`). Each
393
+ revalidates a tag and asserts the next dynamic RSC request still returns the
394
+ **stale** value once. cdk-nextjs is _too fresh_. Next.js tracks tag staleness in
395
+ `tagsManifest`, an in-process `Map`
396
+ (`packages/next/src/server/lib/incremental-cache/tags-manifest.external.ts`), and a
397
+ cache handler has no channel for "stale, serve it anyway": `lastModified: -1`
398
+ forces a blocking render. Fixing it needs Next.js to expose tag staleness to the
399
+ handler interface.
400
+
401
+ Don't "fix" it by returning `null` instead: on a PPR route with a `fallback` a hard
402
+ miss is answered from the fallback shell — uncacheable, never upgraded — which was
403
+ defect 23. Our handler returns `{ lastModified: -1, value }` for every tag-expired
404
+ response entry, and still deletes/returns `null` for a fetch entry.
405
+
406
+ The "fetch cache" case would also fail on its own: it prefetches the same URL with
407
+ the same `_rsc` hash as the "use cache" case, and CloudFront returns the earlier
408
+ payload — the problem next.js's `cdn-cache-busting` gates out itself.
409
+
410
+ ## No signal — next.js skips these itself
411
+
412
+ | File | Why |
413
+ | ----------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------ |
414
+ | `app-dir/segment-cache/refresh` | `describe.skip` upstream ("Disabled because too flaky"). "Passes" in ~5s without deploying. |
415
+ | `app-dir/segment-cache/cdn-cache-busting` | next.js skips it under `isNextDeploy` — deploy mode gating out what cannot work behind a CDN. |
416
+ | `app-dir/app-fetch-deduping` | Branches on `isNextStart`/`isNextDev` only; deploy mode gets `it('should skip other scenarios')`. 4s. |
417
+ | `cpu-profiling/cpu-profiling-dev` | `if (!isNextDev) { it('skip…'); return }` _after_ `nextTestSetup` — a full 187s deploy that asserts nothing. |
418
+ | `invalid-server-options` | Calls `next()` in-process and asserts on option-validation errors. Nothing built or deployed. 3.5s. |
419
+ | `next-dynamic-lazy-compilation` | Returns early under `shouldUseTurbopack()`, and the harness sets `IS_TURBOPACK_TEST=1`. Webpack-only. 5.1s. |
420
+ | `skipDeployment: true` files (manifest key `skipDeployment, 236 files`) | See "The `skipDeployment` traps" below. |
421
+
422
+ ## No signal — the fixture cannot be built here
423
+
424
+ ### `next-config-ts-native-ts/**` needs a `next build` flag the harness does not pass
425
+
426
+ All 18 files fail in `next build` with `ERR_REQUIRE_ASYNC_MODULE` or
427
+ `ReferenceError: await is not defined`. Every fixture uses top-level `await` in
428
+ `next.config.ts`, which only Node's native TS loader handles; that loader is gated
429
+ on `__NEXT_NODE_NATIVE_TS_LOADER_ENABLED`, set only by
430
+ `next build --experimental-next-config-strip-types`. A plain `next build` of a copy
431
+ with no adapter fails identically; with the flag it succeeds.
432
+
433
+ Adding the flag to `scripts/e2e-deploy.sh` would apply to the whole run and move
434
+ the 21 green `next-config-ts/*` files off the swc-transpile path they exist to
435
+ cover; the harness cannot set per-fixture build flags. The one question these files
436
+ ask cdk-nextjs — does `modifyConfig` still apply through the native loader? — is
437
+ asserted on every PR by `scripts/zero-config-build.mjs`. `next-config-ts-native-mts/**`
438
+ needs no flag (`.mts` is ESM either way) and is green.
439
+
440
+ ## Debugging a harness failure
441
+
442
+ ### Reading a harness log
443
+
444
+ - **Wholesale failure at ~120s or ~240s** with
445
+ `Exceeded timeout of … ms for a hook` in `beforeAll`: the deploy outran jest's
446
+ hook timeout (`NEXT_E2E_TEST_TIMEOUT`); nothing was asserted. Read the retry, not
447
+ attempt 0, and read durations, not case names — a file can report 8 of 8
448
+ "routing" cases failing when none ran. A deploy that suddenly takes a full
449
+ CloudFormation update (~110s+) instead of a hotswap (~50s) is the usual cause —
450
+ e.g. churning bucket `Tags` once did this; see the README. On the Global
451
+ types, `rejected changes: DistributionConfig` in the log means a CloudFront
452
+ update, which takes 8-10 minutes; if the next file logs `an earlier cdk deploy
453
+ … is still running; waiting`, it is waiting on that update, not failing on its
454
+ own. A wait that ends in `outlived 600s; stopping it` means the earlier deploy
455
+ hung: that file failed on it, and this one starts from a clean slate.
456
+ - **A pass in under ~10s deployed nothing.** A real file spends 100s+ on
457
+ `next build` plus a hotswap. `run-tests.js --timings` prints per-file duration —
458
+ the only signal for a passing file, since output prints on failure only.
459
+ - **But a slow pass can still be empty:** check for an early `return` after
460
+ `nextTestSetup` (`cpu-profiling-dev`) alongside the timing, not instead of it.
461
+ - **Playwright traces** land in `test/traces/` (`run-tests.js` sets
462
+ `TRACE_PLAYWRIGHT=true`). The `0-trace.network` entry shows every request's real
463
+ status and headers — this is how defect 1's 403s were found while the Lambda's own
464
+ logs were clean, and how defect 23's fallback shell was spotted.
465
+ - **Server logs:** `scripts/e2e-logs.sh` output _is_ `next.cliOutput`; anything
466
+ extra it prints can fail log-asserting fixtures (`deprecation-warnings`).
467
+
468
+ ### The `skipDeployment` traps
469
+
470
+ - `nextTestSetup({ skipDeployment: true })` replaces the file with
471
+ `it.only('should skip next deploy')` (`test/lib/e2e-utils/index.ts`): a ~4s pass,
472
+ nothing deployed. Two files once sat in `rules.include` on such a pass.
473
+ `screen.mjs` disqualifies it.
474
+ - **It is not always in the test file.** It can be in a sibling `shared.util.ts`, or
475
+ the test is a two-line `require('./client.test')` wrapper
476
+ (`app-dir/cache-components-errors/*`). `screen.mjs` follows relative
477
+ imports two levels deep for this screen only; mode-gate screens stay
478
+ test-file-only, since a helper's `isNextDev` branch may cover only some cases.
479
+ - **`skipDeployment: !isAdapterTest` is different.** Ten files gate on
480
+ `NEXT_ENABLE_ADAPTER === '1'`; unset, they take the skip path (how
481
+ `partial-fallback-shell-upgrade` "passed" in 5.4s once). The harness exports
482
+ `NEXT_ENABLE_ADAPTER=1`, and `screen.mjs` deliberately does not disqualify these.
483
+ - **Mode gates after `nextTestSetup`** cost a full deploy and prove nothing, and
484
+ the `mode-gated` screen cannot see them.
485
+
486
+ ### `NEXT_PRIVATE_TEST_MODE` is what makes a PPR response splittable
487
+
488
+ PPR tests split the body on `<!-- PPR_BOUNDARY_SENTINEL -->`
489
+ (`splitResponseWithPPRSentinel`, `test/lib/e2e-utils/ppr.ts`). The sentinel is
490
+ emitted only under `process.env.__NEXT_TEST_MODE`, a build-time constant inlined by
491
+ `define-env.ts`; the harness's `next.config` snippet aliases
492
+ `NEXT_PRIVATE_TEST_MODE` → `__NEXT_TEST_MODE`, and `scripts/e2e-deploy.sh` exports
493
+ it. Without it everything lands in `static$` and every "dynamic part not in the
494
+ static part" assertion fails — a missing env var, not a defect. It once masked
495
+ defect 14: after the var was set, `sub-shell-generation` still failed 6 of 7 but on
496
+ a _different_ assertion. **If the shape of a failure changes after an environment
497
+ fix, there is a second problem.**
498
+
499
+ ### Reproduce offline against `next start` on the same build
500
+
501
+ `scripts/e2e-offline.sh <fixture>` builds the fixture through the adapter and
502
+ serves it twice — our container shell and `next start` — so a disagreement is one
503
+ `diff` of two responses (see the README, "Debugging one failing file without
504
+ deploying"). Defects 11, 13, 14, 15, 20, 21, 22, 33 and 35 and both upstream
505
+ verdicts were settled this way. Limits:
506
+
507
+ - The runtime cache reads S3 only, so a fully-prerendered route answers
508
+ `invariant: cache entry required but not generated` offline. Read the seed
509
+ directory (`.next/cdk-nextjs-init-cache`) instead — it is how defect 14's missing
510
+ shells were found (the seeding loop had no jest coverage then).
511
+ - **Offline agreement only rules out the request path, not the deploy.** Seeding
512
+ happens in the `BucketDeployment`, so an offline app has no seeded cache. Defect
513
+ 32 agreed offline (both 404) and was a seeding bug.
514
+ - For a question below the HTTP layer, call the library directly: defect 11 ran
515
+ `resolveRoutes` from a shell; defect 35 fed candidate param shapes to next's built
516
+ `dist` to find which one `_getPathname` accepts.
517
+ - An oracle `next start` also settles "is this ours?" for deployed-type runs: the
518
+ Regional Functions back-fill ran `next start` on the same build with the same
519
+ `basePath` (`NEXTJS_BASE_PATH=/prod`) and found two failures that were the test's
520
+ own bug, one wrong expectation, and one real defect (36).
521
+
522
+ ### Who answered? Reading response headers
523
+
524
+ | You see | It means |
525
+ | --------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------- |
526
+ | `x-cache: Hit from cloudfront` | Edge cache served it; the origin never saw the request (stale content after a revalidate: invalidation timing). |
527
+ | `x-cache: Error from cloudfront`, `400`, `content-length: 0` | CloudFront rejected the request itself (e.g. literal `[`/`]`). |
528
+ | A `400` with no `x-amzn-*` headers | CloudFront answered — says nothing about _why_, and not that a function cannot run (defect 31's bare `//`). |
529
+ | `x-cache: FunctionGeneratedResponse from cloudfront` | A viewer-request CloudFront Function answered (defect 31's 308). Assert as a prefix. |
530
+ | `403`, `x-amzn-errortype: InvalidSignatureException` | Lambda Function URL rejected OAC's signature: an unhashed body (1, `proxy-readable-toweb`) or a path the URL canonicalizes (31). |
531
+ | `403` for a static asset | S3 via OAC for a missing key (no `ListBucket`), or API Gateway for a path outside the stage (36). |
532
+ | `accept-ranges: bytes`, `last-modified: … 1980`, weak `etag` | `send` served a file off disk — a `static-file` dispatch, typed by file extension (defect 12). |
533
+ | `x-nextjs-cache: HIT`/`MISS`/`STALE` | The Next.js cache path. `HIT` with a wrong body/status → look at what was seeded (4, 32); `MISS` where a prerender should exist → seeding (26). |
534
+ | `cache-control: private, no-store` + `x-nextjs-postponed: 1`, no `x-nextjs-cache` | Answered from a PPR fallback shell, not a cache entry (defect 23). |
535
+
536
+ Useful probes: `curl -sI '…/?_rsc=probe' -H 'RSC: 1' -H 'Next-Router-Prefetch: 1'
537
+ -H 'Next-Router-Segment-Prefetch: /__PAGE__'` for a segment prefetch; call it twice
538
+ to see edge caching. `curl -g --path-as-is` keeps literal brackets and `//` on the
539
+ wire (a browser or Playwright `APIRequestContext` cannot send a `\` at all — WHATWG
540
+ parsing turns it into `/`). In Playwright, read duplicate headers with
541
+ `headersArray()`; `headers()` folds two `vary`s into one string.
542
+
543
+ ### Symptom patterns worth recognizing
544
+
545
+ - **Unrelated-looking failures sharing one URL.** Defect 17's eleven files failed on
546
+ empty text, `JSON.parse('')`, a `naturalWidth` of `null` — every failing case
547
+ requested `/`, every passing one did not. Group failures by URL before by assertion.
548
+ - **A 404 page still hydrates.** A built-in 404 is a Next.js document; the browser
549
+ logs a single "Failed to load resource: 404" and the test times out on a selector
550
+ (defects 13, 17). Probe the page URL directly before debugging hydration.
551
+ - **jest misattributes un-awaited rejections.** A fixture's `bundles.forEach(async …)`
552
+ reports its failure against whichever test is running (defect 16).
553
+ - **A route returning 200 can still have failed.** A fixture that catches and
554
+ returns `{revalidated: false}` passes its status assertion; read the body and the
555
+ server log (defect 33).
556
+ - **Worker errors do not reach the page console.** They fire on the worker's
557
+ `error` event, which fixtures rarely listen for (defect 25).
558
+ - **Wrong `content-type` on RSC breaks things far away:** a `NaN` navigation
559
+ duration, or an action `redirect()` with no head (next fetches the target and
560
+ cancels it unless it is `text/x-component`) (defect 2).
561
+ - **`JSON.stringify` hides `undefined`-valued keys**, so logging a query object can
562
+ lie; inspect with a debugger or `Object.keys` (defect 11).
563
+ - **Races need more than one measurement.** Re-run a suspected defect several
564
+ times before filing it: a server-action redirect that looked like a third
565
+ instance of defect 36 was a pre-hydration click (native form POST), fixed by
566
+ waiting for the client router.
567
+ - **Instrument before guessing.** Defect 3 survived three plausible theories; one
568
+ run with `[probe]` logs in the deployed function settled it.
569
+ - **For slowness, split the time.** Compare Lambda duration with end-to-end, repeat
570
+ with `Accept-Encoding: identity` to rule out gzip/CDN, and look at the stored
571
+ S3 object's size and shape (defect 27: an 11.6 MiB entry for a ~1 MiB page).
572
+
573
+ ### Deploy-side traps
574
+
575
+ - **`cdk deploy --hotswap` does not check custom resources.** It invokes them with
576
+ placeholder response URLs and prints success even when they send `FAILED`. If a
577
+ cache looks partly seeded, count objects in the cache bucket against the seed
578
+ directory and read the `BucketDeployment` Lambda's log (defect 26).
579
+ - **Hotswap never runs CloudFormation**, so the post-deploy custom resource does
580
+ not fire between fixtures — anything it does (e.g. pruning the previous build) is not
581
+ under harness coverage.
582
+ - **`NEXT_DEPLOYMENT_ID` makes every build id the same constant**; if one fixture
583
+ serves another's content, check for one cache prefix shared across builds
584
+ (defect 10).
585
+ - **npm-pinned fixtures** (`packageManager: npm@…`): npm with `--prefer-offline`
586
+ treats a stale cached packument as final (`ETARGET … No matching version found for
587
+ next@16.3.5`), so `e2e-deploy.sh` drops the flag for npm; and pnpm refuses to run
588
+ the hardcoded `pnpm post-build` in a project pinned to npm, so `e2e-deploy.sh`
589
+ exports `npm_config_package_manager_strict=false`.
590
+
591
+ ### Keeping verdicts durable
592
+
593
+ `screen.mjs`'s **verdict** screen keeps a file out of `candidates` only if its
594
+ `excluded-notes` key is the file path, or a directory ending in `/**`. A verdict written
595
+ under a prose key ("revalidation behind the CDN, two files") is not matched, and
596
+ the file gets re-offered and re-deployed. Key new verdicts by path.
597
+
598
+ [harness]: https://nextjs.org/docs/app/api-reference/adapters/testing-adapters