@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.
@@ -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
- _lastModified = store.lastModified;
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
- rscData: Buffer.from(cacheData.rsc),
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: cacheData.type === "page" ? cacheData.json : cacheData.rsc,
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.toString("utf8"),
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
- : data?.kind === "PAGE"
415
- ? (data.headers?.["x-next-cache-tags"]?.split(",") ?? [])
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["x-isr"] === "1",
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, false, maximumResponseBody);
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, false);
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
- "x-prerender-revalidate": prerenderManifest.preview.previewModeId,
27
- "x-isr": "1",
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["x-nextjs-cache"] !== "REVALIDATED") {
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?.["x-next-cache-tags"]) {
185
- fileData.headers["x-next-cache-tags"]
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.32.6";
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["x-isr"] === "1",
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 { debug, error } from "../../adapters/logger";
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
- "cache-control": "private, no-cache, no-store, max-age=0, must-revalidate",
45
- "x-opennext-cache": "ERROR",
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
- "cache-control": `s-maxage=${sMaxAge}, stale-while-revalidate=${CACHE_ONE_MONTH}`,
81
- "x-opennext-cache": isStale ? "STALE" : "HIT",
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
- "cache-control": `s-maxage=${CACHE_ONE_YEAR}, stale-while-revalidate=${CACHE_ONE_MONTH}`,
87
- "x-opennext-cache": "HIT",
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
- try {
96
- const segmentHeader = `${event.headers[NEXT_SEGMENT_PREFETCH_HEADER]}`;
97
- const isSegmentResponse = Boolean(segmentHeader) &&
98
- segmentHeader in (cachedValue.segmentData || {}) &&
99
- !NextConfig.experimental?.prefetchInlining;
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: isSegmentResponse
106
- ? { [NEXT_PRERENDER_HEADER]: "1", [NEXT_POSTPONED_HEADER]: "2" }
107
- : {},
111
+ body: cachedValue.segmentData[segmentHeader],
112
+ additionalHeaders: {
113
+ [NEXT_PRERENDER_HEADER]: "1",
114
+ [NEXT_POSTPONED_HEADER]: "2",
115
+ },
108
116
  };
109
117
  }
110
- catch (e) {
111
- error("Error while getting body for app router from cache:", e);
112
- return { body: cachedValue.rsc, additionalHeaders: {} };
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 { body: appRouterBody, additionalHeaders: appHeaders } = getBodyForAppRouter(event, cachedValue);
125
- body = appRouterBody;
126
- additionalHeaders = appHeaders;
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
- // Sometimes other status codes can be cached, like 404. For these cases, we should return the correct status code
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["x-prerender-revalidate"]))
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
- return generateResult(event, localizedPath, cachedData.value, cachedData.lastModified, _isStale);
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: event.rewriteStatusCode ?? cachedData.value.meta?.status ?? 200,
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["x-isr"] &&
27
- headers["x-prerender-revalidate"] ===
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: match?.[1] ? `/${match[1]}` : url,
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
- if (process.env.OPEN_NEXT_DANGEROUSLY_SET_ERROR_HEADERS === "true") {
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[CommonHeaders.CACHE_CONTROL] =
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[CommonHeaders.CACHE_CONTROL];
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[CommonHeaders.CACHE_CONTROL] = cacheControl.replace(/\bstale-while-revalidate(?!=)/, "stale-while-revalidate=2592000");
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[CommonHeaders.NEXT_CACHE] === "STALE") {
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[CommonHeaders.CACHE_CONTROL]?.match(sMaxAgeRegex);
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[CommonHeaders.NEXT_CACHE] === "REVALIDATED") {
315
- headers[CommonHeaders.CACHE_CONTROL] =
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[CommonHeaders.NEXT_CACHE] === "HIT" && _lastModified > 0) {
321
- debug("cache-control", headers[CommonHeaders.CACHE_CONTROL], _lastModified, Date.now());
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[CommonHeaders.CACHE_CONTROL] =
328
+ headers[CACHE_CONTROL_HEADER] =
328
329
  `s-maxage=${remainingTtl}, stale-while-revalidate=2592000`;
329
330
  }
330
331
  }
331
- if (headers[CommonHeaders.NEXT_CACHE] !== "STALE")
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[CommonHeaders.CACHE_CONTROL] =
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["x-isr"] === "1";
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": "private, no-cache, no-store, max-age=0, must-revalidate",
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
- if (process.env.OPEN_NEXT_DANGEROUSLY_SET_ERROR_HEADERS === "true") {
331
- return;
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
- "x-prerender-revalidate": revalidateId,
13
- "x-isr": "1",
13
+ [PRERENDER_REVALIDATE_HEADER]: revalidateId,
14
+ [ISR_HEADER]: "1",
14
15
  },
15
16
  });
16
17
  },
@@ -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: string;
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
- html: string;
43
- rsc: string;
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
  } | {
@@ -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
@@ -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?.["x-next-cache-tags"]?.split(",") ?? [];
66
- delete value.meta?.headers?.["x-next-cache-tags"];
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.0",
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.21 <16 || >=16.2.11"
64
+ "next": ">=15.5.24 <16 || >=16.3.3"
64
65
  },
65
66
  "bugs": {
66
67
  "url": "https://github.com/opennextjs/opennextjs-aws/issues"