@opennextjs/aws 4.1.0 → 4.1.2
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 +42 -7
- package/dist/adapters/middleware.js +2 -1
- package/dist/adapters/plugins/image-optimization/image-optimization.replacement.js +6 -5
- package/dist/adapters/revalidate.js +4 -3
- package/dist/build/copyTracedFiles.js +9 -0
- package/dist/build/createAssets.js +10 -2
- package/dist/build/createImageOptimizationBundle.js +1 -1
- package/dist/core/requestHandler.js +2 -1
- package/dist/core/routing/cacheInterceptor.js +92 -48
- package/dist/core/routing/middleware.js +3 -2
- package/dist/core/routing/util.d.ts +10 -0
- package/dist/core/routing/util.js +28 -29
- package/dist/core/routingHandler.js +2 -1
- package/dist/http/openNextResponse.js +4 -11
- package/dist/overrides/queue/direct.js +3 -2
- package/dist/types/cache.d.ts +1 -1
- package/dist/types/overrides.d.ts +25 -3
- package/dist/utils/cache.d.ts +15 -0
- package/dist/utils/cache.js +31 -2
- package/dist/utils/cacheHeaders.d.ts +52 -0
- package/dist/utils/cacheHeaders.js +58 -0
- package/package.json +3 -2
package/dist/adapters/cache.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import { getTagsFromValue, hasBeenRevalidated, isStale, writeTags, } from "../utils/cache";
|
|
1
|
+
import { getStaleLastModified, getTagsFromValue, hasBeenRevalidated, isStale, writeTags, } from "../utils/cache";
|
|
2
2
|
import { isBinaryContentType } from "../utils/binary";
|
|
3
|
+
import { CACHE_TAGS_HEADER } from "../utils/cacheHeaders";
|
|
3
4
|
import { compareSemver } from "../utils/semver";
|
|
4
5
|
import { debug, error, warn } from "./logger";
|
|
5
6
|
export const SOFT_TAG_PREFIX = "_N_T_/";
|
|
@@ -92,8 +93,12 @@ export default class Cache {
|
|
|
92
93
|
: await isStale(key, tags, _lastModified);
|
|
93
94
|
const store = globalThis.__openNextAls.getStore();
|
|
94
95
|
if (store) {
|
|
96
|
+
// The sentinel is kept as is in the store so that the revalidation queue keeps
|
|
97
|
+
// deduplicating every stale request for the same path.
|
|
95
98
|
store.lastModified = _isStale ? 1 : _lastModified;
|
|
96
|
-
|
|
99
|
+
}
|
|
100
|
+
if (_isStale) {
|
|
101
|
+
_lastModified = getStaleLastModified(cacheData.revalidate);
|
|
97
102
|
}
|
|
98
103
|
if (cacheData?.type === "route") {
|
|
99
104
|
return {
|
|
@@ -111,6 +116,16 @@ export default class Cache {
|
|
|
111
116
|
};
|
|
112
117
|
}
|
|
113
118
|
if (cacheData?.type === "page" || cacheData?.type === "app") {
|
|
119
|
+
// Without the html there is no valid value to return, so we report a miss and let
|
|
120
|
+
// the server render the page. Next.js behaves the same way, its file system cache
|
|
121
|
+
// reads the `.html` file first and turns a read failure into a miss.
|
|
122
|
+
//
|
|
123
|
+
// The check is on `undefined` and not on falsiness because an empty string is a
|
|
124
|
+
// valid value: a PPR route with an empty static shell has an empty `.html` file.
|
|
125
|
+
if (cacheData.html === undefined) {
|
|
126
|
+
warn("No html in the cache entry", key);
|
|
127
|
+
return null;
|
|
128
|
+
}
|
|
114
129
|
if (compareSemver(globalThis.nextVersion, ">=", "15.0.0") &&
|
|
115
130
|
cacheData?.type === "app") {
|
|
116
131
|
const segmentData = new Map();
|
|
@@ -124,7 +139,13 @@ export default class Cache {
|
|
|
124
139
|
value: {
|
|
125
140
|
kind: "APP_PAGE",
|
|
126
141
|
html: cacheData.html,
|
|
127
|
-
|
|
142
|
+
// `rsc` is absent when the build collected neither a `.rsc` nor a
|
|
143
|
+
// `.prefetch.rsc` file, see `CachedFile`. An undefined value is expected: the
|
|
144
|
+
// Next.js file system cache also leaves `rscData` undefined for fallback
|
|
145
|
+
// shells, and on 16.2+ for postponed PPR routes.
|
|
146
|
+
rscData: cacheData.rsc === undefined
|
|
147
|
+
? undefined
|
|
148
|
+
: Buffer.from(cacheData.rsc),
|
|
128
149
|
status: meta?.status,
|
|
129
150
|
headers: meta?.headers,
|
|
130
151
|
postponed: meta?.postponed,
|
|
@@ -132,6 +153,13 @@ export default class Cache {
|
|
|
132
153
|
},
|
|
133
154
|
};
|
|
134
155
|
}
|
|
156
|
+
const pageData = cacheData.type === "page" ? cacheData.json : cacheData.rsc;
|
|
157
|
+
// Only reachable for an app page on Next.js < 15, where an absent `rsc` leaves us
|
|
158
|
+
// without any page data to return.
|
|
159
|
+
if (pageData === undefined) {
|
|
160
|
+
warn("No page data in the cache entry", key);
|
|
161
|
+
return null;
|
|
162
|
+
}
|
|
135
163
|
return {
|
|
136
164
|
lastModified: _lastModified,
|
|
137
165
|
value: {
|
|
@@ -139,7 +167,7 @@ export default class Cache {
|
|
|
139
167
|
? "PAGES"
|
|
140
168
|
: "PAGE",
|
|
141
169
|
html: cacheData.html,
|
|
142
|
-
pageData
|
|
170
|
+
pageData,
|
|
143
171
|
status: meta?.status,
|
|
144
172
|
headers: meta?.headers,
|
|
145
173
|
},
|
|
@@ -232,7 +260,7 @@ export default class Cache {
|
|
|
232
260
|
await globalThis.incrementalCache.set(key, {
|
|
233
261
|
type: "app",
|
|
234
262
|
html,
|
|
235
|
-
rsc: rscData
|
|
263
|
+
rsc: rscData?.toString("utf8"),
|
|
236
264
|
meta: {
|
|
237
265
|
status,
|
|
238
266
|
headers,
|
|
@@ -408,11 +436,18 @@ export default class Cache {
|
|
|
408
436
|
}
|
|
409
437
|
// Write derivedTags to the tag cache
|
|
410
438
|
// If we use an in house version of getDerivedTags in build we should use it here instead of next's one
|
|
439
|
+
// `x-next-cache-tags` is always a comma separated string: Next.js sets it from
|
|
440
|
+
// `metadata.fetchTags`, which is a string. The header type allows an array because it
|
|
441
|
+
// is shared with headers that can legitimately repeat, so we narrow on `typeof` here.
|
|
442
|
+
// Next.js guards the same way when it reads the header back.
|
|
443
|
+
const pageCacheTags = data?.kind === "PAGE" || data?.kind === "APP_PAGE"
|
|
444
|
+
? data.headers?.[CACHE_TAGS_HEADER]
|
|
445
|
+
: undefined;
|
|
411
446
|
const derivedTags = data?.kind === "FETCH"
|
|
412
447
|
? //@ts-expect-error - On older versions of next, ctx was a number, but for these cases we use data?.data?.tags
|
|
413
448
|
(ctx?.tags ?? data?.data?.tags ?? []) // before version 14 next.js used data?.data?.tags so we keep it for backward compatibility
|
|
414
|
-
:
|
|
415
|
-
?
|
|
449
|
+
: typeof pageCacheTags === "string"
|
|
450
|
+
? pageCacheTags.split(",")
|
|
416
451
|
: [];
|
|
417
452
|
debug("derivedTags", derivedTags);
|
|
418
453
|
// Get all tags stored in dynamodb for the given key
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { ISR_HEADER } from "../utils/cacheHeaders";
|
|
1
2
|
import { runWithOpenNextRequestContext } from "../utils/promise";
|
|
2
3
|
import { debug, error } from "../adapters/logger";
|
|
3
4
|
import { createGenericHandler } from "../core/createGenericHandler";
|
|
@@ -21,7 +22,7 @@ const defaultHandler = async (internalEvent, options) => {
|
|
|
21
22
|
const requestId = Math.random().toString(36);
|
|
22
23
|
// We run everything in the async local storage context so that it is available in the external middleware
|
|
23
24
|
return runWithOpenNextRequestContext({
|
|
24
|
-
isISRRevalidation: internalEvent.headers[
|
|
25
|
+
isISRRevalidation: internalEvent.headers[ISR_HEADER] === "1",
|
|
25
26
|
waitUntil: options?.waitUntil,
|
|
26
27
|
requestId,
|
|
27
28
|
}, async () => {
|
|
@@ -17,18 +17,19 @@ export async function optimizeImage(headers, imageParams, nextConfig, handleRequ
|
|
|
17
17
|
*
|
|
18
18
|
* https://github.com/vercel/next.js/blob/bfe2ab4/packages/next/src/server/image-optimizer.ts#L711
|
|
19
19
|
*/
|
|
20
|
+
// Next 16 rejects upstream hosts resolving to private IPs unless the user
|
|
21
|
+
// opts out with `images.dangerouslyAllowLocalIP` — forward their setting
|
|
22
|
+
// instead of always disabling the opt-out.
|
|
23
|
+
const dangerouslyAllowLocalIP = Boolean(nextConfig.images?.dangerouslyAllowLocalIP);
|
|
20
24
|
const callFetchExternalImage = () => {
|
|
21
25
|
const version = globalThis.nextVersion;
|
|
22
|
-
// v16.1.5+
|
|
23
26
|
if (compareSemver(version, ">=", "16.1.5")) {
|
|
24
|
-
return fetchExternalImage(href,
|
|
27
|
+
return fetchExternalImage(href, dangerouslyAllowLocalIP, maximumResponseBody);
|
|
25
28
|
}
|
|
26
|
-
// v16.0.0–v16.1.4
|
|
27
29
|
if (compareSemver(version, ">=", "16")) {
|
|
28
30
|
// @ts-expect-error - fetchExternalImage signature varies across Next.js versions
|
|
29
|
-
return fetchExternalImage(href,
|
|
31
|
+
return fetchExternalImage(href, dangerouslyAllowLocalIP);
|
|
30
32
|
}
|
|
31
|
-
// v15.5.10–v15.x
|
|
32
33
|
if (compareSemver(version, ">=", "15.5.10")) {
|
|
33
34
|
// @ts-expect-error - fetchExternalImage signature varies across Next.js versions
|
|
34
35
|
return fetchExternalImage(href, maximumResponseBody);
|
|
@@ -2,6 +2,7 @@ import fs from "node:fs";
|
|
|
2
2
|
import https from "node:https";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { createGenericHandler } from "../core/createGenericHandler.js";
|
|
5
|
+
import { ISR_HEADER, NEXTJS_CACHE_HEADER, PRERENDER_REVALIDATE_HEADER, } from "../utils/cacheHeaders.js";
|
|
5
6
|
import { debug, error } from "./logger.js";
|
|
6
7
|
const prerenderManifest = loadPrerenderManifest();
|
|
7
8
|
const ALLOWED_STATUS_CODES = [200, 307, 308, 404];
|
|
@@ -23,8 +24,8 @@ const defaultHandler = async (event) => {
|
|
|
23
24
|
const req = https.request(`https://${host}${url}`, {
|
|
24
25
|
method: "HEAD",
|
|
25
26
|
headers: {
|
|
26
|
-
|
|
27
|
-
|
|
27
|
+
[PRERENDER_REVALIDATE_HEADER]: prerenderManifest.preview.previewModeId,
|
|
28
|
+
[ISR_HEADER]: "1",
|
|
28
29
|
},
|
|
29
30
|
}, (res) => {
|
|
30
31
|
debug("revalidating", {
|
|
@@ -34,7 +35,7 @@ const defaultHandler = async (event) => {
|
|
|
34
35
|
statusCode: res.statusCode,
|
|
35
36
|
});
|
|
36
37
|
if (!ALLOWED_STATUS_CODES.includes(res.statusCode ?? 0) ||
|
|
37
|
-
res.headers[
|
|
38
|
+
res.headers[NEXTJS_CACHE_HEADER] !== "REVALIDATED") {
|
|
38
39
|
failedRecords.push(record);
|
|
39
40
|
}
|
|
40
41
|
resolve(res);
|
|
@@ -159,6 +159,15 @@ File ${serverPath} does not exist
|
|
|
159
159
|
if (existsSync(path.join(dotNextDir, INSTRUMENTATION_TRACE_FILE))) {
|
|
160
160
|
// We still need to copy the nft.json file so that computeCopyFilesForPage doesn't throw
|
|
161
161
|
copyFileAndMakeOwnerWritable(path.join(dotNextDir, INSTRUMENTATION_TRACE_FILE), path.join(standaloneNextDir, INSTRUMENTATION_TRACE_FILE));
|
|
162
|
+
// Next 16's standalone output no longer includes `server/instrumentation.js`
|
|
163
|
+
// in the standalone dir, but `computeCopyFilesForPage` asserts it exists
|
|
164
|
+
// there. Copy it from the build dir (mirroring the .nft.json copy above) so
|
|
165
|
+
// the assertion passes and the instrumentation file ships in the bundle.
|
|
166
|
+
const instrumentationServerFile = path.join("server", "instrumentation.js");
|
|
167
|
+
if (existsSync(path.join(dotNextDir, instrumentationServerFile)) &&
|
|
168
|
+
!existsSync(path.join(standaloneNextDir, instrumentationServerFile))) {
|
|
169
|
+
copyFileAndMakeOwnerWritable(path.join(dotNextDir, instrumentationServerFile), path.join(standaloneNextDir, instrumentationServerFile));
|
|
170
|
+
}
|
|
162
171
|
computeCopyFilesForPage("instrumentation");
|
|
163
172
|
logger.debug("Adding instrumentation trace files");
|
|
164
173
|
}
|
|
@@ -4,6 +4,7 @@ import { loadConfig } from "../adapters/config/util.js";
|
|
|
4
4
|
import { safeParseJsonFile } from "../utils/safe-json-parse.js";
|
|
5
5
|
import logger from "../logger.js";
|
|
6
6
|
import { isBinaryContentType } from "../utils/binary.js";
|
|
7
|
+
import { CACHE_TAGS_HEADER } from "../utils/cacheHeaders.js";
|
|
7
8
|
import * as buildHelper from "./helper.js";
|
|
8
9
|
/**
|
|
9
10
|
* Copy the static assets to the output folder
|
|
@@ -94,6 +95,13 @@ export function createCacheAssets(options) {
|
|
|
94
95
|
case ".json":
|
|
95
96
|
case ".body":
|
|
96
97
|
case ".rsc": {
|
|
98
|
+
// Stripping `.prefetch` maps both `<page>.rsc` and `<page>.prefetch.rsc` onto
|
|
99
|
+
// the same cache entry, so the `rsc` field can hold either payload. When both
|
|
100
|
+
// files exist the first one traversed wins, as the spread below keeps the value
|
|
101
|
+
// already recorded for that extension.
|
|
102
|
+
//
|
|
103
|
+
// Next 16.1 removed `.prefetch.rsc`, so the strip is a no-op on Next 16 where
|
|
104
|
+
// the `rsc` field always holds a full payload.
|
|
97
105
|
const newFilePath = path
|
|
98
106
|
.join(outputCachePath, relativePath)
|
|
99
107
|
.substring(0, path.join(outputCachePath, relativePath).length - ext.length)
|
|
@@ -181,8 +189,8 @@ export function createCacheAssets(options) {
|
|
|
181
189
|
buildHelper.traverseFiles(sourceDir, ({ absolutePath, relativePath }) => absolutePath.endsWith(".meta") && !isFileSkipped(relativePath), ({ absolutePath, relativePath }) => {
|
|
182
190
|
const fileContent = fs.readFileSync(absolutePath, "utf8");
|
|
183
191
|
const fileData = safeParseJsonFile(fileContent, absolutePath);
|
|
184
|
-
if (fileData?.headers?.[
|
|
185
|
-
fileData.headers[
|
|
192
|
+
if (fileData?.headers?.[CACHE_TAGS_HEADER]) {
|
|
193
|
+
fileData.headers[CACHE_TAGS_HEADER]
|
|
186
194
|
.split(",")
|
|
187
195
|
.forEach((tag) => {
|
|
188
196
|
// TODO: We should split the tag using getDerivedTags from next.js or maybe use an in house implementation
|
|
@@ -74,7 +74,7 @@ export async function createImageOptimizationBundle(options) {
|
|
|
74
74
|
// Sharp provides pre-build binaries for all platforms. https://github.com/lovell/sharp/blob/main/docs/install.md#cross-platform
|
|
75
75
|
// Target should be same as used by Lambda, see https://github.com/sst/sst/blob/ca6f763fdfddd099ce2260202d0ce48c72e211ea/packages/sst/src/constructs/NextjsSite.ts#L114
|
|
76
76
|
// For SHARP_IGNORE_GLOBAL_LIBVIPS see: https://github.com/lovell/sharp/blob/main/docs/install.md#aws-lambda
|
|
77
|
-
const sharpVersion = process.env.SHARP_VERSION ?? "0.
|
|
77
|
+
const sharpVersion = process.env.SHARP_VERSION ?? "0.35.3";
|
|
78
78
|
// In development, we want to use the local machine settings
|
|
79
79
|
const isDev = config.imageOptimization?.loader === "fs-dev";
|
|
80
80
|
installDependencies(outputPath, config.imageOptimization?.install ?? {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
2
2
|
import { IncomingMessage } from "../http/index.js";
|
|
3
|
+
import { ISR_HEADER } from "../utils/cacheHeaders";
|
|
3
4
|
import { runWithOpenNextRequestContext } from "../utils/promise";
|
|
4
5
|
import { NextConfig } from "../adapters/config/index";
|
|
5
6
|
import { debug, error } from "../adapters/logger";
|
|
@@ -22,7 +23,7 @@ export async function openNextHandler(internalEvent, options) {
|
|
|
22
23
|
: Math.random().toString(36);
|
|
23
24
|
// We run everything in the async local storage context so that it is available in the middleware as well as in NextServer
|
|
24
25
|
return runWithOpenNextRequestContext({
|
|
25
|
-
isISRRevalidation: initialHeaders[
|
|
26
|
+
isISRRevalidation: initialHeaders[ISR_HEADER] === "1",
|
|
26
27
|
waitUntil: options?.waitUntil,
|
|
27
28
|
requestId,
|
|
28
29
|
}, async () => {
|
|
@@ -3,7 +3,8 @@ import { NextConfig, PrerenderManifest } from "../../adapters/config/index";
|
|
|
3
3
|
import { emptyReadableStream, toReadableStream } from "../../utils/stream";
|
|
4
4
|
import { isBinaryContentType } from "../../utils/binary";
|
|
5
5
|
import { getTagsFromValue, hasBeenRevalidated, isStale } from "../../utils/cache";
|
|
6
|
-
import {
|
|
6
|
+
import { CACHE_CONTROL_HEADER, NO_STORE_CACHE_CONTROL, OPEN_NEXT_CACHE_HEADER, PRERENDER_REVALIDATE_HEADER, fixCacheControlForError, } from "../../utils/cacheHeaders";
|
|
7
|
+
import { debug } from "../../adapters/logger";
|
|
7
8
|
import { localizePath } from "./i18n";
|
|
8
9
|
import { generateMessageGroupId } from "./queue";
|
|
9
10
|
const CACHE_ONE_YEAR = 60 * 60 * 24 * 365;
|
|
@@ -41,8 +42,8 @@ async function computeCacheControl(path, body, host, revalidate, lastModified, i
|
|
|
41
42
|
if (revalidate === 0) {
|
|
42
43
|
// This one should never happen
|
|
43
44
|
return {
|
|
44
|
-
|
|
45
|
-
|
|
45
|
+
[CACHE_CONTROL_HEADER]: NO_STORE_CACHE_CONTROL,
|
|
46
|
+
[OPEN_NEXT_CACHE_HEADER]: "ERROR",
|
|
46
47
|
etag,
|
|
47
48
|
};
|
|
48
49
|
}
|
|
@@ -77,53 +78,76 @@ async function computeCacheControl(path, body, host, revalidate, lastModified, i
|
|
|
77
78
|
});
|
|
78
79
|
}
|
|
79
80
|
return {
|
|
80
|
-
|
|
81
|
-
|
|
81
|
+
[CACHE_CONTROL_HEADER]: `s-maxage=${sMaxAge}, stale-while-revalidate=${CACHE_ONE_MONTH}`,
|
|
82
|
+
[OPEN_NEXT_CACHE_HEADER]: isStale ? "STALE" : "HIT",
|
|
82
83
|
etag,
|
|
83
84
|
};
|
|
84
85
|
}
|
|
85
86
|
return {
|
|
86
|
-
|
|
87
|
-
|
|
87
|
+
[CACHE_CONTROL_HEADER]: `s-maxage=${CACHE_ONE_YEAR}, stale-while-revalidate=${CACHE_ONE_MONTH}`,
|
|
88
|
+
[OPEN_NEXT_CACHE_HEADER]: "HIT",
|
|
88
89
|
etag,
|
|
89
90
|
};
|
|
90
91
|
}
|
|
92
|
+
/**
|
|
93
|
+
* Computes the body of an RSC response from a cached app router entry.
|
|
94
|
+
*
|
|
95
|
+
* @param event The incoming event, used to read the segment prefetch header
|
|
96
|
+
* @param cachedValue The cache entry, must be of type `app`
|
|
97
|
+
* @returns The body and the headers to add to the response, or `undefined` when
|
|
98
|
+
* the entry can not serve the request - the caller should then fallback to the server.
|
|
99
|
+
* @throws When `cachedValue` is not of type `app`
|
|
100
|
+
*/
|
|
91
101
|
function getBodyForAppRouter(event, cachedValue) {
|
|
92
102
|
if (cachedValue.type !== "app") {
|
|
93
103
|
throw new Error("getBodyForAppRouter called with non-app cache value");
|
|
94
104
|
}
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
const body = isSegmentResponse
|
|
101
|
-
? cachedValue.segmentData[segmentHeader]
|
|
102
|
-
: cachedValue.rsc;
|
|
105
|
+
const segmentHeader = `${event.headers[NEXT_SEGMENT_PREFETCH_HEADER]}`;
|
|
106
|
+
const isSegmentResponse = Boolean(segmentHeader) &&
|
|
107
|
+
segmentHeader in (cachedValue.segmentData || {}) &&
|
|
108
|
+
!NextConfig.experimental?.prefetchInlining;
|
|
109
|
+
if (isSegmentResponse) {
|
|
103
110
|
return {
|
|
104
|
-
body,
|
|
105
|
-
additionalHeaders:
|
|
106
|
-
|
|
107
|
-
:
|
|
111
|
+
body: cachedValue.segmentData[segmentHeader],
|
|
112
|
+
additionalHeaders: {
|
|
113
|
+
[NEXT_PRERENDER_HEADER]: "1",
|
|
114
|
+
[NEXT_POSTPONED_HEADER]: "2",
|
|
115
|
+
},
|
|
108
116
|
};
|
|
109
117
|
}
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
118
|
+
// `rsc` is absent when the build collected neither a `.rsc` nor a `.prefetch.rsc` file for
|
|
119
|
+
// this entry - fallback shells, and postponed PPR routes on Next 16.2+, see `CachedFile`.
|
|
120
|
+
// There is nothing valid to serve, and falling back to an empty payload would break the
|
|
121
|
+
// router and let the CDN cache the empty response, so let the server generate it.
|
|
122
|
+
if (cachedValue.rsc === undefined) {
|
|
123
|
+
return undefined;
|
|
113
124
|
}
|
|
125
|
+
return { body: cachedValue.rsc, additionalHeaders: {} };
|
|
114
126
|
}
|
|
127
|
+
/**
|
|
128
|
+
* Generates the response to serve for a cached `app` or `page` entry.
|
|
129
|
+
*
|
|
130
|
+
* @param event The incoming event
|
|
131
|
+
* @param localizedPath The localized path, used to compute the cache control
|
|
132
|
+
* @param cachedValue The cache entry, must be of type `app` or `page`
|
|
133
|
+
* @param lastModified Time of the last update to the cache entry
|
|
134
|
+
* @param isStaleFromTagCache Whether the tag cache reported the entry as stale
|
|
135
|
+
* @returns The result to serve, or `undefined` when the entry can not serve the
|
|
136
|
+
* request - the caller should then fallback to the server.
|
|
137
|
+
* @throws When `cachedValue` is neither of type `app` nor `page`
|
|
138
|
+
*/
|
|
115
139
|
async function generateResult(event, localizedPath, cachedValue, lastModified, isStaleFromTagCache = false) {
|
|
116
140
|
debug("Returning result from experimental cache");
|
|
117
|
-
let body
|
|
141
|
+
let body;
|
|
118
142
|
let type = "application/octet-stream";
|
|
119
143
|
let isDataRequest = false;
|
|
120
144
|
let additionalHeaders = {};
|
|
121
145
|
if (cachedValue.type === "app") {
|
|
122
146
|
isDataRequest = event.headers.rsc === "1";
|
|
123
147
|
if (isDataRequest) {
|
|
124
|
-
const
|
|
125
|
-
body =
|
|
126
|
-
additionalHeaders =
|
|
148
|
+
const appRouterResult = getBodyForAppRouter(event, cachedValue);
|
|
149
|
+
body = appRouterResult?.body;
|
|
150
|
+
additionalHeaders = appRouterResult?.additionalHeaders ?? {};
|
|
127
151
|
}
|
|
128
152
|
else {
|
|
129
153
|
body = cachedValue.html;
|
|
@@ -138,24 +162,37 @@ async function generateResult(event, localizedPath, cachedValue, lastModified, i
|
|
|
138
162
|
else {
|
|
139
163
|
throw new Error("generateResult called with unsupported cache value type, only 'app' and 'page' are supported");
|
|
140
164
|
}
|
|
165
|
+
// Next.js does not write every file for every route at build time, so the entry might
|
|
166
|
+
// not hold the data needed to serve this particular request.
|
|
167
|
+
if (body === undefined) {
|
|
168
|
+
debug("Missing body in the cache entry, falling back to the server");
|
|
169
|
+
return undefined;
|
|
170
|
+
}
|
|
141
171
|
const cacheControl = await computeCacheControl(localizedPath, body, event.headers.host, cachedValue.revalidate, lastModified, isStaleFromTagCache);
|
|
172
|
+
// Sometimes other status codes can be cached, like 404. For these cases, we should return the correct status code
|
|
173
|
+
// Also set the status code to the rewriteStatusCode if defined
|
|
174
|
+
// This can happen in handleMiddleware in routingHandler.
|
|
175
|
+
// `NextResponse.rewrite(url, { status: xxx})
|
|
176
|
+
// The rewrite status code should take precedence over the cached one
|
|
177
|
+
const statusCode = event.rewriteStatusCode ?? cachedValue.meta?.status ?? 200;
|
|
178
|
+
const headers = {
|
|
179
|
+
...cacheControl,
|
|
180
|
+
"content-type": type,
|
|
181
|
+
...cachedValue.meta?.headers,
|
|
182
|
+
vary: VARY_HEADER,
|
|
183
|
+
...additionalHeaders,
|
|
184
|
+
};
|
|
185
|
+
// Applied last so that it wins over both the computed cache control and the one
|
|
186
|
+
// that could be stored in the entry's own headers. This is the same override the
|
|
187
|
+
// server path applies in `OpenNextNodeResponse.fixHeadersForError`, which the
|
|
188
|
+
// interceptor bypasses by returning a result directly.
|
|
189
|
+
fixCacheControlForError(headers, statusCode);
|
|
142
190
|
return {
|
|
143
191
|
type: "core",
|
|
144
|
-
|
|
145
|
-
// Also set the status code to the rewriteStatusCode if defined
|
|
146
|
-
// This can happen in handleMiddleware in routingHandler.
|
|
147
|
-
// `NextResponse.rewrite(url, { status: xxx})
|
|
148
|
-
// The rewrite status code should take precedence over the cached one
|
|
149
|
-
statusCode: event.rewriteStatusCode ?? cachedValue.meta?.status ?? 200,
|
|
192
|
+
statusCode,
|
|
150
193
|
body: toReadableStream(body, false),
|
|
151
194
|
isBase64Encoded: false,
|
|
152
|
-
headers
|
|
153
|
-
...cacheControl,
|
|
154
|
-
"content-type": type,
|
|
155
|
-
...cachedValue.meta?.headers,
|
|
156
|
-
vary: VARY_HEADER,
|
|
157
|
-
...additionalHeaders,
|
|
158
|
-
},
|
|
195
|
+
headers,
|
|
159
196
|
};
|
|
160
197
|
}
|
|
161
198
|
/**
|
|
@@ -180,7 +217,7 @@ function decodePathParams(pathname) {
|
|
|
180
217
|
}
|
|
181
218
|
export async function cacheInterceptor(event) {
|
|
182
219
|
if (Boolean(event.headers["next-action"]) ||
|
|
183
|
-
Boolean(event.headers[
|
|
220
|
+
Boolean(event.headers[PRERENDER_REVALIDATE_HEADER]))
|
|
184
221
|
return event;
|
|
185
222
|
// Check for Next.js preview mode cookies
|
|
186
223
|
const cookies = event.headers.cookie || "";
|
|
@@ -239,8 +276,11 @@ export async function cacheInterceptor(event) {
|
|
|
239
276
|
const host = event.headers.host;
|
|
240
277
|
switch (cachedData?.value?.type) {
|
|
241
278
|
case "app":
|
|
242
|
-
case "page":
|
|
243
|
-
|
|
279
|
+
case "page": {
|
|
280
|
+
const result = await generateResult(event, localizedPath, cachedData.value, cachedData.lastModified, _isStale);
|
|
281
|
+
// The cache entry can not serve this request, fallback to the server.
|
|
282
|
+
return result ?? event;
|
|
283
|
+
}
|
|
244
284
|
case "redirect": {
|
|
245
285
|
const cacheControl = await computeCacheControl(localizedPath, "", host, cachedData.value.revalidate, cachedData.lastModified, _isStale);
|
|
246
286
|
return {
|
|
@@ -258,15 +298,19 @@ export async function cacheInterceptor(event) {
|
|
|
258
298
|
case "route": {
|
|
259
299
|
const cacheControl = await computeCacheControl(localizedPath, cachedData.value.body, host, cachedData.value.revalidate, cachedData.lastModified, _isStale);
|
|
260
300
|
const isBinary = isBinaryContentType(String(cachedData.value.meta?.headers?.["content-type"]));
|
|
301
|
+
const statusCode = event.rewriteStatusCode ?? cachedData.value.meta?.status ?? 200;
|
|
302
|
+
const headers = {
|
|
303
|
+
...cacheControl,
|
|
304
|
+
...cachedData.value.meta?.headers,
|
|
305
|
+
vary: VARY_HEADER,
|
|
306
|
+
};
|
|
307
|
+
// See the note in `generateResult`.
|
|
308
|
+
fixCacheControlForError(headers, statusCode);
|
|
261
309
|
return {
|
|
262
310
|
type: "core",
|
|
263
|
-
statusCode
|
|
311
|
+
statusCode,
|
|
264
312
|
body: toReadableStream(cachedData.value.body, isBinary),
|
|
265
|
-
headers
|
|
266
|
-
...cacheControl,
|
|
267
|
-
...cachedData.value.meta?.headers,
|
|
268
|
-
vary: VARY_HEADER,
|
|
269
|
-
},
|
|
313
|
+
headers,
|
|
270
314
|
isBase64Encoded: isBinary,
|
|
271
315
|
};
|
|
272
316
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { FunctionsConfigManifest, MiddlewareManifest, NextConfig, PrerenderManifest, } from "../../adapters/config/index.js";
|
|
2
|
+
import { ISR_HEADER, PRERENDER_REVALIDATE_HEADER } from "../../utils/cacheHeaders.js";
|
|
2
3
|
import { emptyReadableStream } from "../../utils/stream.js";
|
|
3
4
|
import { getQueryFromSearchParams } from "../../overrides/converters/utils.js";
|
|
4
5
|
import { localizePath } from "./i18n/index.js";
|
|
@@ -23,8 +24,8 @@ export async function handleMiddleware(internalEvent, initialSearch, middlewareL
|
|
|
23
24
|
// We bypass the middleware if the request is internal
|
|
24
25
|
// We should only do that if the request has the correct `x-prerender-revalidate` header
|
|
25
26
|
// The `x-prerender-revalidate` header is set at build time and should be safe to trust
|
|
26
|
-
if (headers[
|
|
27
|
-
headers[
|
|
27
|
+
if (headers[ISR_HEADER] &&
|
|
28
|
+
headers[PRERENDER_REVALIDATE_HEADER] ===
|
|
28
29
|
PrerenderManifest?.preview?.previewModeId)
|
|
29
30
|
return internalEvent;
|
|
30
31
|
// We only need the normalizedPath to check if the middleware should run
|
|
@@ -11,6 +11,16 @@ import { ReadableStream } from "node:stream/web";
|
|
|
11
11
|
export declare function isExternal(url?: string, host?: string): boolean;
|
|
12
12
|
export declare function convertFromQueryString(query: string): Record<string, string | string[]>;
|
|
13
13
|
/**
|
|
14
|
+
* Splits an URL into its parts
|
|
15
|
+
*
|
|
16
|
+
* The URL is a route destination, i.e. a `path-to-regexp` pattern rather than a
|
|
17
|
+
* well formed URL, so it can not be parsed with `new URL()` which would percent
|
|
18
|
+
* encode some of the pattern characters.
|
|
19
|
+
*
|
|
20
|
+
* @param url The URL to split
|
|
21
|
+
* @param isExternal Whether the URL points to an external host
|
|
22
|
+
* @returns The protocol, hostname, pathname and query string of the URL
|
|
23
|
+
* @throws When `isExternal` is true and the URL is not an absolute HTTP(S) URL
|
|
14
24
|
*
|
|
15
25
|
* @__PURE__
|
|
16
26
|
*/
|
|
@@ -6,6 +6,7 @@ import { getQueryFromIterator, parseHeaders } from "../../http/util.js";
|
|
|
6
6
|
import { ReadableStream } from "node:stream/web";
|
|
7
7
|
import { debug, error } from "../../adapters/logger.js";
|
|
8
8
|
import { isBinaryContentType } from "../../utils/binary.js";
|
|
9
|
+
import { CACHE_CONTROL_HEADER, ISR_HEADER, NEXTJS_CACHE_HEADER, NO_STORE_CACHE_CONTROL, fixCacheControlForError, } from "../../utils/cacheHeaders.js";
|
|
9
10
|
import { localizePath } from "./i18n/index.js";
|
|
10
11
|
import { generateMessageGroupId } from "./queue.js";
|
|
11
12
|
/**
|
|
@@ -40,6 +41,16 @@ export function convertFromQueryString(query) {
|
|
|
40
41
|
}));
|
|
41
42
|
}
|
|
42
43
|
/**
|
|
44
|
+
* Splits an URL into its parts
|
|
45
|
+
*
|
|
46
|
+
* The URL is a route destination, i.e. a `path-to-regexp` pattern rather than a
|
|
47
|
+
* well formed URL, so it can not be parsed with `new URL()` which would percent
|
|
48
|
+
* encode some of the pattern characters.
|
|
49
|
+
*
|
|
50
|
+
* @param url The URL to split
|
|
51
|
+
* @param isExternal Whether the URL points to an external host
|
|
52
|
+
* @returns The protocol, hostname, pathname and query string of the URL
|
|
53
|
+
* @throws When `isExternal` is true and the URL is not an absolute HTTP(S) URL
|
|
43
54
|
*
|
|
44
55
|
* @__PURE__
|
|
45
56
|
*/
|
|
@@ -49,12 +60,12 @@ export function getUrlParts(url, isExternal) {
|
|
|
49
60
|
const match = url.match(regex);
|
|
50
61
|
return {
|
|
51
62
|
hostname: "",
|
|
52
|
-
pathname:
|
|
63
|
+
pathname: url.startsWith("/") ? `/${match?.[1] ?? ""}` : "",
|
|
53
64
|
protocol: "",
|
|
54
65
|
queryString: match?.[2] ?? "",
|
|
55
66
|
};
|
|
56
67
|
}
|
|
57
|
-
const regex = /^(https?:)\/\/?([^\/\s]+)(\/[^?]*)?(\?.*)?/;
|
|
68
|
+
const regex = /^(https?:)\/\/?([^\/\s?]+)(\/[^?]*)?(\?.*)?/;
|
|
58
69
|
const match = url.match(regex);
|
|
59
70
|
if (!match) {
|
|
60
71
|
throw new Error(`Invalid external URL: ${url}`);
|
|
@@ -195,11 +206,6 @@ export function convertBodyToReadableStream(method, body) {
|
|
|
195
206
|
},
|
|
196
207
|
});
|
|
197
208
|
}
|
|
198
|
-
var CommonHeaders;
|
|
199
|
-
(function (CommonHeaders) {
|
|
200
|
-
CommonHeaders["CACHE_CONTROL"] = "cache-control";
|
|
201
|
-
CommonHeaders["NEXT_CACHE"] = "x-nextjs-cache";
|
|
202
|
-
})(CommonHeaders || (CommonHeaders = {}));
|
|
203
209
|
/**
|
|
204
210
|
*
|
|
205
211
|
* @__PURE__
|
|
@@ -207,11 +213,7 @@ var CommonHeaders;
|
|
|
207
213
|
export function fixCacheHeaderForHtmlPages(internalEvent, headers) {
|
|
208
214
|
// We don't want to cache error pages
|
|
209
215
|
if (internalEvent.rawPath === "/404" || internalEvent.rawPath === "/500") {
|
|
210
|
-
|
|
211
|
-
return;
|
|
212
|
-
}
|
|
213
|
-
headers[CommonHeaders.CACHE_CONTROL] =
|
|
214
|
-
"private, no-cache, no-store, max-age=0, must-revalidate";
|
|
216
|
+
fixCacheControlForError(headers, internalEvent.rawPath === "/404" ? 404 : 500);
|
|
215
217
|
return;
|
|
216
218
|
}
|
|
217
219
|
const localizedPath = localizePath(internalEvent);
|
|
@@ -220,7 +222,7 @@ export function fixCacheHeaderForHtmlPages(internalEvent, headers) {
|
|
|
220
222
|
// Requests containing an `x-middleware-prefetch` header must not be cached
|
|
221
223
|
if (HtmlPages.includes(localizedPath) &&
|
|
222
224
|
!internalEvent.headers["x-middleware-prefetch"]) {
|
|
223
|
-
headers[
|
|
225
|
+
headers[CACHE_CONTROL_HEADER] =
|
|
224
226
|
"public, max-age=0, s-maxage=31536000, must-revalidate";
|
|
225
227
|
}
|
|
226
228
|
}
|
|
@@ -230,7 +232,7 @@ export function fixCacheHeaderForHtmlPages(internalEvent, headers) {
|
|
|
230
232
|
*/
|
|
231
233
|
export function fixSWRCacheHeader(headers) {
|
|
232
234
|
// WORKAROUND: `NextServer` does not set correct SWR cache headers — https://github.com/sst/open-next#workaround-nextserver-does-not-set-correct-swr-cache-headers
|
|
233
|
-
let cacheControl = headers[
|
|
235
|
+
let cacheControl = headers[CACHE_CONTROL_HEADER];
|
|
234
236
|
if (!cacheControl)
|
|
235
237
|
return;
|
|
236
238
|
if (Array.isArray(cacheControl)) {
|
|
@@ -238,7 +240,7 @@ export function fixSWRCacheHeader(headers) {
|
|
|
238
240
|
}
|
|
239
241
|
if (typeof cacheControl !== "string")
|
|
240
242
|
return;
|
|
241
|
-
headers[
|
|
243
|
+
headers[CACHE_CONTROL_HEADER] = cacheControl.replace(/\bstale-while-revalidate(?!=)/, "stale-while-revalidate=2592000");
|
|
242
244
|
}
|
|
243
245
|
/**
|
|
244
246
|
*
|
|
@@ -261,7 +263,7 @@ export function addOpenNextHeader(headers) {
|
|
|
261
263
|
* @__PURE__
|
|
262
264
|
*/
|
|
263
265
|
export async function revalidateIfRequired(host, rawPath, headers, req) {
|
|
264
|
-
if (headers[
|
|
266
|
+
if (headers[NEXTJS_CACHE_HEADER] === "STALE") {
|
|
265
267
|
// If the URL is rewritten, revalidation needs to be done on the rewritten URL.
|
|
266
268
|
// - Link to Next.js doc: https://nextjs.org/docs/pages/building-your-application/data-fetching/incremental-static-regeneration#on-demand-revalidation
|
|
267
269
|
// - Link to NextInternalRequestMeta: https://github.com/vercel/next.js/blob/57ab2818b93627e91c937a130fb56a36c41629c3/packages/next/src/server/request-meta.ts#L11
|
|
@@ -305,37 +307,35 @@ export async function revalidateIfRequired(host, rawPath, headers, req) {
|
|
|
305
307
|
*/
|
|
306
308
|
export function fixISRHeaders(headers) {
|
|
307
309
|
const sMaxAgeRegex = /s-maxage=(\d+)/;
|
|
308
|
-
const match = headers[
|
|
310
|
+
const match = headers[CACHE_CONTROL_HEADER]?.match(sMaxAgeRegex);
|
|
309
311
|
const sMaxAge = match ? Number.parseInt(match[1]) : undefined;
|
|
310
312
|
// We only apply the fix if the cache-control header contains s-maxage
|
|
311
313
|
if (!sMaxAge) {
|
|
312
314
|
return;
|
|
313
315
|
}
|
|
314
|
-
if (headers[
|
|
315
|
-
headers[
|
|
316
|
-
"private, no-cache, no-store, max-age=0, must-revalidate";
|
|
316
|
+
if (headers[NEXTJS_CACHE_HEADER] === "REVALIDATED") {
|
|
317
|
+
headers[CACHE_CONTROL_HEADER] = NO_STORE_CACHE_CONTROL;
|
|
317
318
|
return;
|
|
318
319
|
}
|
|
319
320
|
const _lastModified = globalThis.__openNextAls.getStore()?.lastModified ?? 0;
|
|
320
|
-
if (headers[
|
|
321
|
-
debug("cache-control", headers[
|
|
321
|
+
if (headers[NEXTJS_CACHE_HEADER] === "HIT" && _lastModified > 0) {
|
|
322
|
+
debug("cache-control", headers[CACHE_CONTROL_HEADER], _lastModified, Date.now());
|
|
322
323
|
// 31536000 is the default s-maxage value for SSG pages
|
|
323
324
|
if (sMaxAge && sMaxAge !== 31536000) {
|
|
324
325
|
// calculate age
|
|
325
326
|
const age = Math.round((Date.now() - _lastModified) / 1000);
|
|
326
327
|
const remainingTtl = Math.max(sMaxAge - age, 1);
|
|
327
|
-
headers[
|
|
328
|
+
headers[CACHE_CONTROL_HEADER] =
|
|
328
329
|
`s-maxage=${remainingTtl}, stale-while-revalidate=2592000`;
|
|
329
330
|
}
|
|
330
331
|
}
|
|
331
|
-
if (headers[
|
|
332
|
+
if (headers[NEXTJS_CACHE_HEADER] !== "STALE")
|
|
332
333
|
return;
|
|
333
334
|
// If the cache is stale, we revalidate in the background
|
|
334
335
|
// In order for CloudFront SWR to work, we set the stale-while-revalidate value to 2 seconds
|
|
335
336
|
// This will cause CloudFront to cache the stale data for a short period of time while we revalidate in the background
|
|
336
337
|
// Once the revalidation is complete, CloudFront will serve the fresh data
|
|
337
|
-
headers[
|
|
338
|
-
"s-maxage=2, stale-while-revalidate=2592000";
|
|
338
|
+
headers[CACHE_CONTROL_HEADER] = "s-maxage=2, stale-while-revalidate=2592000";
|
|
339
339
|
}
|
|
340
340
|
/**
|
|
341
341
|
*
|
|
@@ -361,9 +361,8 @@ export function createServerResponse(routingResult, headers, responseStream) {
|
|
|
361
361
|
export async function invalidateCDNOnRequest(params, headers) {
|
|
362
362
|
const { internalEvent, resolvedRoutes, initialURL } = params;
|
|
363
363
|
const initialPath = new URL(initialURL).pathname;
|
|
364
|
-
const isIsrRevalidation = internalEvent.headers[
|
|
365
|
-
if (!isIsrRevalidation &&
|
|
366
|
-
headers[CommonHeaders.NEXT_CACHE] === "REVALIDATED") {
|
|
364
|
+
const isIsrRevalidation = internalEvent.headers[ISR_HEADER] === "1";
|
|
365
|
+
if (!isIsrRevalidation && headers[NEXTJS_CACHE_HEADER] === "REVALIDATED") {
|
|
367
366
|
await globalThis.cdnInvalidationHandler.invalidatePaths([
|
|
368
367
|
{
|
|
369
368
|
initialPath,
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { BuildId, ConfigHeaders, NextConfig, PrerenderManifest, RoutesManifest, } from "../adapters/config/index";
|
|
2
2
|
import { debug, error } from "../adapters/logger";
|
|
3
|
+
import { NO_STORE_CACHE_CONTROL } from "../utils/cacheHeaders";
|
|
3
4
|
import { cacheInterceptor } from "./routing/cacheInterceptor";
|
|
4
5
|
import { detectLocale } from "./routing/i18n";
|
|
5
6
|
import { fixDataPage, getNextConfigHeaders, handleFallbackFalse, handleRedirects, handleRewrites, } from "./routing/matcher";
|
|
@@ -169,7 +170,7 @@ export default async function routingHandler(event, { assetResolver }) {
|
|
|
169
170
|
url: constructNextUrl(eventOrResult.url, "/404"),
|
|
170
171
|
headers: {
|
|
171
172
|
...eventOrResult.headers,
|
|
172
|
-
"x-middleware-response-cache-control":
|
|
173
|
+
"x-middleware-response-cache-control": NO_STORE_CACHE_CONTROL,
|
|
173
174
|
},
|
|
174
175
|
};
|
|
175
176
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { Transform } from "node:stream";
|
|
2
2
|
import { debug } from "../adapters/logger";
|
|
3
|
+
import { fixCacheControlForError } from "../utils/cacheHeaders";
|
|
3
4
|
import { parseHeaders, parseSetCookieHeader } from "./util";
|
|
4
5
|
const SET_COOKIE_HEADER = "set-cookie";
|
|
5
6
|
const CANNOT_BE_USED = "This cannot be used in OpenNext";
|
|
@@ -327,16 +328,8 @@ export class OpenNextNodeResponse extends Transform {
|
|
|
327
328
|
// For some reason, next returns the 500 error page with some cache-control headers
|
|
328
329
|
// We need to fix that
|
|
329
330
|
fixHeadersForError() {
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
// We only check for 404 and 500 errors
|
|
334
|
-
// The rest should be errors that are handled by the user and they should set the cache headers themselves
|
|
335
|
-
if (this.statusCode === 404 || this.statusCode === 500) {
|
|
336
|
-
// For some reason calling this.setHeader("Cache-Control", "no-cache, no-store, must-revalidate") does not work here
|
|
337
|
-
// The function is not even called, i'm probably missing something obvious
|
|
338
|
-
this.headers["cache-control"] =
|
|
339
|
-
"private, no-cache, no-store, max-age=0, must-revalidate";
|
|
340
|
-
}
|
|
331
|
+
// NOTE: this assigns to `this.headers` directly rather than going through
|
|
332
|
+
// `this.setHeader()`, because for some reason `setHeader` is not even called from here.
|
|
333
|
+
fixCacheControlForError(this.headers, this.statusCode);
|
|
341
334
|
}
|
|
342
335
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { ISR_HEADER, PRERENDER_REVALIDATE_HEADER } from "../../utils/cacheHeaders.js";
|
|
1
2
|
const queue = {
|
|
2
3
|
name: "dev-queue",
|
|
3
4
|
send: async (message) => {
|
|
@@ -9,8 +10,8 @@ const queue = {
|
|
|
9
10
|
await globalThis.internalFetch(`${protocol}://${host}${url}`, {
|
|
10
11
|
method: "HEAD",
|
|
11
12
|
headers: {
|
|
12
|
-
|
|
13
|
-
|
|
13
|
+
[PRERENDER_REVALIDATE_HEADER]: revalidateId,
|
|
14
|
+
[ISR_HEADER]: "1",
|
|
14
15
|
},
|
|
15
16
|
});
|
|
16
17
|
},
|
package/dist/types/cache.d.ts
CHANGED
|
@@ -40,7 +40,7 @@ interface IncrementalCachedPageValue {
|
|
|
40
40
|
interface IncrementalCachedAppPageValue {
|
|
41
41
|
kind: "APP_PAGE";
|
|
42
42
|
html: string;
|
|
43
|
-
rscData: Buffer;
|
|
43
|
+
rscData: Buffer | undefined;
|
|
44
44
|
headers?: Record<string, undefined | string | string[]>;
|
|
45
45
|
postponed?: string;
|
|
46
46
|
status?: number;
|
|
@@ -34,13 +34,35 @@ export type CachedFile = {
|
|
|
34
34
|
meta?: Meta;
|
|
35
35
|
} | {
|
|
36
36
|
type: "page";
|
|
37
|
-
html
|
|
37
|
+
/** Absent when the build did not collect an `.html` file for this entry. */
|
|
38
|
+
html?: string;
|
|
38
39
|
json: Object;
|
|
39
40
|
meta?: Meta;
|
|
40
41
|
} | {
|
|
41
42
|
type: "app";
|
|
42
|
-
|
|
43
|
-
|
|
43
|
+
/**
|
|
44
|
+
* The prerendered HTML.
|
|
45
|
+
*
|
|
46
|
+
* Absent when the build did not collect an `.html` file for this entry. Note that
|
|
47
|
+
* Next.js writes the `.html` unconditionally when it prerenders an app page, so this
|
|
48
|
+
* is only expected to be absent for a malformed build output. A PPR route with an
|
|
49
|
+
* empty static shell is *not* one of those cases: the file is written, empty.
|
|
50
|
+
*/
|
|
51
|
+
html?: string;
|
|
52
|
+
/**
|
|
53
|
+
* The RSC payload.
|
|
54
|
+
*
|
|
55
|
+
* Both `<page>.rsc` and `<page>.prefetch.rsc` are stored here, see `createAssets`.
|
|
56
|
+
*
|
|
57
|
+
* Absent when the build emitted neither file, which depends on the Next.js version:
|
|
58
|
+
* - `15.x`: only for fallback shells. A PPR route emits a `.prefetch.rsc` in place of
|
|
59
|
+
* its `.rsc`, so the field is set but holds the *prefetch* payload, which this type
|
|
60
|
+
* can not distinguish from a full one.
|
|
61
|
+
* - `>= 16.2`: `.prefetch.rsc` no longer exists and a PPR route only emits a `.rsc`
|
|
62
|
+
* when it is fully static, so the field is absent for postponed PPR routes and for
|
|
63
|
+
* fallback shells, and holds a full payload otherwise.
|
|
64
|
+
*/
|
|
65
|
+
rsc?: string;
|
|
44
66
|
meta?: Meta;
|
|
45
67
|
segmentData?: Record<string, string>;
|
|
46
68
|
} | {
|
package/dist/utils/cache.d.ts
CHANGED
|
@@ -9,6 +9,21 @@ import type { CacheValue, NextModeTagCacheWriteInput, OriginalTagCacheWriteInput
|
|
|
9
9
|
* In this case, the cache entry is still valid and can be served, but it should trigger a background revalidation to update the cache.
|
|
10
10
|
*/
|
|
11
11
|
export declare function isStale(key: string, tags: string[], lastModified?: number): Promise<boolean>;
|
|
12
|
+
/**
|
|
13
|
+
* Next.js has no explicit way for a cache handler to report an entry as stale,
|
|
14
|
+
* the only lever we have is the `lastModified` we hand back to the incremental cache.
|
|
15
|
+
*
|
|
16
|
+
* Up to Next 16.2 we return `1` (i.e. right after the epoch), which is enough for Next to
|
|
17
|
+
* compute `revalidateAfter` in the past and mark the entry as stale.
|
|
18
|
+
* Starting with Next 16.3 the incremental cache also compares `lastModified + expire` to now, and
|
|
19
|
+
* forces a blocking revalidation (`isStale === -1`, surfacing as `x-nextjs-cache: REVALIDATED`)
|
|
20
|
+
* when that is in the past. An epoch based value always trips that check, so from that version on
|
|
21
|
+
* we go back just far enough for the entry to be stale while remaining inside its expire window.
|
|
22
|
+
*
|
|
23
|
+
* @param revalidate The revalidate value stored alongside the cache entry, in seconds
|
|
24
|
+
* @returns The `lastModified` to report to Next.js for a stale entry
|
|
25
|
+
*/
|
|
26
|
+
export declare function getStaleLastModified(revalidate?: number | false): number;
|
|
12
27
|
/**
|
|
13
28
|
* @param key The key for that specific cache entry
|
|
14
29
|
* @param tags Array of tags associated with that cache entry
|
package/dist/utils/cache.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { debug } from "../adapters/logger";
|
|
2
|
+
import { CACHE_TAGS_HEADER } from "./cacheHeaders";
|
|
2
3
|
import { compareSemver } from "./semver";
|
|
3
4
|
/**
|
|
4
5
|
*
|
|
@@ -24,6 +25,34 @@ export async function isStale(key, tags, lastModified) {
|
|
|
24
25
|
}
|
|
25
26
|
return (await globalThis.tagCache.isStale?.(key, lastModified)) ?? false;
|
|
26
27
|
}
|
|
28
|
+
/**
|
|
29
|
+
* Next.js has no explicit way for a cache handler to report an entry as stale,
|
|
30
|
+
* the only lever we have is the `lastModified` we hand back to the incremental cache.
|
|
31
|
+
*
|
|
32
|
+
* Up to Next 16.2 we return `1` (i.e. right after the epoch), which is enough for Next to
|
|
33
|
+
* compute `revalidateAfter` in the past and mark the entry as stale.
|
|
34
|
+
* Starting with Next 16.3 the incremental cache also compares `lastModified + expire` to now, and
|
|
35
|
+
* forces a blocking revalidation (`isStale === -1`, surfacing as `x-nextjs-cache: REVALIDATED`)
|
|
36
|
+
* when that is in the past. An epoch based value always trips that check, so from that version on
|
|
37
|
+
* we go back just far enough for the entry to be stale while remaining inside its expire window.
|
|
38
|
+
*
|
|
39
|
+
* @param revalidate The revalidate value stored alongside the cache entry, in seconds
|
|
40
|
+
* @returns The `lastModified` to report to Next.js for a stale entry
|
|
41
|
+
*/
|
|
42
|
+
export function getStaleLastModified(revalidate) {
|
|
43
|
+
// The `expire` check only exists from Next 16.3, before that the sentinel is what Next expects.
|
|
44
|
+
if (!compareSemver(globalThis.nextVersion, ">=", "16.3.0")) {
|
|
45
|
+
return 1;
|
|
46
|
+
}
|
|
47
|
+
if (typeof revalidate !== "number") {
|
|
48
|
+
// Without a revalidate value Next cannot derive an expire time either,
|
|
49
|
+
// so the historical sentinel is still safe here.
|
|
50
|
+
return 1;
|
|
51
|
+
}
|
|
52
|
+
// 1ms past the revalidate window. `expire` is always >= `revalidate`, so the entry is
|
|
53
|
+
// stale without being expired (when both are equal, being expired is the correct outcome).
|
|
54
|
+
return Date.now() - revalidate * 1000 - 1;
|
|
55
|
+
}
|
|
27
56
|
/**
|
|
28
57
|
* @param key The key for that specific cache entry
|
|
29
58
|
* @param tags Array of tags associated with that cache entry
|
|
@@ -62,8 +91,8 @@ export function getTagsFromValue(value) {
|
|
|
62
91
|
}
|
|
63
92
|
// The try catch is necessary for older version of next.js that may fail on this
|
|
64
93
|
try {
|
|
65
|
-
const cacheTags = value.meta?.headers?.[
|
|
66
|
-
delete value.meta?.headers?.[
|
|
94
|
+
const cacheTags = value.meta?.headers?.[CACHE_TAGS_HEADER]?.split(",") ?? [];
|
|
95
|
+
delete value.meta?.headers?.[CACHE_TAGS_HEADER];
|
|
67
96
|
return cacheTags;
|
|
68
97
|
}
|
|
69
98
|
catch (e) {
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import type { OutgoingHttpHeaders } from "node:http";
|
|
2
|
+
/**
|
|
3
|
+
* The name of the `cache-control` response header.
|
|
4
|
+
*/
|
|
5
|
+
export declare const CACHE_CONTROL_HEADER = "cache-control";
|
|
6
|
+
/**
|
|
7
|
+
* The name of the header Next.js uses to report the cache status of a response.
|
|
8
|
+
*/
|
|
9
|
+
export declare const NEXTJS_CACHE_HEADER = "x-nextjs-cache";
|
|
10
|
+
/**
|
|
11
|
+
* The name of the header OpenNext uses to report the cache status of a response
|
|
12
|
+
* served by the cache interceptor.
|
|
13
|
+
*/
|
|
14
|
+
export declare const OPEN_NEXT_CACHE_HEADER = "x-opennext-cache";
|
|
15
|
+
/**
|
|
16
|
+
* The name of the header Next.js uses to store the tags of a cache entry.
|
|
17
|
+
*
|
|
18
|
+
* Note that `types/cache.ts` repeats the literal, on purpose: it is a types only
|
|
19
|
+
* module and importing a value into it would give it a runtime dependency.
|
|
20
|
+
*/
|
|
21
|
+
export declare const CACHE_TAGS_HEADER = "x-next-cache-tags";
|
|
22
|
+
/**
|
|
23
|
+
* The name of the header OpenNext uses to mark a request as an ISR revalidation.
|
|
24
|
+
*/
|
|
25
|
+
export declare const ISR_HEADER = "x-isr";
|
|
26
|
+
/**
|
|
27
|
+
* The name of the header Next.js uses to authorize a revalidation request.
|
|
28
|
+
*
|
|
29
|
+
* Carries the preview mode id from the prerender manifest.
|
|
30
|
+
*/
|
|
31
|
+
export declare const PRERENDER_REVALIDATE_HEADER = "x-prerender-revalidate";
|
|
32
|
+
/**
|
|
33
|
+
* The `cache-control` OpenNext forces on error responses so that they are never cached.
|
|
34
|
+
*/
|
|
35
|
+
export declare const NO_STORE_CACHE_CONTROL = "private, no-cache, no-store, max-age=0, must-revalidate";
|
|
36
|
+
/**
|
|
37
|
+
* Overrides `cache-control` on error responses so that they are not cached.
|
|
38
|
+
*
|
|
39
|
+
* Next.js attaches cacheable `cache-control` headers to some error responses, and it writes
|
|
40
|
+
* `notFound()` results for ISR routes to the incremental cache. Without this the CDN would hold
|
|
41
|
+
* on to a 404 or a 500 - a transient one, such as a row that is not replicated yet, would stay
|
|
42
|
+
* at the edge for as long as the route's revalidate period, up to a year for an SSG route.
|
|
43
|
+
*
|
|
44
|
+
* Only 404 and 500 are overridden. Any other error status is assumed to be produced by the
|
|
45
|
+
* application, which is then responsible for setting its own cache headers.
|
|
46
|
+
*
|
|
47
|
+
* Set `OPEN_NEXT_DANGEROUSLY_SET_ERROR_HEADERS=true` to opt out and keep the original headers.
|
|
48
|
+
*
|
|
49
|
+
* @param headers The response headers, mutated in place
|
|
50
|
+
* @param statusCode The status code of the response
|
|
51
|
+
*/
|
|
52
|
+
export declare function fixCacheControlForError(headers: OutgoingHttpHeaders | Record<string, string | string[]>, statusCode: number): void;
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The name of the `cache-control` response header.
|
|
3
|
+
*/
|
|
4
|
+
export const CACHE_CONTROL_HEADER = "cache-control";
|
|
5
|
+
/**
|
|
6
|
+
* The name of the header Next.js uses to report the cache status of a response.
|
|
7
|
+
*/
|
|
8
|
+
export const NEXTJS_CACHE_HEADER = "x-nextjs-cache";
|
|
9
|
+
/**
|
|
10
|
+
* The name of the header OpenNext uses to report the cache status of a response
|
|
11
|
+
* served by the cache interceptor.
|
|
12
|
+
*/
|
|
13
|
+
export const OPEN_NEXT_CACHE_HEADER = "x-opennext-cache";
|
|
14
|
+
/**
|
|
15
|
+
* The name of the header Next.js uses to store the tags of a cache entry.
|
|
16
|
+
*
|
|
17
|
+
* Note that `types/cache.ts` repeats the literal, on purpose: it is a types only
|
|
18
|
+
* module and importing a value into it would give it a runtime dependency.
|
|
19
|
+
*/
|
|
20
|
+
export const CACHE_TAGS_HEADER = "x-next-cache-tags";
|
|
21
|
+
/**
|
|
22
|
+
* The name of the header OpenNext uses to mark a request as an ISR revalidation.
|
|
23
|
+
*/
|
|
24
|
+
export const ISR_HEADER = "x-isr";
|
|
25
|
+
/**
|
|
26
|
+
* The name of the header Next.js uses to authorize a revalidation request.
|
|
27
|
+
*
|
|
28
|
+
* Carries the preview mode id from the prerender manifest.
|
|
29
|
+
*/
|
|
30
|
+
export const PRERENDER_REVALIDATE_HEADER = "x-prerender-revalidate";
|
|
31
|
+
/**
|
|
32
|
+
* The `cache-control` OpenNext forces on error responses so that they are never cached.
|
|
33
|
+
*/
|
|
34
|
+
export const NO_STORE_CACHE_CONTROL = "private, no-cache, no-store, max-age=0, must-revalidate";
|
|
35
|
+
/**
|
|
36
|
+
* Overrides `cache-control` on error responses so that they are not cached.
|
|
37
|
+
*
|
|
38
|
+
* Next.js attaches cacheable `cache-control` headers to some error responses, and it writes
|
|
39
|
+
* `notFound()` results for ISR routes to the incremental cache. Without this the CDN would hold
|
|
40
|
+
* on to a 404 or a 500 - a transient one, such as a row that is not replicated yet, would stay
|
|
41
|
+
* at the edge for as long as the route's revalidate period, up to a year for an SSG route.
|
|
42
|
+
*
|
|
43
|
+
* Only 404 and 500 are overridden. Any other error status is assumed to be produced by the
|
|
44
|
+
* application, which is then responsible for setting its own cache headers.
|
|
45
|
+
*
|
|
46
|
+
* Set `OPEN_NEXT_DANGEROUSLY_SET_ERROR_HEADERS=true` to opt out and keep the original headers.
|
|
47
|
+
*
|
|
48
|
+
* @param headers The response headers, mutated in place
|
|
49
|
+
* @param statusCode The status code of the response
|
|
50
|
+
*/
|
|
51
|
+
export function fixCacheControlForError(headers, statusCode) {
|
|
52
|
+
if (process.env.OPEN_NEXT_DANGEROUSLY_SET_ERROR_HEADERS === "true") {
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
if (statusCode === 404 || statusCode === 500) {
|
|
56
|
+
headers[CACHE_CONTROL_HEADER] = NO_STORE_CACHE_CONTROL;
|
|
57
|
+
}
|
|
58
|
+
}
|
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.2",
|
|
7
7
|
"bin": {
|
|
8
8
|
"open-next": "./dist/index.js"
|
|
9
9
|
},
|
|
@@ -56,11 +56,12 @@
|
|
|
56
56
|
"@types/express": "5.0.0",
|
|
57
57
|
"@types/node": "20.17.6",
|
|
58
58
|
"concurrently": "^9.2.1",
|
|
59
|
+
"next": "16.3.3",
|
|
59
60
|
"tsc-alias": "^1.8.16",
|
|
60
61
|
"typescript": "5.9.3"
|
|
61
62
|
},
|
|
62
63
|
"peerDependencies": {
|
|
63
|
-
"next": ">=15.5.
|
|
64
|
+
"next": ">=15.5.24 <16 || >=16.3.3"
|
|
64
65
|
},
|
|
65
66
|
"bugs": {
|
|
66
67
|
"url": "https://github.com/opennextjs/opennextjs-aws/issues"
|