@opennextjs/aws 4.1.14 → 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.
- 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/routing/cacheInterceptor.js +27 -4
- package/dist/core/routing/matcher.js +16 -4
- package/dist/core/routing/middleware.d.ts +4 -0
- package/dist/core/routing/middleware.js +19 -0
- package/dist/core/routing/routeParamPlaceholder.d.ts +14 -0
- package/dist/core/routing/routeParamPlaceholder.js +42 -0
- package/dist/core/routingHandler.js +8 -2
- 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/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/utils/cache.d.ts +14 -0
- package/dist/utils/cache.js +20 -0
- package/package.json +3 -3
|
@@ -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",
|
|
@@ -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
|
-
|
|
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;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { dynamicRouteMatcher } from "./routeMatcher";
|
|
2
|
+
// The placeholders of a route pattern: `[slug]`, `[...slug]` or `[[...slug]]`
|
|
3
|
+
const PLACEHOLDER_REGEX = /\[\[\.\.\.[^\]]+\]\]|\[(?:\.\.\.)?[^\]]+\]/g;
|
|
4
|
+
// A placeholder starts with `[`, which is either literal or percent encoded in the pathname
|
|
5
|
+
const OPENING_BRACKET_REGEX = /\[|%5b/i;
|
|
6
|
+
/**
|
|
7
|
+
* Returns whether a segment of the pathname is the placeholder of a matched dynamic App Router page.
|
|
8
|
+
*
|
|
9
|
+
* Next.js versions before 16.4.5 treat a param whose value is its placeholder (e.g. `[slug]` for `[slug]`) as an
|
|
10
|
+
* unresolved fallback param. Such values are not legitimate and must not reach Next.js.
|
|
11
|
+
*
|
|
12
|
+
* Segments are decoded like Next.js decodes param values (catch-all values are split on `/` first).
|
|
13
|
+
* Segments with a malformed percent encoding never match.
|
|
14
|
+
* Pathnames without an opening bracket return early, without matching the dynamic routes.
|
|
15
|
+
*
|
|
16
|
+
* @param pathname The internal pathname, which may include the basePath, a locale prefix and a trailing slash
|
|
17
|
+
* @returns Whether a segment of the pathname is a placeholder of a matched dynamic App Router page
|
|
18
|
+
*/
|
|
19
|
+
export function hasRouteParamPlaceholder(pathname) {
|
|
20
|
+
if (!OPENING_BRACKET_REGEX.test(pathname)) {
|
|
21
|
+
return false;
|
|
22
|
+
}
|
|
23
|
+
const placeholders = new Set(dynamicRouteMatcher(pathname)
|
|
24
|
+
.filter(({ type }) => type === "app")
|
|
25
|
+
.flatMap(({ route }) => route.match(PLACEHOLDER_REGEX) ?? []));
|
|
26
|
+
return (placeholders.size > 0 &&
|
|
27
|
+
pathname.split("/").some((segment) => placeholders.has(safeDecode(segment))));
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Decodes a URI component without throwing.
|
|
31
|
+
*
|
|
32
|
+
* @param value The encoded value
|
|
33
|
+
* @returns The decoded value, or an empty string when the encoding is malformed
|
|
34
|
+
*/
|
|
35
|
+
function safeDecode(value) {
|
|
36
|
+
try {
|
|
37
|
+
return decodeURIComponent(value);
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
return "";
|
|
41
|
+
}
|
|
42
|
+
}
|
|
@@ -6,6 +6,7 @@ import { detectLocale } from "./routing/i18n";
|
|
|
6
6
|
import { fixDataPage, getNextConfigHeaders, handleBeforeFilesRewrites, handleFallbackFalse, handleRedirects, handleRewrites, } from "./routing/matcher";
|
|
7
7
|
import { handleMiddleware } from "./routing/middleware";
|
|
8
8
|
import { dynamicRouteMatcher, staticRouteMatcher, } from "./routing/routeMatcher";
|
|
9
|
+
import { hasRouteParamPlaceholder } from "./routing/routeParamPlaceholder";
|
|
9
10
|
import { constructNextUrl, normalizeLocationHeader } from "./routing/util";
|
|
10
11
|
export const MIDDLEWARE_HEADER_PREFIX = "x-middleware-response-";
|
|
11
12
|
export const MIDDLEWARE_HEADER_PREFIX_LEN = MIDDLEWARE_HEADER_PREFIX.length;
|
|
@@ -159,7 +160,7 @@ export default async function routingHandler(event, { assetResolver }) {
|
|
|
159
160
|
const isRouteFoundBeforeAllRewrites = isStaticRoute || isDynamicRoute || isExternalRewrite;
|
|
160
161
|
// Resolve against the final internal pathname after every rewrite phase.
|
|
161
162
|
// These ordered matches are also the authoritative cache owner candidates.
|
|
162
|
-
|
|
163
|
+
let resolvedRoutes = [
|
|
163
164
|
...staticRouteMatcher(eventOrResult.rawPath),
|
|
164
165
|
...dynamicRouteMatcher(eventOrResult.rawPath),
|
|
165
166
|
];
|
|
@@ -169,7 +170,12 @@ export default async function routingHandler(event, { assetResolver }) {
|
|
|
169
170
|
isNextImageRoute ||
|
|
170
171
|
// We need to check again once all rewrites have been applied
|
|
171
172
|
staticRouteMatcher(eventOrResult.rawPath).length > 0 ||
|
|
172
|
-
dynamicRouteMatcher(eventOrResult.rawPath).length > 0)
|
|
173
|
+
dynamicRouteMatcher(eventOrResult.rawPath).length > 0) ||
|
|
174
|
+
// Next.js versions before 16.4.5 treat a param whose value is its placeholder (e.g. `[slug]`) as unresolved.
|
|
175
|
+
// Next.js receives both the rawPath and the URL pathname, which is normalized (e.g. dot segments).
|
|
176
|
+
(!isExternalRewrite &&
|
|
177
|
+
[eventOrResult.rawPath, new URL(eventOrResult.url).pathname].some(hasRouteParamPlaceholder))) {
|
|
178
|
+
resolvedRoutes = [];
|
|
173
179
|
eventOrResult = {
|
|
174
180
|
...eventOrResult,
|
|
175
181
|
rawPath: "/404",
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { ServerResponse } from "node:http";
|
|
2
|
+
import { type Writable } from "node:stream";
|
|
3
|
+
/**
|
|
4
|
+
* Largest `content-length` that is verified by buffering the body.
|
|
5
|
+
*
|
|
6
|
+
* It matches the largest response compressed by CloudFront, which only compresses responses with a `content-length`.
|
|
7
|
+
* Larger responses are streamed without `content-length`.
|
|
8
|
+
*/
|
|
9
|
+
export declare const MAX_VERIFIED_CONTENT_LENGTH = 10000000;
|
|
10
|
+
/**
|
|
11
|
+
* Writes the head of a Node.js `ServerResponse` and returns the stream to write the body to.
|
|
12
|
+
*
|
|
13
|
+
* The framing headers set by the app may not match the body that is actually written (e.g. a `fetch()` response
|
|
14
|
+
* body is decoded but keeps the upstream `content-length`). The bytes past a stale `content-length` would stay on
|
|
15
|
+
* the keep-alive connection and be read as the next response.
|
|
16
|
+
*
|
|
17
|
+
* - `transfer-encoding` is always removed, Node.js picks the transfer coding.
|
|
18
|
+
* - A `content-length` that is not a valid length is removed.
|
|
19
|
+
* - `content-length` is kept for HEAD requests and 304 responses, Node.js does not write a body for them.
|
|
20
|
+
* - Otherwise `content-length` is only kept when the body has exactly the declared length. The body is buffered
|
|
21
|
+
* until this is known, and the head is written then, so the first byte is only sent once the whole body is
|
|
22
|
+
* received. When the length doesn't match, or is larger than `MAX_VERIFIED_CONTENT_LENGTH`, `content-length` is
|
|
23
|
+
* removed and Node.js frames the body itself (chunked on HTTP/1.1).
|
|
24
|
+
*
|
|
25
|
+
* The status code and the headers are always validated synchronously, even when the head is written later.
|
|
26
|
+
*
|
|
27
|
+
* @param res The Node.js response.
|
|
28
|
+
* @param method The request method.
|
|
29
|
+
* @param statusCode The response status code.
|
|
30
|
+
* @param headers The response headers, keys are matched case-insensitively.
|
|
31
|
+
* @returns The stream to write the body to.
|
|
32
|
+
* @throws When the status code or a header is invalid, or the headers were already sent (same errors as
|
|
33
|
+
* `res.writeHead()`).
|
|
34
|
+
*/
|
|
35
|
+
export declare function writeResponseHead(res: ServerResponse, method: string | undefined, statusCode: number, headers: Record<string, string>): Writable;
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
import { Transform } from "node:stream";
|
|
2
|
+
import { error } from "../adapters/logger";
|
|
3
|
+
/**
|
|
4
|
+
* Largest `content-length` that is verified by buffering the body.
|
|
5
|
+
*
|
|
6
|
+
* It matches the largest response compressed by CloudFront, which only compresses responses with a `content-length`.
|
|
7
|
+
* Larger responses are streamed without `content-length`.
|
|
8
|
+
*/
|
|
9
|
+
export const MAX_VERIFIED_CONTENT_LENGTH = 10_000_000;
|
|
10
|
+
/**
|
|
11
|
+
* Writes the head of a Node.js `ServerResponse` and returns the stream to write the body to.
|
|
12
|
+
*
|
|
13
|
+
* The framing headers set by the app may not match the body that is actually written (e.g. a `fetch()` response
|
|
14
|
+
* body is decoded but keeps the upstream `content-length`). The bytes past a stale `content-length` would stay on
|
|
15
|
+
* the keep-alive connection and be read as the next response.
|
|
16
|
+
*
|
|
17
|
+
* - `transfer-encoding` is always removed, Node.js picks the transfer coding.
|
|
18
|
+
* - A `content-length` that is not a valid length is removed.
|
|
19
|
+
* - `content-length` is kept for HEAD requests and 304 responses, Node.js does not write a body for them.
|
|
20
|
+
* - Otherwise `content-length` is only kept when the body has exactly the declared length. The body is buffered
|
|
21
|
+
* until this is known, and the head is written then, so the first byte is only sent once the whole body is
|
|
22
|
+
* received. When the length doesn't match, or is larger than `MAX_VERIFIED_CONTENT_LENGTH`, `content-length` is
|
|
23
|
+
* removed and Node.js frames the body itself (chunked on HTTP/1.1).
|
|
24
|
+
*
|
|
25
|
+
* The status code and the headers are always validated synchronously, even when the head is written later.
|
|
26
|
+
*
|
|
27
|
+
* @param res The Node.js response.
|
|
28
|
+
* @param method The request method.
|
|
29
|
+
* @param statusCode The response status code.
|
|
30
|
+
* @param headers The response headers, keys are matched case-insensitively.
|
|
31
|
+
* @returns The stream to write the body to.
|
|
32
|
+
* @throws When the status code or a header is invalid, or the headers were already sent (same errors as
|
|
33
|
+
* `res.writeHead()`).
|
|
34
|
+
*/
|
|
35
|
+
export function writeResponseHead(res, method, statusCode, headers) {
|
|
36
|
+
validateHead(res, statusCode);
|
|
37
|
+
let contentLength;
|
|
38
|
+
for (const [key, value] of Object.entries(headers)) {
|
|
39
|
+
const lowerKey = key.toLowerCase();
|
|
40
|
+
if (lowerKey === "content-length") {
|
|
41
|
+
contentLength = value;
|
|
42
|
+
}
|
|
43
|
+
else if (lowerKey !== "transfer-encoding") {
|
|
44
|
+
// `setHeader()` validates the header like `writeHead()` does
|
|
45
|
+
res.setHeader(key, value);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
const expectedLength = parseContentLength(contentLength);
|
|
49
|
+
if (!Number.isNaN(expectedLength) &&
|
|
50
|
+
(method?.toUpperCase() === "HEAD" || statusCode === 304)) {
|
|
51
|
+
res.setHeader("content-length", String(expectedLength));
|
|
52
|
+
res.writeHead(statusCode);
|
|
53
|
+
res.flushHeaders();
|
|
54
|
+
return res;
|
|
55
|
+
}
|
|
56
|
+
if (statusCode === 204 || !(expectedLength <= MAX_VERIFIED_CONTENT_LENGTH)) {
|
|
57
|
+
res.writeHead(statusCode);
|
|
58
|
+
res.flushHeaders();
|
|
59
|
+
return res;
|
|
60
|
+
}
|
|
61
|
+
const body = new ContentLengthVerifier(res, statusCode, expectedLength);
|
|
62
|
+
body.pipe(res);
|
|
63
|
+
return body;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Checks that the head can be written, the way `res.writeHead()` does.
|
|
67
|
+
*
|
|
68
|
+
* The headers are validated by `res.setHeader()`.
|
|
69
|
+
*
|
|
70
|
+
* @param res The Node.js response.
|
|
71
|
+
* @param statusCode The response status code.
|
|
72
|
+
* @throws {Error} When the headers were already sent (code `ERR_HTTP_HEADERS_SENT`).
|
|
73
|
+
* @throws {RangeError} When the status code is not in 100-999 (code `ERR_HTTP_INVALID_STATUS_CODE`).
|
|
74
|
+
*/
|
|
75
|
+
function validateHead(res, statusCode) {
|
|
76
|
+
if (res.headersSent) {
|
|
77
|
+
throw Object.assign(new Error("Cannot write headers after they are sent to the client"), { code: "ERR_HTTP_HEADERS_SENT" });
|
|
78
|
+
}
|
|
79
|
+
const code = statusCode | 0;
|
|
80
|
+
if (code < 100 || code > 999) {
|
|
81
|
+
throw Object.assign(new RangeError(`Invalid status code: ${statusCode}`), {
|
|
82
|
+
code: "ERR_HTTP_INVALID_STATUS_CODE",
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Parses a `content-length` header value.
|
|
88
|
+
*
|
|
89
|
+
* @param contentLength The header value.
|
|
90
|
+
* @returns The length, or `NaN` when the value is missing or not a valid length.
|
|
91
|
+
*/
|
|
92
|
+
function parseContentLength(contentLength) {
|
|
93
|
+
const value = contentLength?.trim() ?? "";
|
|
94
|
+
const length = /^\d+$/.test(value) ? Number(value) : Number.NaN;
|
|
95
|
+
return Number.isSafeInteger(length) ? length : Number.NaN;
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Buffers the body until it is known whether it has the declared length, then writes the head and the body.
|
|
99
|
+
*
|
|
100
|
+
* The headers, other than `content-length`, are already set on the response.
|
|
101
|
+
*/
|
|
102
|
+
class ContentLengthVerifier extends Transform {
|
|
103
|
+
res;
|
|
104
|
+
statusCode;
|
|
105
|
+
expectedLength;
|
|
106
|
+
chunks = [];
|
|
107
|
+
length = 0;
|
|
108
|
+
headWritten = false;
|
|
109
|
+
/**
|
|
110
|
+
* @param res The Node.js response.
|
|
111
|
+
* @param statusCode The response status code, already validated.
|
|
112
|
+
* @param expectedLength The declared `content-length`.
|
|
113
|
+
*/
|
|
114
|
+
constructor(res, statusCode, expectedLength) {
|
|
115
|
+
super();
|
|
116
|
+
this.res = res;
|
|
117
|
+
this.statusCode = statusCode;
|
|
118
|
+
this.expectedLength = expectedLength;
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Buffers the chunk until the head is written, then passes it through.
|
|
122
|
+
*
|
|
123
|
+
* The head is written without `content-length` as soon as the body is longer than declared.
|
|
124
|
+
*
|
|
125
|
+
* @param chunk The body chunk.
|
|
126
|
+
* @param _encoding Unused, chunks are buffers.
|
|
127
|
+
* @param callback Called once the chunk is processed.
|
|
128
|
+
*/
|
|
129
|
+
_transform(chunk, _encoding, callback) {
|
|
130
|
+
if (this.headWritten) {
|
|
131
|
+
callback(null, chunk);
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
this.chunks.push(chunk);
|
|
135
|
+
this.length += chunk.length;
|
|
136
|
+
if (this.length > this.expectedLength) {
|
|
137
|
+
this.writeHead(false);
|
|
138
|
+
}
|
|
139
|
+
callback();
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Writes the head, when not already written, once the whole body is received.
|
|
143
|
+
*
|
|
144
|
+
* @param callback Called once the head and the buffered chunks are written.
|
|
145
|
+
*/
|
|
146
|
+
_flush(callback) {
|
|
147
|
+
if (!this.headWritten) {
|
|
148
|
+
this.writeHead(this.length === this.expectedLength);
|
|
149
|
+
}
|
|
150
|
+
callback();
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* Writes the head and the buffered chunks.
|
|
154
|
+
*
|
|
155
|
+
* This runs from the stream callbacks, where a thrown error would be uncaught and crash the process. The
|
|
156
|
+
* head is validated synchronously in `writeResponseHead()`, if writing it still fails, the response is destroyed.
|
|
157
|
+
*
|
|
158
|
+
* @param keepContentLength Whether the body has the declared length.
|
|
159
|
+
*/
|
|
160
|
+
writeHead(keepContentLength) {
|
|
161
|
+
this.headWritten = true;
|
|
162
|
+
const chunks = this.chunks;
|
|
163
|
+
this.chunks = [];
|
|
164
|
+
try {
|
|
165
|
+
if (keepContentLength) {
|
|
166
|
+
this.res.setHeader("content-length", String(this.expectedLength));
|
|
167
|
+
}
|
|
168
|
+
this.res.writeHead(this.statusCode);
|
|
169
|
+
}
|
|
170
|
+
catch (e) {
|
|
171
|
+
error("Failed to write the response head", e);
|
|
172
|
+
this.res.destroy();
|
|
173
|
+
// Without an error, no `error` event is emitted
|
|
174
|
+
this.destroy();
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
for (const chunk of chunks) {
|
|
178
|
+
this.push(chunk);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
package/dist/http/util.d.ts
CHANGED
|
@@ -8,6 +8,17 @@ export declare const convertHeader: (header: http.OutgoingHttpHeader) => string;
|
|
|
8
8
|
* @returns A list of Set-Cookie header
|
|
9
9
|
*/
|
|
10
10
|
export declare function parseSetCookieHeader(cookies: string | string[] | null | undefined): string[];
|
|
11
|
+
/**
|
|
12
|
+
* Converts the headers of a response returned by an edge function.
|
|
13
|
+
*
|
|
14
|
+
* `Set-Cookie` headers are returned as a list.
|
|
15
|
+
* The internal `x-middleware-set-cookie` header is dropped: `NextResponse` sets it when
|
|
16
|
+
* `response.cookies` is modified (i.e. in a route handler) and it contains the `HttpOnly` cookies.
|
|
17
|
+
*
|
|
18
|
+
* @param headers The headers of the response
|
|
19
|
+
* @returns The response headers to send to the client
|
|
20
|
+
*/
|
|
21
|
+
export declare function convertEdgeResponseHeaders(headers: Pick<Headers, "forEach" | "getSetCookie">): Record<string, string | string[]>;
|
|
11
22
|
/**
|
|
12
23
|
*
|
|
13
24
|
* Get the query object from an iterable of [key, value] pairs
|
package/dist/http/util.js
CHANGED
|
@@ -58,6 +58,32 @@ export function parseSetCookieHeader(cookies) {
|
|
|
58
58
|
}
|
|
59
59
|
return cookies;
|
|
60
60
|
}
|
|
61
|
+
/**
|
|
62
|
+
* Converts the headers of a response returned by an edge function.
|
|
63
|
+
*
|
|
64
|
+
* `Set-Cookie` headers are returned as a list.
|
|
65
|
+
* The internal `x-middleware-set-cookie` header is dropped: `NextResponse` sets it when
|
|
66
|
+
* `response.cookies` is modified (i.e. in a route handler) and it contains the `HttpOnly` cookies.
|
|
67
|
+
*
|
|
68
|
+
* @param headers The headers of the response
|
|
69
|
+
* @returns The response headers to send to the client
|
|
70
|
+
*/
|
|
71
|
+
export function convertEdgeResponseHeaders(headers) {
|
|
72
|
+
const responseHeaders = {};
|
|
73
|
+
headers.forEach((value, key) => {
|
|
74
|
+
const lowerKey = key.toLowerCase();
|
|
75
|
+
// Headers.forEach folds same-name headers; use getSetCookie() below instead.
|
|
76
|
+
if (lowerKey === "set-cookie" || lowerKey === "x-middleware-set-cookie") {
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
responseHeaders[key] = value;
|
|
80
|
+
});
|
|
81
|
+
const setCookies = headers.getSetCookie();
|
|
82
|
+
if (setCookies.length > 0) {
|
|
83
|
+
responseHeaders["set-cookie"] = setCookies;
|
|
84
|
+
}
|
|
85
|
+
return responseHeaders;
|
|
86
|
+
}
|
|
61
87
|
/**
|
|
62
88
|
*
|
|
63
89
|
* Get the query object from an iterable of [key, value] pairs
|
|
@@ -4,7 +4,11 @@ const fetchProxy = {
|
|
|
4
4
|
// @ts-ignore
|
|
5
5
|
proxy: async (internalEvent) => {
|
|
6
6
|
const { url, headers: eventHeaders, method, body } = internalEvent;
|
|
7
|
-
const headers = Object.fromEntries(Object.entries(eventHeaders).filter(([key]) =>
|
|
7
|
+
const headers = Object.fromEntries(Object.entries(eventHeaders).filter(([key]) => !["cf-connecting-ip", "x-forwarded-host"].includes(key.toLowerCase())));
|
|
8
|
+
// Like Next.js, do not relay the `x-forwarded-host` sent by the client
|
|
9
|
+
if (eventHeaders.host) {
|
|
10
|
+
headers["x-forwarded-host"] = eventHeaders.host;
|
|
11
|
+
}
|
|
8
12
|
const response = await fetch(url, {
|
|
9
13
|
method,
|
|
10
14
|
headers,
|
|
@@ -6,6 +6,7 @@ function filterHeadersForProxy(headers) {
|
|
|
6
6
|
const filteredHeaders = {};
|
|
7
7
|
const disallowedHeaders = [
|
|
8
8
|
"host",
|
|
9
|
+
"x-forwarded-host",
|
|
9
10
|
"connection",
|
|
10
11
|
"via",
|
|
11
12
|
"x-cache",
|
|
@@ -30,6 +31,10 @@ const nodeProxy = {
|
|
|
30
31
|
debug("proxyRequest", url);
|
|
31
32
|
return new Promise((resolve, reject) => {
|
|
32
33
|
const filteredHeaders = filterHeadersForProxy(headers);
|
|
34
|
+
// Like Next.js, do not relay the `x-forwarded-host` sent by the client
|
|
35
|
+
if (headers.host) {
|
|
36
|
+
filteredHeaders["x-forwarded-host"] = headers.host;
|
|
37
|
+
}
|
|
33
38
|
debug("filteredHeaders", filteredHeaders);
|
|
34
39
|
const req = request(url, {
|
|
35
40
|
headers: filteredHeaders,
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
2
|
import express from "express";
|
|
3
3
|
import { NextConfig } from "../../adapters/config/index.js";
|
|
4
|
+
import { writeResponseHead } from "../../http/framing.js";
|
|
4
5
|
import { getMonorepoRelativePath } from "../../utils/normalize-path.js";
|
|
5
6
|
const wrapper = async (handler, converter) => {
|
|
6
7
|
const app = express();
|
|
@@ -16,8 +17,7 @@ const wrapper = async (handler, converter) => {
|
|
|
16
17
|
const internalEvent = await converter.convertFrom(req);
|
|
17
18
|
const streamCreator = {
|
|
18
19
|
writeHeaders: (prelude) => {
|
|
19
|
-
res.
|
|
20
|
-
return res;
|
|
20
|
+
return writeResponseHead(res, req.method, prelude.statusCode, prelude.headers);
|
|
21
21
|
},
|
|
22
22
|
};
|
|
23
23
|
await imageHandler(internalEvent, { streamCreator });
|
|
@@ -34,9 +34,7 @@ const wrapper = async (handler, converter) => {
|
|
|
34
34
|
const streamCreator = {
|
|
35
35
|
writeHeaders: (prelude) => {
|
|
36
36
|
res.setHeader("Set-Cookie", prelude.cookies);
|
|
37
|
-
res.
|
|
38
|
-
res.flushHeaders();
|
|
39
|
-
return res;
|
|
37
|
+
return writeResponseHead(res, req.method, prelude.statusCode, prelude.headers);
|
|
40
38
|
},
|
|
41
39
|
onFinish: () => { },
|
|
42
40
|
abortSignal: abortController.signal,
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { createServer } from "node:http";
|
|
2
|
+
import { writeResponseHead } from "../../http/framing.js";
|
|
2
3
|
import { debug, error } from "../../adapters/logger";
|
|
3
4
|
const wrapper = async (handler, converter) => {
|
|
4
5
|
const server = createServer(async (req, res) => {
|
|
@@ -7,9 +8,7 @@ const wrapper = async (handler, converter) => {
|
|
|
7
8
|
const streamCreator = {
|
|
8
9
|
writeHeaders: (prelude) => {
|
|
9
10
|
res.setHeader("Set-Cookie", prelude.cookies);
|
|
10
|
-
res.
|
|
11
|
-
res.flushHeaders();
|
|
12
|
-
return res;
|
|
11
|
+
return writeResponseHead(res, req.method, prelude.statusCode, prelude.headers);
|
|
13
12
|
},
|
|
14
13
|
abortSignal: abortController.signal,
|
|
15
14
|
};
|
package/dist/utils/cache.d.ts
CHANGED
|
@@ -1,4 +1,18 @@
|
|
|
1
1
|
import type { CacheValue, NextModeTagCacheWriteInput, OriginalTagCacheWriteInput, WithLastModified } from "../types/overrides.js";
|
|
2
|
+
/**
|
|
3
|
+
* Computes the key used to store a composable (`"use cache"`) entry.
|
|
4
|
+
*
|
|
5
|
+
* The cache key provided by Next.js embeds the serialized arguments of the cached function and may
|
|
6
|
+
* contain any UTF-16 string. Storage backends transform keys (path normalization, URL parsing,
|
|
7
|
+
* UTF-8 encoding, ...) which could map distinct Next.js keys to the same stored entry.
|
|
8
|
+
*
|
|
9
|
+
* The storage key is the sha256 of the exact UTF-16 code units of the cache key, as a 64 characters
|
|
10
|
+
* lowercase hex string that every backend can store as is.
|
|
11
|
+
*
|
|
12
|
+
* @param cacheKey The cache key provided by Next.js
|
|
13
|
+
* @returns The key to use for the incremental cache and the tag cache
|
|
14
|
+
*/
|
|
15
|
+
export declare function getComposableCacheStorageKey(cacheKey: string): string;
|
|
2
16
|
/**
|
|
3
17
|
*
|
|
4
18
|
* @param key The key for that specific cache entry
|
package/dist/utils/cache.js
CHANGED
|
@@ -1,6 +1,26 @@
|
|
|
1
|
+
import { Buffer } from "node:buffer";
|
|
2
|
+
import { createHash } from "node:crypto";
|
|
1
3
|
import { debug } from "../adapters/logger";
|
|
2
4
|
import { CACHE_TAGS_HEADER } from "./cacheHeaders";
|
|
3
5
|
import { compareSemver } from "./semver";
|
|
6
|
+
/**
|
|
7
|
+
* Computes the key used to store a composable (`"use cache"`) entry.
|
|
8
|
+
*
|
|
9
|
+
* The cache key provided by Next.js embeds the serialized arguments of the cached function and may
|
|
10
|
+
* contain any UTF-16 string. Storage backends transform keys (path normalization, URL parsing,
|
|
11
|
+
* UTF-8 encoding, ...) which could map distinct Next.js keys to the same stored entry.
|
|
12
|
+
*
|
|
13
|
+
* The storage key is the sha256 of the exact UTF-16 code units of the cache key, as a 64 characters
|
|
14
|
+
* lowercase hex string that every backend can store as is.
|
|
15
|
+
*
|
|
16
|
+
* @param cacheKey The cache key provided by Next.js
|
|
17
|
+
* @returns The key to use for the incremental cache and the tag cache
|
|
18
|
+
*/
|
|
19
|
+
export function getComposableCacheStorageKey(cacheKey) {
|
|
20
|
+
return createHash("sha256")
|
|
21
|
+
.update(Buffer.from(cacheKey, "utf16le"))
|
|
22
|
+
.digest("hex");
|
|
23
|
+
}
|
|
4
24
|
/**
|
|
5
25
|
*
|
|
6
26
|
* @param key The key for that specific cache entry
|
package/package.json
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"access": "public"
|
|
4
4
|
},
|
|
5
5
|
"name": "@opennextjs/aws",
|
|
6
|
-
"version": "4.1.
|
|
6
|
+
"version": "4.1.15",
|
|
7
7
|
"bin": {
|
|
8
8
|
"open-next": "./dist/index.js"
|
|
9
9
|
},
|
|
@@ -57,13 +57,13 @@
|
|
|
57
57
|
"@types/node": "20.17.6",
|
|
58
58
|
"concurrently": "^9.2.4",
|
|
59
59
|
"diff": "^9.0.0",
|
|
60
|
-
"next": "16.4.
|
|
60
|
+
"next": "16.4.5",
|
|
61
61
|
"tsc-alias": "^1.9.7",
|
|
62
62
|
"typescript": "5.9.3"
|
|
63
63
|
},
|
|
64
64
|
"peerDependencies": {
|
|
65
65
|
"diff": "^9.0.0",
|
|
66
|
-
"next": ">=15.5.
|
|
66
|
+
"next": ">=15.5.29 <16 || >=16.4.5"
|
|
67
67
|
},
|
|
68
68
|
"peerDependenciesMeta": {
|
|
69
69
|
"diff": {
|