@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.
- package/dist/adapters/cache.js +2 -1
- package/dist/adapters/composable-cache.d.ts +20 -0
- package/dist/adapters/composable-cache.js +33 -8
- package/dist/adapters/edge-adapter.js +2 -11
- package/dist/core/requestHandler.d.ts +17 -0
- package/dist/core/requestHandler.js +48 -18
- package/dist/core/revalidate.js +1 -0
- package/dist/core/routing/cacheInterceptor.js +28 -24
- package/dist/core/routing/matcher.d.ts +14 -1
- package/dist/core/routing/matcher.js +41 -9
- package/dist/core/routing/middleware.d.ts +4 -0
- package/dist/core/routing/middleware.js +22 -0
- package/dist/core/routing/routeParamPlaceholder.d.ts +14 -0
- package/dist/core/routing/routeParamPlaceholder.js +42 -0
- package/dist/core/routing/util.js +10 -2
- package/dist/core/routingHandler.d.ts +2 -0
- package/dist/core/routingHandler.js +39 -29
- package/dist/http/framing.d.ts +35 -0
- package/dist/http/framing.js +181 -0
- package/dist/http/util.d.ts +11 -0
- package/dist/http/util.js +26 -0
- package/dist/minimize-js.d.ts +9 -0
- package/dist/minimize-js.js +27 -26
- package/dist/overrides/proxyExternalRequest/fetch.js +5 -1
- package/dist/overrides/proxyExternalRequest/node.js +5 -0
- package/dist/overrides/wrappers/express-dev.js +3 -5
- package/dist/overrides/wrappers/node.js +2 -3
- package/dist/types/global.d.ts +12 -0
- package/dist/utils/cache.d.ts +14 -0
- package/dist/utils/cache.js +20 -0
- package/dist/utils/normalize-path.d.ts +15 -0
- package/dist/utils/normalize-path.js +32 -0
- package/package.json +5 -5
package/dist/adapters/cache.js
CHANGED
|
@@ -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
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
-
|
|
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(
|
|
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:
|
|
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
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
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
|
-
...
|
|
171
|
+
...requestHeaders,
|
|
142
172
|
//#override appendPrefetch
|
|
143
173
|
purpose: "prefetch",
|
|
144
174
|
//#endOverride
|
package/dist/core/revalidate.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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;
|