@opennextjs/aws 4.1.14 → 4.1.16

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.
@@ -167,8 +167,9 @@ export default class Cache {
167
167
  const store = globalThis.__openNextAls.getStore();
168
168
  if (store) {
169
169
  // The sentinel is kept as is in the store so that the revalidation queue keeps
170
- // deduplicating every stale request for the same path.
170
+ // deduplicating every stale request for the same path. The queue gets the real time.
171
171
  store.lastModified = _isStale ? 1 : _lastModified;
172
+ store.entryLastModified = _lastModified;
172
173
  }
173
174
  if (_isStale) {
174
175
  _lastModified = getStaleLastModified(cacheData.revalidate);
@@ -1,5 +1,15 @@
1
1
  import type { ComposableCacheEntry } from "../types/cache.js";
2
2
  declare const _default: {
3
+ /**
4
+ * Reads a composable cache entry.
5
+ *
6
+ * The incremental cache and the tag cache are accessed with a key derived from `cacheKey`,
7
+ * see `getComposableCacheStorageKey`.
8
+ *
9
+ * @param cacheKey The cache key provided by Next.js
10
+ * @returns The cache entry, or `undefined` on a miss or when the entry has been revalidated.
11
+ * Errors are caught and reported as a miss.
12
+ */
3
13
  get(cacheKey: string): Promise<{
4
14
  value: import("stream/web").ReadableStream<any>;
5
15
  tags: string[];
@@ -8,6 +18,16 @@ declare const _default: {
8
18
  expire: number;
9
19
  revalidate: number;
10
20
  } | undefined>;
21
+ /**
22
+ * Writes a composable cache entry.
23
+ *
24
+ * The incremental cache and the tag cache are accessed with a key derived from `cacheKey`,
25
+ * see `getComposableCacheStorageKey`.
26
+ *
27
+ * @param cacheKey The cache key provided by Next.js
28
+ * @param pendingEntry The entry to store, resolves once the cached function has completed
29
+ * @throws When the entry can not be resolved or when the incremental cache or tag cache write fails
30
+ */
11
31
  set(cacheKey: string, pendingEntry: Promise<ComposableCacheEntry>): Promise<void>;
12
32
  refreshTags(): Promise<void>;
13
33
  /**
@@ -1,4 +1,4 @@
1
- import { isStale, writeTags } from "../utils/cache.js";
1
+ import { getComposableCacheStorageKey, isStale, writeTags } from "../utils/cache.js";
2
2
  import { fromReadableStream, toReadableStream } from "../utils/stream.js";
3
3
  import { debug } from "./logger";
4
4
  /**
@@ -17,12 +17,25 @@ import { debug } from "./logger";
17
17
  * flight from the same render while making a cross-request share impossible.
18
18
  * With no ALS store (e.g. at build time) the dedupe is skipped and `get` falls
19
19
  * through to the incremental cache.
20
+ *
21
+ * The map is keyed by the cache key provided by Next.js: it is an in-memory exact string
22
+ * comparison so there is no need to use the storage key (see `getComposableCacheStorageKey`).
20
23
  */
21
24
  const PENDING_WRITES_CACHE_KEY = "composable-cache:pending-writes";
22
25
  const getPendingWritePromiseMap = () => globalThis.__openNextAls
23
26
  ?.getStore()
24
27
  ?.requestCache?.getOrCreate(PENDING_WRITES_CACHE_KEY);
25
28
  export default {
29
+ /**
30
+ * Reads a composable cache entry.
31
+ *
32
+ * The incremental cache and the tag cache are accessed with a key derived from `cacheKey`,
33
+ * see `getComposableCacheStorageKey`.
34
+ *
35
+ * @param cacheKey The cache key provided by Next.js
36
+ * @returns The cache entry, or `undefined` on a miss or when the entry has been revalidated.
37
+ * Errors are caught and reported as a miss.
38
+ */
26
39
  async get(cacheKey) {
27
40
  try {
28
41
  // We first check if we have a pending write for this cache key
@@ -37,7 +50,8 @@ export default {
37
50
  }));
38
51
  }
39
52
  }
40
- const result = await globalThis.incrementalCache.get(cacheKey, "composable");
53
+ const storageKey = getComposableCacheStorageKey(cacheKey);
54
+ const result = await globalThis.incrementalCache.get(storageKey, "composable");
41
55
  if (!result?.value?.value) {
42
56
  return undefined;
43
57
  }
@@ -54,7 +68,7 @@ export default {
54
68
  // Check if tags are stale – entry is valid but needs background revalidation
55
69
  const isCacheStale = result.shouldBypassTagCache
56
70
  ? false
57
- : await isStale(cacheKey, result.value.tags, result.lastModified);
71
+ : await isStale(storageKey, result.value.tags, result.lastModified);
58
72
  if (isCacheStale) {
59
73
  revalidate = -1;
60
74
  }
@@ -63,13 +77,13 @@ export default {
63
77
  globalThis.tagCache.mode === undefined) {
64
78
  const hasBeenRevalidated = result.shouldBypassTagCache
65
79
  ? false
66
- : (await globalThis.tagCache.getLastModified(cacheKey, result.lastModified)) === -1;
80
+ : (await globalThis.tagCache.getLastModified(storageKey, result.lastModified)) === -1;
67
81
  if (hasBeenRevalidated)
68
82
  return undefined;
69
83
  // Check if tags are stale – entry is valid but needs background revalidation
70
84
  const isCacheStale = result.shouldBypassTagCache
71
85
  ? false
72
- : await isStale(cacheKey, result.value.tags, result.lastModified);
86
+ : await isStale(storageKey, result.value.tags, result.lastModified);
73
87
  if (isCacheStale) {
74
88
  revalidate = -1;
75
89
  }
@@ -85,6 +99,16 @@ export default {
85
99
  return undefined;
86
100
  }
87
101
  },
102
+ /**
103
+ * Writes a composable cache entry.
104
+ *
105
+ * The incremental cache and the tag cache are accessed with a key derived from `cacheKey`,
106
+ * see `getComposableCacheStorageKey`.
107
+ *
108
+ * @param cacheKey The cache key provided by Next.js
109
+ * @param pendingEntry The entry to store, resolves once the cached function has completed
110
+ * @throws When the entry can not be resolved or when the incremental cache or tag cache write fails
111
+ */
88
112
  async set(cacheKey, pendingEntry) {
89
113
  const promiseEntry = pendingEntry.then(async (entry) => ({
90
114
  ...entry,
@@ -95,15 +119,16 @@ export default {
95
119
  const entry = await promiseEntry.finally(() => {
96
120
  pendingWritePromiseMap?.delete(cacheKey);
97
121
  });
98
- await globalThis.incrementalCache.set(cacheKey, {
122
+ const storageKey = getComposableCacheStorageKey(cacheKey);
123
+ await globalThis.incrementalCache.set(storageKey, {
99
124
  ...entry,
100
125
  value: entry.value,
101
126
  }, "composable");
102
127
  if (globalThis.tagCache.mode === "original") {
103
- const storedTags = await globalThis.tagCache.getByPath(cacheKey);
128
+ const storedTags = await globalThis.tagCache.getByPath(storageKey);
104
129
  const tagsToWrite = entry.tags.filter((tag) => !storedTags.includes(tag));
105
130
  if (tagsToWrite.length > 0) {
106
- await writeTags(tagsToWrite.map((tag) => ({ tag, path: cacheKey })));
131
+ await writeTags(tagsToWrite.map((tag) => ({ tag, path: storageKey })));
107
132
  }
108
133
  }
109
134
  },
@@ -1,3 +1,4 @@
1
+ import { convertEdgeResponseHeaders } from "../http/util.js";
1
2
  import { runWithOpenNextRequestContext } from "../utils/promise.js";
2
3
  import { emptyReadableStream } from "../utils/stream.js";
3
4
  // We import it like that so that the edge plugin can replace it
@@ -26,17 +27,7 @@ const defaultHandler = async (internalEvent, options) => {
26
27
  url: internalEvent.url,
27
28
  body: convertBodyToReadableStream(internalEvent.method, internalEvent.body),
28
29
  });
29
- const responseHeaders = {};
30
- response.headers.forEach((value, key) => {
31
- // Headers.forEach folds same-name headers; use getSetCookie() below instead.
32
- if (key.toLowerCase() === "set-cookie")
33
- return;
34
- responseHeaders[key] = value;
35
- });
36
- const setCookies = response.headers.getSetCookie();
37
- if (setCookies.length > 0) {
38
- responseHeaders["set-cookie"] = setCookies;
39
- }
30
+ const responseHeaders = convertEdgeResponseHeaders(response.headers);
40
31
  const body = response.body ?? emptyReadableStream();
41
32
  return {
42
33
  type: "core",
@@ -2,6 +2,22 @@ import type { OpenNextNodeResponse } from "../http/index.js";
2
2
  import { IncomingMessage } from "../http/index.js";
3
3
  import type { InternalEvent, InternalResult, RoutingResult } from "../types/open-next.js";
4
4
  import type { OpenNextHandlerOptions } from "../types/overrides.js";
5
+ type HeaderRecord = InternalEvent["headers"];
6
+ /**
7
+ * Splits the headers of a routed event into the request and response headers.
8
+ *
9
+ * The routing adds the middleware and `next.config` response headers to the event with a prefix each (see
10
+ * `applyResponseHeaders`). As in Next.js, both go on the response but only the middleware headers go on the request:
11
+ * a `Content-Security-Policy` from `next.config` must not replace the one, holding the script nonce, that the
12
+ * middleware set on the request.
13
+ *
14
+ * @param eventHeaders The headers of the routed event, not modified
15
+ * @returns The headers to send to Next.js and the headers to set on the response
16
+ */
17
+ export declare function splitRoutingHeaders(eventHeaders: HeaderRecord): {
18
+ requestHeaders: HeaderRecord;
19
+ responseHeaders: HeaderRecord;
20
+ };
5
21
  export declare function openNextHandler(internalEvent: InternalEvent, options?: OpenNextHandlerOptions): Promise<InternalResult>;
6
22
  /**
7
23
  * Renders the next resolved route after a `fallback: false` route threw a `NoFallbackError`.
@@ -26,3 +42,4 @@ export declare function openNextHandler(internalEvent: InternalEvent, options?:
26
42
  * @return A promise that resolves after rendering a route or an error response
27
43
  */
28
44
  export declare function handleNoFallbackError(req: IncomingMessage, res: OpenNextNodeResponse, routingResult: RoutingResult, metadata: Record<string, unknown>, index?: number): Promise<void>;
45
+ export {};
@@ -8,13 +8,45 @@ import { debug, error } from "../adapters/logger";
8
8
  import { patchAsyncStorage } from "./patchAsyncStorage";
9
9
  import { getRouteMatchMetadata } from "./routeMatchMetadata";
10
10
  import { constructNextUrl, convertRes, convertToQuery, createServerResponse, } from "./routing/util";
11
- import routingHandler, { INTERNAL_EVENT_REQUEST_ID, INTERNAL_HEADER_REWRITE_STATUS_CODE, INTERNAL_HEADER_INITIAL_URL, INTERNAL_HEADER_RESOLVED_ROUTES, MIDDLEWARE_HEADER_PREFIX, MIDDLEWARE_HEADER_PREFIX_LEN, } from "./routingHandler";
11
+ import routingHandler, { INTERNAL_EVENT_REQUEST_ID, INTERNAL_HEADER_REWRITE_STATUS_CODE, INTERNAL_HEADER_INITIAL_URL, INTERNAL_HEADER_RESOLVED_ROUTES, MIDDLEWARE_HEADER_PREFIX, MIDDLEWARE_HEADER_PREFIX_LEN, NEXT_CONFIG_HEADER_PREFIX, NEXT_CONFIG_HEADER_PREFIX_LEN, } from "./routingHandler";
12
12
  import { requestHandler, setNextjsPrebundledReact } from "./util";
13
13
  // This is used to identify requests in the cache
14
14
  globalThis.__openNextAls = new AsyncLocalStorage();
15
15
  //#override patchAsyncStorage
16
16
  patchAsyncStorage();
17
17
  //#endOverride
18
+ /**
19
+ * Splits the headers of a routed event into the request and response headers.
20
+ *
21
+ * The routing adds the middleware and `next.config` response headers to the event with a prefix each (see
22
+ * `applyResponseHeaders`). As in Next.js, both go on the response but only the middleware headers go on the request:
23
+ * a `Content-Security-Policy` from `next.config` must not replace the one, holding the script nonce, that the
24
+ * middleware set on the request.
25
+ *
26
+ * @param eventHeaders The headers of the routed event, not modified
27
+ * @returns The headers to send to Next.js and the headers to set on the response
28
+ */
29
+ export function splitRoutingHeaders(eventHeaders) {
30
+ const requestHeaders = {};
31
+ const middlewareHeaders = {};
32
+ const nextConfigHeaders = {};
33
+ for (const [key, value] of Object.entries(eventHeaders)) {
34
+ if (key.startsWith(MIDDLEWARE_HEADER_PREFIX)) {
35
+ middlewareHeaders[key.slice(MIDDLEWARE_HEADER_PREFIX_LEN)] = value;
36
+ }
37
+ else if (key.startsWith(NEXT_CONFIG_HEADER_PREFIX)) {
38
+ nextConfigHeaders[key.slice(NEXT_CONFIG_HEADER_PREFIX_LEN)] = value;
39
+ }
40
+ else {
41
+ requestHeaders[key] = value;
42
+ }
43
+ }
44
+ return {
45
+ requestHeaders: { ...requestHeaders, ...middlewareHeaders },
46
+ // The routing already dropped the `next.config` headers that must not take precedence.
47
+ responseHeaders: { ...middlewareHeaders, ...nextConfigHeaders },
48
+ };
49
+ }
18
50
  export async function openNextHandler(internalEvent, options) {
19
51
  const initialHeaders = internalEvent.headers;
20
52
  // We only use the requestId header if we are using an external middleware
@@ -60,22 +92,20 @@ export async function openNextHandler(internalEvent, options) {
60
92
  assetResolver: globalThis.assetResolver,
61
93
  });
62
94
  //#endOverride
63
- const headers = "type" in routingResult
64
- ? routingResult.headers
65
- : routingResult.internalEvent.headers;
66
- const overwrittenResponseHeaders = {};
67
- for (const [rawKey, value] of Object.entries(headers)) {
68
- if (!rawKey.startsWith(MIDDLEWARE_HEADER_PREFIX)) {
69
- continue;
70
- }
71
- const key = rawKey.slice(MIDDLEWARE_HEADER_PREFIX_LEN);
72
- // We skip this header here since it is used by Next internally and we don't want it on the response headers.
73
- // This header needs to be present in the request headers for processRequest, so cookies().get() from Next will work on initial render.
74
- if (key !== "x-middleware-set-cookie") {
75
- overwrittenResponseHeaders[key] = value;
76
- }
77
- headers[key] = value;
78
- delete headers[rawKey];
95
+ // The headers of a result are not prefixed, they are the response headers.
96
+ let requestHeaders = {};
97
+ let overwrittenResponseHeaders = {};
98
+ if (!("type" in routingResult)) {
99
+ const split = splitRoutingHeaders(routingResult.internalEvent.headers);
100
+ requestHeaders = split.requestHeaders;
101
+ overwrittenResponseHeaders = split.responseHeaders;
102
+ routingResult = {
103
+ ...routingResult,
104
+ internalEvent: {
105
+ ...routingResult.internalEvent,
106
+ headers: requestHeaders,
107
+ },
108
+ };
79
109
  }
80
110
  if ("isExternalRewrite" in routingResult &&
81
111
  routingResult.isExternalRewrite === true) {
@@ -138,7 +168,7 @@ export async function openNextHandler(internalEvent, options) {
138
168
  // 2. We could alter the lastModified time of our cache to make next believe that the cache is fresh, but this could cause issues with stale data since the cdn will cache the stale data as if it was fresh
139
169
  // 3. OUR CHOICE: We could pass a purpose prefetch header to the serverless function to make next believe that the request is a prefetch request and not trigger revalidation (This could potentially break in the future if next changes the behavior of prefetch requests)
140
170
  headers: {
141
- ...headers,
171
+ ...requestHeaders,
142
172
  //#override appendPrefetch
143
173
  purpose: "prefetch",
144
174
  //#endOverride
@@ -121,6 +121,7 @@ export function createRevalidate(revalidateInProcess) {
121
121
  ? globalThis.__openNextAls.run({
122
122
  ...store,
123
123
  lastModified: undefined,
124
+ entryLastModified: undefined,
124
125
  writtenTags: new Set(),
125
126
  requestCache: new RequestCache(),
126
127
  }, renderInProcess)
@@ -1,5 +1,6 @@
1
1
  import { createHash } from "node:crypto";
2
2
  import { AppPathsManifest, NextConfig, PagesManifest, PrerenderManifest, } from "../../adapters/config/index.js";
3
+ import { decodePathParams } from "../../utils/normalize-path.js";
3
4
  import { emptyReadableStream, toReadableStream } from "../../utils/stream.js";
4
5
  import { isBinaryContentType } from "../../utils/binary.js";
5
6
  import { getTagsFromValue, hasBeenRevalidated, isStale } from "../../utils/cache.js";
@@ -24,6 +25,30 @@ const VARY_HEADER = "RSC, Next-Router-State-Tree, Next-Router-Prefetch, Next-Rou
24
25
  const NEXT_SEGMENT_PREFETCH_HEADER = "next-router-segment-prefetch";
25
26
  const NEXT_PRERENDER_HEADER = "x-nextjs-prerender";
26
27
  const NEXT_POSTPONED_HEADER = "x-nextjs-postponed";
28
+ /**
29
+ * Internal header that can be stored in a cache entry but must not be sent to the client.
30
+ *
31
+ * It is set when a route handler calls `response.cookies` and contains the `HttpOnly` cookies.
32
+ * Next.js strips it in `sendResponse` when it serves a cached response.
33
+ */
34
+ const MIDDLEWARE_SET_COOKIE_HEADER = "x-middleware-set-cookie";
35
+ /**
36
+ * Returns the headers stored in a cache entry, without the internal `x-middleware-set-cookie` header.
37
+ *
38
+ * @param headers The headers stored in the cache entry meta
39
+ * @returns A copy of the headers without the internal header
40
+ */
41
+ function getCachedResponseHeaders(headers) {
42
+ const result = {};
43
+ for (const [key, value] of Object.entries(headers ?? {})) {
44
+ if (value === undefined ||
45
+ key.toLowerCase() === MIDDLEWARE_SET_COOKIE_HEADER) {
46
+ continue;
47
+ }
48
+ result[key] = value;
49
+ }
50
+ return result;
51
+ }
27
52
  async function computeCacheControl(path, body, host, revalidate, lastModified, isStaleFromTagCache = false, revalidationPath = path, revalidationKey = path) {
28
53
  let finalRevalidate = CACHE_ONE_YEAR;
29
54
  const existingRoute = Object.entries(PrerenderManifest?.routes ?? {}).find((p) => p[0] === path)?.[1];
@@ -194,7 +219,7 @@ async function generateResult(event, localizedPath, cachedValue, lastModified, i
194
219
  const headers = {
195
220
  ...cacheControl,
196
221
  "content-type": type,
197
- ...cachedValue.meta?.headers,
222
+ ...getCachedResponseHeaders(cachedValue.meta?.headers),
198
223
  vary: VARY_HEADER,
199
224
  ...additionalHeaders,
200
225
  };
@@ -231,26 +256,6 @@ function computeStatusCode(rewriteStatusCode, cachedStatusCode) {
231
256
  }
232
257
  return rewriteStatusCode ?? cachedStatusCode ?? 200;
233
258
  }
234
- /**
235
- *
236
- * https://github.com/vercel/next.js/blob/34039551d2e5f611c0abde31a197d9985918adaf/packages/next/src/shared/lib/router/utils/escape-path-delimiters.ts#L2-L10
237
- */
238
- function escapePathDelimiters(segment, escapeEncoded) {
239
- return segment.replace(new RegExp(`([/#?]${escapeEncoded ? "|%(2f|23|3f|5c)" : ""})`, "gi"), (char) => encodeURIComponent(char));
240
- }
241
- /**
242
- *
243
- * SSG cache key needs to be decoded, but some characters needs to be properly escaped
244
- * https://github.com/vercel/next.js/blob/34039551d2e5f611c0abde31a197d9985918adaf/packages/next/src/server/lib/router-utils/decode-path-params.ts#L11-L26
245
- * Decoding must be atomic, as in Next.js. Partially decoding a malformed path
246
- * could select a different cache route than the one evaluated by middleware.
247
- */
248
- function decodePathParams(pathname) {
249
- return pathname
250
- .split("/")
251
- .map((segment) => escapePathDelimiters(decodeURIComponent(segment), true))
252
- .join("/");
253
- }
254
259
  let routeCacheManifests;
255
260
  function getRouteCacheManifests() {
256
261
  routeCacheManifests ??= {
@@ -400,8 +405,7 @@ export async function cacheInterceptor(event, resolvedRoutes) {
400
405
  statusCode: cachedData.value.meta?.status ?? 307,
401
406
  body: emptyReadableStream(),
402
407
  headers: {
403
- ...(cachedData.value.meta?.headers ??
404
- {}),
408
+ ...getCachedResponseHeaders(cachedData.value.meta?.headers),
405
409
  ...cacheControl,
406
410
  },
407
411
  isBase64Encoded: false,
@@ -413,7 +417,7 @@ export async function cacheInterceptor(event, resolvedRoutes) {
413
417
  const statusCode = computeStatusCode(event.rewriteStatusCode, cachedData.value.meta?.status);
414
418
  const headers = {
415
419
  ...cacheControl,
416
- ...cachedData.value.meta?.headers,
420
+ ...getCachedResponseHeaders(cachedData.value.meta?.headers),
417
421
  vary: VARY_HEADER,
418
422
  };
419
423
  // See the note in `generateResult`.
@@ -20,9 +20,12 @@ export declare function getCompiledRegExp(pattern: string, flags?: string): RegE
20
20
  * interpolation, with condition parameters taking precedence. See
21
21
  * https://github.com/vercel/next.js/blob/ae745ba/packages/next/src/server/lib/router-utils/resolve-routes.ts#L400-L414
22
22
  *
23
+ * Header names are lowercased. As Next.js applies them with the case-insensitive `res.setHeader()`, the last matching
24
+ * route wins when several set the same header, whatever its case.
25
+ *
23
26
  * @param event The request used to match configured headers
24
27
  * @param configHeaders The configured header routes
25
- * @returns The response headers produced by every matching route
28
+ * @returns The response headers produced by every matching route, keyed by lowercase name
26
29
  * @throws {SyntaxError} When a configured route pattern is invalid
27
30
  */
28
31
  export declare function getNextConfigHeaders(event: InternalEvent, configHeaders?: Header[] | undefined): Record<string, string | undefined>;
@@ -78,6 +81,16 @@ export declare function handleRewrites<T extends RewriteDefinition>(event: Inter
78
81
  export declare function handleBeforeFilesRewrites<T extends RewriteDefinition>(event: InternalEvent, rewrites: T[]): ReturnType<typeof handleRewrites<T>>;
79
82
  export declare function handleRedirects(event: InternalEvent, redirects: RedirectDefinition[]): InternalResult | undefined;
80
83
  export declare function fixDataPage(internalEvent: InternalEvent, buildId: string): InternalEvent | InternalResult;
84
+ /**
85
+ * Rewrites the event to `/404` for paths that `fallback: false` routes do not pregenerate.
86
+ *
87
+ * A path is pregenerated when its decoded form is a key of the prerender manifest routes.
88
+ * The rewrite only applies when no other static or dynamic route can serve the path.
89
+ *
90
+ * @param internalEvent The incoming event
91
+ * @param prerenderManifest The prerender manifest
92
+ * @returns The (possibly rewritten) event and whether the path is pregenerated
93
+ */
81
94
  export declare function handleFallbackFalse(internalEvent: InternalEvent, prerenderManifest?: PrerenderManifest): {
82
95
  event: InternalEvent;
83
96
  isISR: boolean;
@@ -1,6 +1,6 @@
1
1
  import { NextConfig } from "../../adapters/config/index.js";
2
2
  import { compile, match, parse } from "path-to-regexp";
3
- import { normalizeRepeatedSlashes } from "../../utils/normalize-path.js";
3
+ import { decodePathParams, normalizeRepeatedSlashes, } from "../../utils/normalize-path.js";
4
4
  import { emptyReadableStream, toReadableStream } from "../../utils/stream.js";
5
5
  import { debug } from "../../adapters/logger";
6
6
  import { handleLocaleRedirect, localizePath } from "./i18n";
@@ -223,9 +223,12 @@ function compileQueryString(queryString, params) {
223
223
  * interpolation, with condition parameters taking precedence. See
224
224
  * https://github.com/vercel/next.js/blob/ae745ba/packages/next/src/server/lib/router-utils/resolve-routes.ts#L400-L414
225
225
  *
226
+ * Header names are lowercased. As Next.js applies them with the case-insensitive `res.setHeader()`, the last matching
227
+ * route wins when several set the same header, whatever its case.
228
+ *
226
229
  * @param event The request used to match configured headers
227
230
  * @param configHeaders The configured header routes
228
- * @returns The response headers produced by every matching route
231
+ * @returns The response headers produced by every matching route, keyed by lowercase name
229
232
  * @throws {SyntaxError} When a configured route pattern is invalid
230
233
  */
231
234
  export function getNextConfigHeaders(event, configHeaders) {
@@ -254,11 +257,11 @@ export function getNextConfigHeaders(event, configHeaders) {
254
257
  try {
255
258
  const key = hasParams ? compileNonPath(h.key, params) : h.key;
256
259
  const value = hasParams ? compileNonPath(h.value, params) : h.value;
257
- requestHeaders[key] = value;
260
+ requestHeaders[key.toLowerCase()] = value;
258
261
  }
259
262
  catch {
260
263
  debug(`Error matching header ${h.key} with value ${h.value}`);
261
- requestHeaders[h.key] = h.value;
264
+ requestHeaders[h.key.toLowerCase()] = h.value;
262
265
  }
263
266
  });
264
267
  }
@@ -426,6 +429,17 @@ function handleRepeatedSlashRedirect(event) {
426
429
  }
427
430
  return false;
428
431
  }
432
+ /**
433
+ * Returns the internal trailing slash redirect for the request, if any.
434
+ *
435
+ * Mirrors the internal redirects that Next.js generates from `trailingSlash`. They apply to every
436
+ * path, including API routes, and run before the middleware so that it always sees the canonical
437
+ * pathname. Data requests (`/_next/data/<buildId>/*.json`) are not redirected.
438
+ * See https://github.com/vercel/next.js/blob/324b297/packages/next/src/lib/load-custom-routes.ts#L791-L849
439
+ *
440
+ * @param event The incoming request
441
+ * @returns The 308 redirect, or `false` when the path is already canonical
442
+ */
429
443
  function handleTrailingSlashRedirect(event) {
430
444
  // When rawPath is `//domain`, `url.host` would be `domain`.
431
445
  // https://github.com/opennextjs/opennextjs-aws/issues/355
@@ -433,14 +447,15 @@ function handleTrailingSlashRedirect(event) {
433
447
  if (
434
448
  // Someone is trying to redirect to a different origin, let's not do that
435
449
  url.host !== "localhost" ||
436
- NextConfig.skipTrailingSlashRedirect ||
437
- // We should not apply trailing slash redirect to API routes
438
- event.rawPath.startsWith("/api/")) {
450
+ NextConfig.skipTrailingSlashRedirect) {
439
451
  return false;
440
452
  }
441
453
  const emptyBody = emptyReadableStream();
442
454
  if (NextConfig.trailingSlash &&
443
- !(event.query.__nextDataReq === "1") &&
455
+ // Data requests (`/_next/data/<buildId>/*.json`) are never redirected, as with Next.js.
456
+ // The header is set by `fixDataPage`, a client supplied value is removed by the routing handler.
457
+ // The `__nextDataReq` query parameter can not be used as it is set by the client.
458
+ event.headers["x-nextjs-data"] !== "1" &&
444
459
  !event.rawPath.endsWith("/") &&
445
460
  !event.rawPath.match(/[\w-]+\.[\w]+$/g)) {
446
461
  const headersLocation = event.url.split("?");
@@ -528,6 +543,16 @@ export function fixDataPage(internalEvent, buildId) {
528
543
  }
529
544
  return internalEvent;
530
545
  }
546
+ /**
547
+ * Rewrites the event to `/404` for paths that `fallback: false` routes do not pregenerate.
548
+ *
549
+ * A path is pregenerated when its decoded form is a key of the prerender manifest routes.
550
+ * The rewrite only applies when no other static or dynamic route can serve the path.
551
+ *
552
+ * @param internalEvent The incoming event
553
+ * @param prerenderManifest The prerender manifest
554
+ * @returns The (possibly rewritten) event and whether the path is pregenerated
555
+ */
531
556
  export function handleFallbackFalse(internalEvent, prerenderManifest) {
532
557
  const { rawPath } = internalEvent;
533
558
  const { dynamicRoutes = {}, routes = {} } = prerenderManifest ?? {};
@@ -551,7 +576,14 @@ export function handleFallbackFalse(internalEvent, prerenderManifest) {
551
576
  const matchedStaticRoute = staticRouteMatcher(localizedPath);
552
577
  const prerenderedFallbackRoutesName = prerenderedFallbackRoutes.map(([name]) => name);
553
578
  const matchedDynamicRoute = dynamicRouteMatcher(localizedPath).filter(({ route }) => !prerenderedFallbackRoutesName.includes(route));
554
- const isPregenerated = Object.keys(routes).includes(localizedPath);
579
+ let isPregenerated = false;
580
+ try {
581
+ // The prerender manifest keys routes by their decoded path while rawPath is percent-encoded.
582
+ isPregenerated = Object.hasOwn(routes, decodePathParams(localizedPath));
583
+ }
584
+ catch {
585
+ // A malformed escape sequence cannot name a prerendered route.
586
+ }
555
587
  if (routeFallback &&
556
588
  !isPregenerated &&
557
589
  matchedStaticRoute.length === 0 &&
@@ -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,12 +98,16 @@ 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.
104
+ // See `resolve-routes.ts` in Next.js for the headers it drops from the middleware response.
97
105
  const filteredHeaders = [
98
106
  "x-middleware-override-headers",
99
107
  "x-middleware-next",
100
108
  "x-middleware-rewrite",
109
+ "x-middleware-redirect",
110
+ "x-middleware-refresh",
101
111
  // We need to drop `content-encoding` because it will be decoded
102
112
  "content-encoding",
103
113
  ];
@@ -112,6 +122,13 @@ export async function handleMiddleware(internalEvent, initialSearch, middlewareL
112
122
  else {
113
123
  if (filteredHeaders.includes(normalizedKey))
114
124
  return;
125
+ // `x-middleware-set-cookie` holds a copy of the cookies set by the middleware,
126
+ // including the `HttpOnly` ones. It must never be sent to the client.
127
+ // It is only forwarded on the request (see below) so that `cookies().get()` sees them.
128
+ if (normalizedKey === MIDDLEWARE_SET_COOKIE_HEADER) {
129
+ middlewareSetCookie = value;
130
+ return;
131
+ }
115
132
  // Headers.forEach folds same-name headers; use getSetCookie() below instead.
116
133
  if (normalizedKey === "set-cookie")
117
134
  return;
@@ -139,6 +156,11 @@ export async function handleMiddleware(internalEvent, initialSearch, middlewareL
139
156
  const newHeaders = Object.fromEntries([...Object.entries(internalEvent.headers), ...Object.entries(reqHeaders)]
140
157
  .map(([key, value]) => [key.toLowerCase(), value])
141
158
  .filter(([key]) => !overriddenHeaders || overriddenHeaders.has(key)));
159
+ // Next.js forwards `x-middleware-set-cookie` after applying the override allowlist,
160
+ // so it is never filtered out (see `resolve-routes.ts`).
161
+ if (middlewareSetCookie !== undefined) {
162
+ newHeaders[MIDDLEWARE_SET_COOKIE_HEADER] = middlewareSetCookie;
163
+ }
142
164
  // If the middleware returned a Rewrite, set the `url` to the pathname of the rewrite
143
165
  // NOTE: the header was added to `req` from above
144
166
  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;