@opennextjs/aws 4.1.13 → 4.1.15

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 (45) hide show
  1. package/dist/adapters/cache.d.ts +21 -1
  2. package/dist/adapters/cache.js +138 -30
  3. package/dist/adapters/composable-cache.d.ts +20 -0
  4. package/dist/adapters/composable-cache.js +33 -8
  5. package/dist/adapters/config/index.js +14 -4
  6. package/dist/adapters/config/util.d.ts +19 -0
  7. package/dist/adapters/config/util.js +21 -0
  8. package/dist/adapters/edge-adapter.js +2 -11
  9. package/dist/adapters/image-optimization-adapter.js +8 -2
  10. package/dist/build/createAssets.d.ts +3 -0
  11. package/dist/build/createAssets.js +11 -8
  12. package/dist/build/createImageOptimizationBundle.js +0 -1
  13. package/dist/build/helper.js +1 -1
  14. package/dist/build/installDeps.js +6 -7
  15. package/dist/build/patch/patches/patchNextServer.d.ts +25 -0
  16. package/dist/build/patch/patches/patchNextServer.js +48 -0
  17. package/dist/core/requestHandler.js +5 -0
  18. package/dist/core/resolve.d.ts +8 -1
  19. package/dist/core/revalidate.d.ts +68 -0
  20. package/dist/core/revalidate.js +143 -0
  21. package/dist/core/routing/cacheInterceptor.js +27 -4
  22. package/dist/core/routing/matcher.js +16 -4
  23. package/dist/core/routing/middleware.d.ts +4 -0
  24. package/dist/core/routing/middleware.js +19 -0
  25. package/dist/core/routing/routeParamPlaceholder.d.ts +14 -0
  26. package/dist/core/routing/routeParamPlaceholder.js +42 -0
  27. package/dist/core/routingHandler.js +8 -2
  28. package/dist/core/util.js +5 -0
  29. package/dist/http/framing.d.ts +35 -0
  30. package/dist/http/framing.js +181 -0
  31. package/dist/http/util.d.ts +11 -0
  32. package/dist/http/util.js +26 -0
  33. package/dist/overrides/cdnInvalidation/cloudfront.js +2 -1
  34. package/dist/overrides/proxyExternalRequest/fetch.js +5 -1
  35. package/dist/overrides/proxyExternalRequest/node.js +5 -0
  36. package/dist/overrides/wrappers/express-dev.js +3 -5
  37. package/dist/overrides/wrappers/node.js +2 -3
  38. package/dist/plugins/edge.js +3 -2
  39. package/dist/types/cache.d.ts +12 -4
  40. package/dist/types/global.d.ts +7 -0
  41. package/dist/types/open-next.d.ts +4 -1
  42. package/dist/types/overrides.d.ts +47 -0
  43. package/dist/utils/cache.d.ts +14 -0
  44. package/dist/utils/cache.js +21 -1
  45. package/package.json +3 -3
@@ -1,6 +1,6 @@
1
1
  import fs from "node:fs";
2
2
  import path from "node:path";
3
- import { loadConfig } from "../adapters/config/util.js";
3
+ import { getOpenNextBuildId, loadConfig } from "../adapters/config/util.js";
4
4
  import { safeParseJsonFile } from "../utils/safe-json-parse.js";
5
5
  import logger from "../logger.js";
6
6
  import { isBinaryContentType } from "../utils/binary.js";
@@ -59,6 +59,9 @@ export function createStaticAssets(options, { useBasePath = false } = {}) {
59
59
  /**
60
60
  * Create the cache assets.
61
61
  *
62
+ * The cache entries, the fetch cache entries and the tag cache seed are prefixed with the Open Next build id
63
+ * so that they match the keys used at runtime.
64
+ *
62
65
  * @param options Build options.
63
66
  * @returns Whether the tag cache is used, and the meta files collected.
64
67
  */
@@ -66,9 +69,9 @@ export function createCacheAssets(options) {
66
69
  logger.info("Bundling cache assets...");
67
70
  const { appBuildOutputPath, outputDir } = options;
68
71
  const packagePath = buildHelper.getPackagePath(options);
69
- const buildId = buildHelper.getBuildId(options);
70
72
  const nextConfig = loadConfig(path.join(appBuildOutputPath, ".next"));
71
- const openNextBuildId = nextConfig.deploymentId ?? buildId;
73
+ // Must match `process.env.OPEN_NEXT_BUILD_ID` used at runtime to read the cache entries and the tags
74
+ const openNextBuildId = getOpenNextBuildId(nextConfig, buildHelper.getBuildId(options));
72
75
  let useTagCache = false;
73
76
  const dotNextPath = path.join(appBuildOutputPath, ".next/standalone", packagePath);
74
77
  const outputCachePath = path.join(outputDir, "cache", openNextBuildId);
@@ -180,8 +183,8 @@ export function createCacheAssets(options) {
180
183
  .split(",")
181
184
  .forEach((tag) => {
182
185
  metaFiles.push({
183
- tag: { S: path.posix.join(buildId, tag.trim()) },
184
- path: { S: path.posix.join(buildId, cacheKey) },
186
+ tag: { S: path.posix.join(openNextBuildId, tag.trim()) },
187
+ path: { S: path.posix.join(openNextBuildId, cacheKey) },
185
188
  revalidatedAt: { N: "1" },
186
189
  });
187
190
  });
@@ -190,7 +193,7 @@ export function createCacheAssets(options) {
190
193
  // Copy fetch-cache to cache folder
191
194
  const fetchCachePath = path.join(appBuildOutputPath, ".next/cache/fetch-cache");
192
195
  if (fs.existsSync(fetchCachePath)) {
193
- const fetchOutputPath = path.join(outputDir, "cache", "__fetch", buildId);
196
+ const fetchOutputPath = path.join(outputDir, "cache", "__fetch", openNextBuildId);
194
197
  fs.mkdirSync(fetchOutputPath, { recursive: true });
195
198
  fs.cpSync(fetchCachePath, fetchOutputPath, { recursive: true });
196
199
  buildHelper.traverseFiles(fetchCachePath, () => true, ({ absolutePath, relativePath }) => {
@@ -198,9 +201,9 @@ export function createCacheAssets(options) {
198
201
  const fileData = safeParseJsonFile(fileContent, absolutePath);
199
202
  fileData?.tags?.forEach((tag) => {
200
203
  metaFiles.push({
201
- tag: { S: path.posix.join(buildId, tag) },
204
+ tag: { S: path.posix.join(openNextBuildId, tag) },
202
205
  path: {
203
- S: path.posix.join(buildId, relativePath),
206
+ S: path.posix.join(openNextBuildId, relativePath),
204
207
  },
205
208
  revalidatedAt: { N: "1" },
206
209
  });
@@ -83,7 +83,6 @@ export async function createImageOptimizationBundle(options) {
83
83
  arch: isDev ? undefined : "arm64",
84
84
  // Use the local platform in dev
85
85
  os: isDev ? os.platform() : "linux",
86
- nodeVersion: "18",
87
86
  libc: "glibc",
88
87
  });
89
88
  }
@@ -381,7 +381,7 @@ export function getPackagePath(options) {
381
381
  * @returns the Next.js runtime used: "webpack" or "turbopack"
382
382
  */
383
383
  export function getBundlerRuntime(options) {
384
- const dotNextServerPath = path.join(options.appPath, ".next/server");
384
+ const dotNextServerPath = path.join(options.appBuildOutputPath, ".next/server");
385
385
  if (fs.existsSync(path.join(dotNextServerPath, "webpack-runtime.js"))) {
386
386
  return "webpack";
387
387
  }
@@ -12,20 +12,19 @@ export function installDependencies(outputDir, installOptions) {
12
12
  // First we create a tempDir
13
13
  const tempInstallDir = fs.mkdtempSync(path.join(os.tmpdir(), `open-next-install-${name}`));
14
14
  logger.info(`Installing dependencies for ${name}...`);
15
+ if (installOptions.nodeVersion) {
16
+ logger.warn("install.nodeVersion is deprecated and ignored (npm no longer supports --target)");
17
+ }
15
18
  // We then need to run install in the tempDir
16
19
  // We don't install in the output dir directly because it could contain a package.json, and npm would then try to reinstall not complete deps from tracing the files
17
- const archOption = installOptions.arch
18
- ? `--arch=${installOptions.arch}`
19
- : "";
20
- const targetOption = installOptions.nodeVersion
21
- ? `--target=${installOptions.nodeVersion}`
22
- : "";
20
+ // npm filters platform-specific optional dependencies (e.g. sharp's @img/* packages) by --cpu; npm 12 rejects (npm 11 warns) both --arch and --target
21
+ const cpuOption = installOptions.arch ? `--cpu=${installOptions.arch}` : "";
23
22
  const libcOption = installOptions.libc
24
23
  ? `--libc=${installOptions.libc}`
25
24
  : "";
26
25
  const osOption = `--os=${installOptions.os ?? "linux"}`;
27
26
  const additionalArgs = installOptions.additionalArgs ?? "";
28
- const installCommand = `npm install ${osOption} ${archOption} ${targetOption} ${libcOption} ${additionalArgs} ${installOptions.packages.join(" ")}`;
27
+ const installCommand = `npm install ${osOption} ${cpuOption} ${libcOption} ${additionalArgs} ${installOptions.packages.join(" ")}`;
29
28
  execSync(installCommand, {
30
29
  stdio: "pipe",
31
30
  cwd: tempInstallDir,
@@ -3,6 +3,31 @@ export declare const disablePreloadingRule = "\nrule:\n kind: statement_block\n
3
3
  export declare const removeMiddlewareManifestRule = "\nrule:\n kind: statement_block\n inside:\n kind: method_definition\n has:\n kind: property_identifier\n regex: ^getMiddlewareManifest$\nfix:\n '{return null;}'\n";
4
4
  export declare const emptyHandleNextImageRequestRule = "\nrule:\n kind: assignment_expression\n pattern: this.handleNextImageRequest = $VALUE\n inside:\n kind: method_definition\n stopBy: end\n has:\n kind: property_identifier\n regex: ^constructor$\n inside:\n kind: class_body\nfix:\n this.handleNextImageRequest = async (req, res, parsedUrl) => false\n";
5
5
  export declare const provideInternalWaitUntil = "\nrule:\n kind: return_statement\n inside:\n kind: method_definition\n stopBy: end\n has:\n kind: property_identifier\n regex: getInternalWaitUntil\n\nfix:\n return globalThis.__openNextAls.getStore()?.waitUntil;\n";
6
+ /**
7
+ * Exposes `NextNodeServer#revalidate` to `res.revalidate()` of the Pages Router API routes.
8
+ *
9
+ * Starting with Next 15.4, `res.revalidate()` uses the `revalidate` function of the router server
10
+ * context (`globalThis[Symbol.for("@next/router-server-methods")]`) to revalidate in-process.
11
+ * The context is populated by the router server which is not used by OpenNext.
12
+ * Without it, `res.revalidate()` throws
13
+ * `Invariant: missing internal router-server-methods this is an internal bug`.
14
+ *
15
+ * `NextNodeServer#handleCatchallRenderRequest` creates the context for environments without
16
+ * router server, this rule adds `revalidate` to it:
17
+ *
18
+ * ```js
19
+ * routerServerGlobal[RouterServerContextSymbol][relativeProjectDir] = {
20
+ * render404: this.render404.bind(this),
21
+ * // added by the rule
22
+ * revalidate: this.revalidate.bind(this),
23
+ * };
24
+ * ```
25
+ *
26
+ * OpenNext wraps `NextNodeServer#revalidate` at runtime, see `core/revalidate.ts`.
27
+ *
28
+ * https://github.com/vercel/next.js/blob/4e6c27b/packages/next/src/server/api-utils/node/api-resolver.ts
29
+ */
30
+ export declare const exposeRevalidateRule = "\nrule:\n pattern:\n context: \"({ render404: this.render404.bind(this) })\"\n selector: pair\n inside:\n kind: object\n not:\n has:\n kind: pair\n has:\n field: key\n regex: ^revalidate$\nfix: |-\n render404: this.render404.bind(this),\n revalidate: this.revalidate.bind(this)\n";
6
31
  /**
7
32
  * Swaps the body for a throwing implementation
8
33
  *
@@ -60,6 +60,47 @@ rule:
60
60
  fix:
61
61
  return globalThis.__openNextAls.getStore()?.waitUntil;
62
62
  `;
63
+ /**
64
+ * Exposes `NextNodeServer#revalidate` to `res.revalidate()` of the Pages Router API routes.
65
+ *
66
+ * Starting with Next 15.4, `res.revalidate()` uses the `revalidate` function of the router server
67
+ * context (`globalThis[Symbol.for("@next/router-server-methods")]`) to revalidate in-process.
68
+ * The context is populated by the router server which is not used by OpenNext.
69
+ * Without it, `res.revalidate()` throws
70
+ * `Invariant: missing internal router-server-methods this is an internal bug`.
71
+ *
72
+ * `NextNodeServer#handleCatchallRenderRequest` creates the context for environments without
73
+ * router server, this rule adds `revalidate` to it:
74
+ *
75
+ * ```js
76
+ * routerServerGlobal[RouterServerContextSymbol][relativeProjectDir] = {
77
+ * render404: this.render404.bind(this),
78
+ * // added by the rule
79
+ * revalidate: this.revalidate.bind(this),
80
+ * };
81
+ * ```
82
+ *
83
+ * OpenNext wraps `NextNodeServer#revalidate` at runtime, see `core/revalidate.ts`.
84
+ *
85
+ * https://github.com/vercel/next.js/blob/4e6c27b/packages/next/src/server/api-utils/node/api-resolver.ts
86
+ */
87
+ export const exposeRevalidateRule = `
88
+ rule:
89
+ pattern:
90
+ context: "({ render404: this.render404.bind(this) })"
91
+ selector: pair
92
+ inside:
93
+ kind: object
94
+ not:
95
+ has:
96
+ kind: pair
97
+ has:
98
+ field: key
99
+ regex: ^revalidate$
100
+ fix: |-
101
+ render404: this.render404.bind(this),
102
+ revalidate: this.revalidate.bind(this)
103
+ `;
63
104
  /**
64
105
  * Swaps the body for a throwing implementation
65
106
  *
@@ -138,6 +179,13 @@ export const patchNextServer = {
138
179
  contentFilter: /getInternalWaitUntil/,
139
180
  patchCode: createPatchCode(provideInternalWaitUntil),
140
181
  },
182
+ // Expose `NextNodeServer#revalidate` to `res.revalidate()`
183
+ {
184
+ versions: ">=15.4.0",
185
+ pathFilter,
186
+ contentFilter: /render404: this\.render404\.bind\(this\)/,
187
+ patchCode: createPatchCode(exposeRevalidateRule),
188
+ },
141
189
  ...babelPatches,
142
190
  ],
143
191
  };
@@ -33,6 +33,11 @@ export async function openNextHandler(internalEvent, options) {
33
33
  if (initialHeaders["x-forwarded-host"]) {
34
34
  initialHeaders.host = initialHeaders["x-forwarded-host"];
35
35
  }
36
+ const requestStore = globalThis.__openNextAls.getStore();
37
+ if (requestStore) {
38
+ // Used by `res.revalidate()`, see `core/revalidate.ts`
39
+ requestStore.host = initialHeaders.host;
40
+ }
36
41
  debug("internalEvent", internalEvent);
37
42
  // These 3 will get overwritten by the routing handler if not using an external middleware
38
43
  const internalHeaders = {
@@ -51,5 +51,12 @@ export declare function resolveProxyRequest(proxyRequest: OverrideOptions["proxy
51
51
  /**
52
52
  * @__PURE__
53
53
  */
54
- export declare function resolveCdnInvalidation(cdnInvalidation: OverrideOptions["cdnInvalidation"]): Promise<import("../types/overrides.js").CDNInvalidationHandler>;
54
+ export declare function resolveCdnInvalidation(cdnInvalidation: OverrideOptions["cdnInvalidation"]): Promise<{
55
+ name: string;
56
+ invalidatePaths: (_: {
57
+ initialPath: string;
58
+ rawPath: string;
59
+ resolvedRoutes: import("../types/open-next.js").ResolvedRoute[];
60
+ }[]) => Promise<void>;
61
+ }>;
55
62
  export {};
@@ -0,0 +1,68 @@
1
+ import type { ResolvedRoute } from "../types/open-next.js";
2
+ /**
3
+ * Arguments passed by Next to the router server `revalidate` function.
4
+ *
5
+ * Next renamed `revalidateHeaders` to `headers` in 16.x.
6
+ */
7
+ export interface RevalidateArgs {
8
+ urlPath: string;
9
+ headers?: Record<string, string | string[] | undefined>;
10
+ revalidateHeaders?: Record<string, string | string[] | undefined>;
11
+ opts: {
12
+ unstable_onlyGenerated?: boolean;
13
+ };
14
+ }
15
+ export type RevalidateFn = (args: RevalidateArgs) => Promise<void>;
16
+ /**
17
+ * Resolve the route of a path.
18
+ *
19
+ * Static routes take precedence over dynamic routes, as in Next.
20
+ *
21
+ * @param urlPath The path passed to `res.revalidate()`, may include the basePath and a locale
22
+ * @returns The resolved route or `undefined` when no route matches
23
+ */
24
+ export declare function resolveRoute(urlPath: string): ResolvedRoute | undefined;
25
+ /**
26
+ * Convert a resolved route to the format used in `functions[name].routes` of the OpenNext config.
27
+ *
28
+ * i.e. `pages/index`, `pages/blog/[slug]`, `app/page`, `app/blog/[slug]/page`, ...
29
+ *
30
+ * @param route The resolved route
31
+ * @returns The candidate route templates (app routes may have several because of route groups)
32
+ */
33
+ export declare function getRouteTemplates(route: ResolvedRoute): string[];
34
+ /**
35
+ * Returns whether the route is served by the current function.
36
+ *
37
+ * Routes that are not listed in any split function are served by the default function.
38
+ * Paths that do not match any route are considered local so that Next handles them.
39
+ *
40
+ * @param route The resolved route
41
+ * @returns `true` when the route is served by the current function
42
+ */
43
+ export declare function isServedByCurrentFunction(route: ResolvedRoute | undefined): boolean;
44
+ /**
45
+ * Revalidate a page served by another function by sending a `HEAD` request to the app.
46
+ *
47
+ * Only the revalidate headers provided by Next are forwarded (no cookies).
48
+ * The function serving the page takes care of the CDN invalidation.
49
+ *
50
+ * TODO: the host comes from the request headers, use a trusted origin instead.
51
+ *
52
+ * @param args The arguments passed by Next to `revalidate`
53
+ * @throws When the host is unknown or the page has not been revalidated
54
+ */
55
+ export declare function revalidateRemote({ urlPath, headers, revalidateHeaders, opts, }: RevalidateArgs): Promise<void>;
56
+ /**
57
+ * Wrap `NextNodeServer#revalidate` used by `res.revalidate()` in the Pages Router API routes.
58
+ *
59
+ * - Pages served by the current function are revalidated in-process, then invalidated in the CDN.
60
+ * - Pages served by another split function are revalidated with a `HEAD` request.
61
+ *
62
+ * `NextNodeServer#revalidate` is exposed to `res.revalidate()` by a build time patch,
63
+ * see `exposeRevalidateRule` in `build/patch/patches/patchNextServer.ts`.
64
+ *
65
+ * @param revalidateInProcess The original `NextNodeServer#revalidate`
66
+ * @returns The wrapped revalidate function
67
+ */
68
+ export declare function createRevalidate(revalidateInProcess: RevalidateFn): RevalidateFn;
@@ -0,0 +1,143 @@
1
+ import { AppPathRoutesManifest } from "../adapters/config/index.js";
2
+ import { NEXTJS_CACHE_HEADER } from "../utils/cacheHeaders.js";
3
+ import { RequestCache } from "../utils/requestCache.js";
4
+ import { debug, error } from "../adapters/logger";
5
+ import { dynamicRouteMatcher, staticRouteMatcher, } from "./routing/routeMatcher";
6
+ /**
7
+ * Timeout of the request revalidating a page served by another function.
8
+ */
9
+ const REMOTE_REVALIDATE_TIMEOUT_MS = 30_000;
10
+ /**
11
+ * Resolve the route of a path.
12
+ *
13
+ * Static routes take precedence over dynamic routes, as in Next.
14
+ *
15
+ * @param urlPath The path passed to `res.revalidate()`, may include the basePath and a locale
16
+ * @returns The resolved route or `undefined` when no route matches
17
+ */
18
+ export function resolveRoute(urlPath) {
19
+ const pathname = urlPath.split(/[?#]/)[0];
20
+ return staticRouteMatcher(pathname)[0] ?? dynamicRouteMatcher(pathname)[0];
21
+ }
22
+ /**
23
+ * Convert a resolved route to the format used in `functions[name].routes` of the OpenNext config.
24
+ *
25
+ * i.e. `pages/index`, `pages/blog/[slug]`, `app/page`, `app/blog/[slug]/page`, ...
26
+ *
27
+ * @param route The resolved route
28
+ * @returns The candidate route templates (app routes may have several because of route groups)
29
+ */
30
+ export function getRouteTemplates(route) {
31
+ if (route.type === "page") {
32
+ return [`pages${route.route === "/" ? "/index" : route.route}`];
33
+ }
34
+ return Object.entries(AppPathRoutesManifest)
35
+ .filter(([, path]) => path === route.route)
36
+ .map(([appPath]) => `app${appPath}`);
37
+ }
38
+ /**
39
+ * Returns whether the route is served by the current function.
40
+ *
41
+ * Routes that are not listed in any split function are served by the default function.
42
+ * Paths that do not match any route are considered local so that Next handles them.
43
+ *
44
+ * @param route The resolved route
45
+ * @returns `true` when the route is served by the current function
46
+ */
47
+ export function isServedByCurrentFunction(route) {
48
+ const functions = globalThis.openNextConfig?.functions ?? {};
49
+ if (!route || Object.keys(functions).length === 0) {
50
+ return true;
51
+ }
52
+ const templates = getRouteTemplates(route);
53
+ const owner = Object.entries(functions).find(([, fn]) => fn.routes?.some((r) => templates.includes(r)))?.[0];
54
+ // `globalThis.fnName` is `undefined` in the default function
55
+ return owner === (globalThis.fnName || undefined);
56
+ }
57
+ /**
58
+ * Revalidate a page served by another function by sending a `HEAD` request to the app.
59
+ *
60
+ * Only the revalidate headers provided by Next are forwarded (no cookies).
61
+ * The function serving the page takes care of the CDN invalidation.
62
+ *
63
+ * TODO: the host comes from the request headers, use a trusted origin instead.
64
+ *
65
+ * @param args The arguments passed by Next to `revalidate`
66
+ * @throws When the host is unknown or the page has not been revalidated
67
+ */
68
+ export async function revalidateRemote({ urlPath, headers, revalidateHeaders, opts, }) {
69
+ const host = globalThis.__openNextAls.getStore()?.host;
70
+ if (!host) {
71
+ throw new Error(`Unable to revalidate ${urlPath}: unknown host`);
72
+ }
73
+ const requestHeaders = new Headers();
74
+ for (const [key, value] of Object.entries(headers ?? revalidateHeaders ?? {})) {
75
+ if (value === undefined)
76
+ continue;
77
+ for (const v of Array.isArray(value) ? value : [value]) {
78
+ requestHeaders.append(key, v);
79
+ }
80
+ }
81
+ const url = `https://${host}${urlPath}`;
82
+ debug("Revalidating page served by another function", { url });
83
+ const res = await globalThis.internalFetch(url, {
84
+ method: "HEAD",
85
+ headers: requestHeaders,
86
+ redirect: "manual",
87
+ signal: AbortSignal.timeout(REMOTE_REVALIDATE_TIMEOUT_MS),
88
+ });
89
+ // Same check as Next, see `NextNodeServer#revalidate`
90
+ if (res.headers.get(NEXTJS_CACHE_HEADER)?.toUpperCase() !== "REVALIDATED" &&
91
+ res.status !== 200 &&
92
+ !(res.status === 404 && opts.unstable_onlyGenerated)) {
93
+ throw new Error(`Invalid response ${res.status}`);
94
+ }
95
+ }
96
+ /**
97
+ * Wrap `NextNodeServer#revalidate` used by `res.revalidate()` in the Pages Router API routes.
98
+ *
99
+ * - Pages served by the current function are revalidated in-process, then invalidated in the CDN.
100
+ * - Pages served by another split function are revalidated with a `HEAD` request.
101
+ *
102
+ * `NextNodeServer#revalidate` is exposed to `res.revalidate()` by a build time patch,
103
+ * see `exposeRevalidateRule` in `build/patch/patches/patchNextServer.ts`.
104
+ *
105
+ * @param revalidateInProcess The original `NextNodeServer#revalidate`
106
+ * @returns The wrapped revalidate function
107
+ */
108
+ export function createRevalidate(revalidateInProcess) {
109
+ return async (args) => {
110
+ const route = resolveRoute(args.urlPath);
111
+ if (!isServedByCurrentFunction(route)) {
112
+ return revalidateRemote(args);
113
+ }
114
+ // Render the page in a context isolated from the API route request
115
+ // so that per-request state (i.e. `lastModified`, request cache) does not leak.
116
+ // The promise runner and `waitUntil` are shared to keep background work alive.
117
+ const store = globalThis.__openNextAls.getStore();
118
+ const renderInProcess = () => revalidateInProcess(args);
119
+ // Throws when the page has not been revalidated
120
+ await (store
121
+ ? globalThis.__openNextAls.run({
122
+ ...store,
123
+ lastModified: undefined,
124
+ writtenTags: new Set(),
125
+ requestCache: new RequestCache(),
126
+ }, renderInProcess)
127
+ : renderInProcess());
128
+ const path = args.urlPath.split(/[?#]/)[0];
129
+ try {
130
+ await globalThis.cdnInvalidationHandler?.invalidatePaths([
131
+ {
132
+ initialPath: path,
133
+ rawPath: path,
134
+ resolvedRoutes: route ? [route] : [{ route: path, type: "page" }],
135
+ },
136
+ ]);
137
+ }
138
+ catch (e) {
139
+ // The page has been revalidated, only the CDN may serve stale content
140
+ error(`Failed to invalidate ${path} in the CDN`, e);
141
+ }
142
+ };
143
+ }
@@ -24,6 +24,30 @@ const VARY_HEADER = "RSC, Next-Router-State-Tree, Next-Router-Prefetch, Next-Rou
24
24
  const NEXT_SEGMENT_PREFETCH_HEADER = "next-router-segment-prefetch";
25
25
  const NEXT_PRERENDER_HEADER = "x-nextjs-prerender";
26
26
  const NEXT_POSTPONED_HEADER = "x-nextjs-postponed";
27
+ /**
28
+ * Internal header that can be stored in a cache entry but must not be sent to the client.
29
+ *
30
+ * It is set when a route handler calls `response.cookies` and contains the `HttpOnly` cookies.
31
+ * Next.js strips it in `sendResponse` when it serves a cached response.
32
+ */
33
+ const MIDDLEWARE_SET_COOKIE_HEADER = "x-middleware-set-cookie";
34
+ /**
35
+ * Returns the headers stored in a cache entry, without the internal `x-middleware-set-cookie` header.
36
+ *
37
+ * @param headers The headers stored in the cache entry meta
38
+ * @returns A copy of the headers without the internal header
39
+ */
40
+ function getCachedResponseHeaders(headers) {
41
+ const result = {};
42
+ for (const [key, value] of Object.entries(headers ?? {})) {
43
+ if (value === undefined ||
44
+ key.toLowerCase() === MIDDLEWARE_SET_COOKIE_HEADER) {
45
+ continue;
46
+ }
47
+ result[key] = value;
48
+ }
49
+ return result;
50
+ }
27
51
  async function computeCacheControl(path, body, host, revalidate, lastModified, isStaleFromTagCache = false, revalidationPath = path, revalidationKey = path) {
28
52
  let finalRevalidate = CACHE_ONE_YEAR;
29
53
  const existingRoute = Object.entries(PrerenderManifest?.routes ?? {}).find((p) => p[0] === path)?.[1];
@@ -194,7 +218,7 @@ async function generateResult(event, localizedPath, cachedValue, lastModified, i
194
218
  const headers = {
195
219
  ...cacheControl,
196
220
  "content-type": type,
197
- ...cachedValue.meta?.headers,
221
+ ...getCachedResponseHeaders(cachedValue.meta?.headers),
198
222
  vary: VARY_HEADER,
199
223
  ...additionalHeaders,
200
224
  };
@@ -400,8 +424,7 @@ export async function cacheInterceptor(event, resolvedRoutes) {
400
424
  statusCode: cachedData.value.meta?.status ?? 307,
401
425
  body: emptyReadableStream(),
402
426
  headers: {
403
- ...(cachedData.value.meta?.headers ??
404
- {}),
427
+ ...getCachedResponseHeaders(cachedData.value.meta?.headers),
405
428
  ...cacheControl,
406
429
  },
407
430
  isBase64Encoded: false,
@@ -413,7 +436,7 @@ export async function cacheInterceptor(event, resolvedRoutes) {
413
436
  const statusCode = computeStatusCode(event.rewriteStatusCode, cachedData.value.meta?.status);
414
437
  const headers = {
415
438
  ...cacheControl,
416
- ...cachedData.value.meta?.headers,
439
+ ...getCachedResponseHeaders(cachedData.value.meta?.headers),
417
440
  vary: VARY_HEADER,
418
441
  };
419
442
  // See the note in `generateResult`.
@@ -426,6 +426,17 @@ function handleRepeatedSlashRedirect(event) {
426
426
  }
427
427
  return false;
428
428
  }
429
+ /**
430
+ * Returns the internal trailing slash redirect for the request, if any.
431
+ *
432
+ * Mirrors the internal redirects that Next.js generates from `trailingSlash`. They apply to every
433
+ * path, including API routes, and run before the middleware so that it always sees the canonical
434
+ * pathname. Data requests (`/_next/data/<buildId>/*.json`) are not redirected.
435
+ * See https://github.com/vercel/next.js/blob/324b297/packages/next/src/lib/load-custom-routes.ts#L791-L849
436
+ *
437
+ * @param event The incoming request
438
+ * @returns The 308 redirect, or `false` when the path is already canonical
439
+ */
429
440
  function handleTrailingSlashRedirect(event) {
430
441
  // When rawPath is `//domain`, `url.host` would be `domain`.
431
442
  // https://github.com/opennextjs/opennextjs-aws/issues/355
@@ -433,14 +444,15 @@ function handleTrailingSlashRedirect(event) {
433
444
  if (
434
445
  // Someone is trying to redirect to a different origin, let's not do that
435
446
  url.host !== "localhost" ||
436
- NextConfig.skipTrailingSlashRedirect ||
437
- // We should not apply trailing slash redirect to API routes
438
- event.rawPath.startsWith("/api/")) {
447
+ NextConfig.skipTrailingSlashRedirect) {
439
448
  return false;
440
449
  }
441
450
  const emptyBody = emptyReadableStream();
442
451
  if (NextConfig.trailingSlash &&
443
- !(event.query.__nextDataReq === "1") &&
452
+ // Data requests (`/_next/data/<buildId>/*.json`) are never redirected, as with Next.js.
453
+ // The header is set by `fixDataPage`, a client supplied value is removed by the routing handler.
454
+ // The `__nextDataReq` query parameter can not be used as it is set by the client.
455
+ event.headers["x-nextjs-data"] !== "1" &&
444
456
  !event.rawPath.endsWith("/") &&
445
457
  !event.rawPath.match(/[\w-]+\.[\w]+$/g)) {
446
458
  const headersLocation = event.url.split("?");
@@ -8,6 +8,10 @@ type MiddlewareLoader = () => Promise<{
8
8
  *
9
9
  * Middleware request-header overrides are normalized and filtered to Next.js's allowlist.
10
10
  *
11
+ * Next.js internal headers are never exposed on the response.
12
+ * `x-middleware-set-cookie` is only forwarded on the request headers of the returned `MiddlewareEvent`,
13
+ * regardless of the allowlist.
14
+ *
11
15
  * @param internalEvent The internal request event.
12
16
  * @param initialSearch The initial query string received by the handler.
13
17
  * @param middlewareLoader The middleware loader, overridden by unit tests.
@@ -9,6 +9,8 @@ const middlewareManifest = MiddlewareManifest;
9
9
  const functionsConfigManifest = FunctionsConfigManifest;
10
10
  const middleMatch = getMiddlewareMatch(middlewareManifest, functionsConfigManifest);
11
11
  const REDIRECTS = new Set([301, 302, 303, 307, 308]);
12
+ // Internal header set by `NextResponse` when `response.cookies` is modified.
13
+ const MIDDLEWARE_SET_COOKIE_HEADER = "x-middleware-set-cookie";
12
14
  function defaultMiddlewareLoader() {
13
15
  // @ts-expect-error - This is bundled
14
16
  return import("./middleware.mjs");
@@ -18,6 +20,10 @@ function defaultMiddlewareLoader() {
18
20
  *
19
21
  * Middleware request-header overrides are normalized and filtered to Next.js's allowlist.
20
22
  *
23
+ * Next.js internal headers are never exposed on the response.
24
+ * `x-middleware-set-cookie` is only forwarded on the request headers of the returned `MiddlewareEvent`,
25
+ * regardless of the allowlist.
26
+ *
21
27
  * @param internalEvent The internal request event.
22
28
  * @param initialSearch The initial query string received by the handler.
23
29
  * @param middlewareLoader The middleware loader, overridden by unit tests.
@@ -92,6 +98,7 @@ export async function handleMiddleware(internalEvent, initialSearch, middlewareL
92
98
  const responseHeaders = result.headers;
93
99
  const reqHeaders = {};
94
100
  const resHeaders = {};
101
+ let middlewareSetCookie;
95
102
  // These are internal headers used by Next.js, we don't want to expose them to the client
96
103
  // NOTE: keys must be in lowercase to match the normalized header keys.
97
104
  const filteredHeaders = [
@@ -112,6 +119,13 @@ export async function handleMiddleware(internalEvent, initialSearch, middlewareL
112
119
  else {
113
120
  if (filteredHeaders.includes(normalizedKey))
114
121
  return;
122
+ // `x-middleware-set-cookie` holds a copy of the cookies set by the middleware,
123
+ // including the `HttpOnly` ones. It must never be sent to the client.
124
+ // It is only forwarded on the request (see below) so that `cookies().get()` sees them.
125
+ if (normalizedKey === MIDDLEWARE_SET_COOKIE_HEADER) {
126
+ middlewareSetCookie = value;
127
+ return;
128
+ }
115
129
  // Headers.forEach folds same-name headers; use getSetCookie() below instead.
116
130
  if (normalizedKey === "set-cookie")
117
131
  return;
@@ -139,6 +153,11 @@ export async function handleMiddleware(internalEvent, initialSearch, middlewareL
139
153
  const newHeaders = Object.fromEntries([...Object.entries(internalEvent.headers), ...Object.entries(reqHeaders)]
140
154
  .map(([key, value]) => [key.toLowerCase(), value])
141
155
  .filter(([key]) => !overriddenHeaders || overriddenHeaders.has(key)));
156
+ // Next.js forwards `x-middleware-set-cookie` after applying the override allowlist,
157
+ // so it is never filtered out (see `resolve-routes.ts`).
158
+ if (middlewareSetCookie !== undefined) {
159
+ newHeaders[MIDDLEWARE_SET_COOKIE_HEADER] = middlewareSetCookie;
160
+ }
142
161
  // If the middleware returned a Rewrite, set the `url` to the pathname of the rewrite
143
162
  // NOTE: the header was added to `req` from above
144
163
  const rewriteUrl = responseHeaders.get("x-middleware-rewrite");
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Returns whether a segment of the pathname is the placeholder of a matched dynamic App Router page.
3
+ *
4
+ * Next.js versions before 16.4.5 treat a param whose value is its placeholder (e.g. `[slug]` for `[slug]`) as an
5
+ * unresolved fallback param. Such values are not legitimate and must not reach Next.js.
6
+ *
7
+ * Segments are decoded like Next.js decodes param values (catch-all values are split on `/` first).
8
+ * Segments with a malformed percent encoding never match.
9
+ * Pathnames without an opening bracket return early, without matching the dynamic routes.
10
+ *
11
+ * @param pathname The internal pathname, which may include the basePath, a locale prefix and a trailing slash
12
+ * @returns Whether a segment of the pathname is a placeholder of a matched dynamic App Router page
13
+ */
14
+ export declare function hasRouteParamPlaceholder(pathname: string): boolean;