@opennextjs/aws 4.1.1 → 4.1.3

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
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_/";
@@ -440,7 +441,7 @@ export default class Cache {
440
441
  // is shared with headers that can legitimately repeat, so we narrow on `typeof` here.
441
442
  // Next.js guards the same way when it reads the header back.
442
443
  const pageCacheTags = data?.kind === "PAGE" || data?.kind === "APP_PAGE"
443
- ? data.headers?.["x-next-cache-tags"]
444
+ ? data.headers?.[CACHE_TAGS_HEADER]
444
445
  : undefined;
445
446
  const derivedTags = data?.kind === "FETCH"
446
447
  ? //@ts-expect-error - On older versions of next, ctx was a number, but for these cases we use data?.data?.tags
@@ -28,15 +28,15 @@ const defaultHandler = async (internalEvent, options) => {
28
28
  });
29
29
  const responseHeaders = {};
30
30
  response.headers.forEach((value, key) => {
31
- if (key.toLowerCase() === "set-cookie") {
32
- responseHeaders[key] = responseHeaders[key]
33
- ? [...responseHeaders[key], value]
34
- : [value];
35
- }
36
- else {
37
- responseHeaders[key] = value;
38
- }
31
+ // Headers.forEach folds same-name headers; use getSetCookie() below instead.
32
+ if (key.toLowerCase() === "set-cookie")
33
+ return;
34
+ responseHeaders[key] = value;
39
35
  });
36
+ const setCookies = response.headers.getSetCookie();
37
+ if (setCookies.length > 0) {
38
+ responseHeaders["set-cookie"] = setCookies;
39
+ }
40
40
  const body = response.body ?? emptyReadableStream();
41
41
  return {
42
42
  type: "core",
@@ -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);
@@ -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
@@ -188,8 +189,8 @@ export function createCacheAssets(options) {
188
189
  buildHelper.traverseFiles(sourceDir, ({ absolutePath, relativePath }) => absolutePath.endsWith(".meta") && !isFileSkipped(relativePath), ({ absolutePath, relativePath }) => {
189
190
  const fileContent = fs.readFileSync(absolutePath, "utf8");
190
191
  const fileData = safeParseJsonFile(fileContent, absolutePath);
191
- if (fileData?.headers?.["x-next-cache-tags"]) {
192
- fileData.headers["x-next-cache-tags"]
192
+ if (fileData?.headers?.[CACHE_TAGS_HEADER]) {
193
+ fileData.headers[CACHE_TAGS_HEADER]
193
194
  .split(",")
194
195
  .forEach((tag) => {
195
196
  // TODO: We should split the tag using getDerivedTags from next.js or maybe use an in house implementation
@@ -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,6 +3,7 @@ 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 { CACHE_CONTROL_HEADER, NO_STORE_CACHE_CONTROL, OPEN_NEXT_CACHE_HEADER, PRERENDER_REVALIDATE_HEADER, fixCacheControlForError, } from "../../utils/cacheHeaders";
6
7
  import { debug } from "../../adapters/logger";
7
8
  import { localizePath } from "./i18n";
8
9
  import { generateMessageGroupId } from "./queue";
@@ -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,14 +78,14 @@ 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
  }
@@ -168,25 +169,47 @@ async function generateResult(event, localizedPath, cachedValue, lastModified, i
168
169
  return undefined;
169
170
  }
170
171
  const cacheControl = await computeCacheControl(localizedPath, body, event.headers.host, cachedValue.revalidate, lastModified, isStaleFromTagCache);
172
+ const statusCode = computeStatusCode(event.rewriteStatusCode, cachedValue.meta?.status);
173
+ const headers = {
174
+ ...cacheControl,
175
+ "content-type": type,
176
+ ...cachedValue.meta?.headers,
177
+ vary: VARY_HEADER,
178
+ ...additionalHeaders,
179
+ };
180
+ // Applied last so that it wins over both the computed cache control and the one
181
+ // that could be stored in the entry's own headers. This is the same override the
182
+ // server path applies in `OpenNextNodeResponse.fixHeadersForError`, which the
183
+ // interceptor bypasses by returning a result directly.
184
+ fixCacheControlForError(headers, statusCode);
171
185
  return {
172
186
  type: "core",
173
- // Sometimes other status codes can be cached, like 404. For these cases, we should return the correct status code
174
- // Also set the status code to the rewriteStatusCode if defined
175
- // This can happen in handleMiddleware in routingHandler.
176
- // `NextResponse.rewrite(url, { status: xxx})
177
- // The rewrite status code should take precedence over the cached one
178
- statusCode: event.rewriteStatusCode ?? cachedValue.meta?.status ?? 200,
187
+ statusCode,
179
188
  body: toReadableStream(body, false),
180
189
  isBase64Encoded: false,
181
- headers: {
182
- ...cacheControl,
183
- "content-type": type,
184
- ...cachedValue.meta?.headers,
185
- vary: VARY_HEADER,
186
- ...additionalHeaders,
187
- },
190
+ headers,
188
191
  };
189
192
  }
193
+ /**
194
+ * Computes the status code to return for a cache hit.
195
+ *
196
+ * Sometimes other status codes can be cached, like 404. For these cases, we should return the correct status code.
197
+ * The rewrite status code can be set in handleMiddleware in routingHandler with
198
+ * `NextResponse.rewrite(url, { status: xxx })`.
199
+ *
200
+ * A meaningful cached status code (i.e. anything but the implicit 200) wins over the rewrite
201
+ * status code, otherwise a rewrite would turn a cached error page into a successful response.
202
+ *
203
+ * @param rewriteStatusCode The status code from the middleware rewrite, if any.
204
+ * @param cachedStatusCode The status code stored in the cache entry meta, if any.
205
+ * @returns The status code to return.
206
+ */
207
+ function computeStatusCode(rewriteStatusCode, cachedStatusCode) {
208
+ if (cachedStatusCode !== undefined && cachedStatusCode !== 200) {
209
+ return cachedStatusCode;
210
+ }
211
+ return rewriteStatusCode ?? cachedStatusCode ?? 200;
212
+ }
190
213
  /**
191
214
  *
192
215
  * https://github.com/vercel/next.js/blob/34039551d2e5f611c0abde31a197d9985918adaf/packages/next/src/shared/lib/router/utils/escape-path-delimiters.ts#L2-L10
@@ -209,7 +232,7 @@ function decodePathParams(pathname) {
209
232
  }
210
233
  export async function cacheInterceptor(event) {
211
234
  if (Boolean(event.headers["next-action"]) ||
212
- Boolean(event.headers["x-prerender-revalidate"]))
235
+ Boolean(event.headers[PRERENDER_REVALIDATE_HEADER]))
213
236
  return event;
214
237
  // Check for Next.js preview mode cookies
215
238
  const cookies = event.headers.cookie || "";
@@ -290,15 +313,19 @@ export async function cacheInterceptor(event) {
290
313
  case "route": {
291
314
  const cacheControl = await computeCacheControl(localizedPath, cachedData.value.body, host, cachedData.value.revalidate, cachedData.lastModified, _isStale);
292
315
  const isBinary = isBinaryContentType(String(cachedData.value.meta?.headers?.["content-type"]));
316
+ const statusCode = computeStatusCode(event.rewriteStatusCode, cachedData.value.meta?.status);
317
+ const headers = {
318
+ ...cacheControl,
319
+ ...cachedData.value.meta?.headers,
320
+ vary: VARY_HEADER,
321
+ };
322
+ // See the note in `generateResult`.
323
+ fixCacheControlForError(headers, statusCode);
293
324
  return {
294
325
  type: "core",
295
- statusCode: event.rewriteStatusCode ?? cachedData.value.meta?.status ?? 200,
326
+ statusCode,
296
327
  body: toReadableStream(cachedData.value.body, isBinary),
297
- headers: {
298
- ...cacheControl,
299
- ...cachedData.value.meta?.headers,
300
- vary: VARY_HEADER,
301
- },
328
+ headers,
302
329
  isBase64Encoded: isBinary,
303
330
  };
304
331
  }
@@ -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
@@ -100,13 +101,10 @@ export async function handleMiddleware(internalEvent, initialSearch, middlewareL
100
101
  else {
101
102
  if (filteredHeaders.includes(key.toLowerCase()))
102
103
  return;
103
- if (key.toLowerCase() === "set-cookie") {
104
- resHeaders[key] = resHeaders[key]
105
- ? [...resHeaders[key], value]
106
- : [value];
107
- }
108
- else if (REDIRECTS.has(statusCode) &&
109
- key.toLowerCase() === "location") {
104
+ // Headers.forEach folds same-name headers; use getSetCookie() below instead.
105
+ if (key.toLowerCase() === "set-cookie")
106
+ return;
107
+ if (REDIRECTS.has(statusCode) && key.toLowerCase() === "location") {
110
108
  resHeaders[key] = normalizeLocationHeader(value, internalEvent.url);
111
109
  }
112
110
  else {
@@ -114,6 +112,10 @@ export async function handleMiddleware(internalEvent, initialSearch, middlewareL
114
112
  }
115
113
  }
116
114
  });
115
+ const setCookies = responseHeaders.getSetCookie();
116
+ if (setCookies.length > 0) {
117
+ resHeaders["set-cookie"] = setCookies;
118
+ }
117
119
  // If the middleware returned a Rewrite, set the `url` to the pathname of the rewrite
118
120
  // NOTE: the header was added to `req` from above
119
121
  const rewriteUrl = responseHeaders.get("x-middleware-rewrite");
@@ -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
  /**
@@ -205,11 +206,6 @@ export function convertBodyToReadableStream(method, body) {
205
206
  },
206
207
  });
207
208
  }
208
- var CommonHeaders;
209
- (function (CommonHeaders) {
210
- CommonHeaders["CACHE_CONTROL"] = "cache-control";
211
- CommonHeaders["NEXT_CACHE"] = "x-nextjs-cache";
212
- })(CommonHeaders || (CommonHeaders = {}));
213
209
  /**
214
210
  *
215
211
  * @__PURE__
@@ -217,11 +213,7 @@ var CommonHeaders;
217
213
  export function fixCacheHeaderForHtmlPages(internalEvent, headers) {
218
214
  // We don't want to cache error pages
219
215
  if (internalEvent.rawPath === "/404" || internalEvent.rawPath === "/500") {
220
- if (process.env.OPEN_NEXT_DANGEROUSLY_SET_ERROR_HEADERS === "true") {
221
- return;
222
- }
223
- headers[CommonHeaders.CACHE_CONTROL] =
224
- "private, no-cache, no-store, max-age=0, must-revalidate";
216
+ fixCacheControlForError(headers, internalEvent.rawPath === "/404" ? 404 : 500);
225
217
  return;
226
218
  }
227
219
  const localizedPath = localizePath(internalEvent);
@@ -230,7 +222,7 @@ export function fixCacheHeaderForHtmlPages(internalEvent, headers) {
230
222
  // Requests containing an `x-middleware-prefetch` header must not be cached
231
223
  if (HtmlPages.includes(localizedPath) &&
232
224
  !internalEvent.headers["x-middleware-prefetch"]) {
233
- headers[CommonHeaders.CACHE_CONTROL] =
225
+ headers[CACHE_CONTROL_HEADER] =
234
226
  "public, max-age=0, s-maxage=31536000, must-revalidate";
235
227
  }
236
228
  }
@@ -240,7 +232,7 @@ export function fixCacheHeaderForHtmlPages(internalEvent, headers) {
240
232
  */
241
233
  export function fixSWRCacheHeader(headers) {
242
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
243
- let cacheControl = headers[CommonHeaders.CACHE_CONTROL];
235
+ let cacheControl = headers[CACHE_CONTROL_HEADER];
244
236
  if (!cacheControl)
245
237
  return;
246
238
  if (Array.isArray(cacheControl)) {
@@ -248,7 +240,7 @@ export function fixSWRCacheHeader(headers) {
248
240
  }
249
241
  if (typeof cacheControl !== "string")
250
242
  return;
251
- 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");
252
244
  }
253
245
  /**
254
246
  *
@@ -271,7 +263,7 @@ export function addOpenNextHeader(headers) {
271
263
  * @__PURE__
272
264
  */
273
265
  export async function revalidateIfRequired(host, rawPath, headers, req) {
274
- if (headers[CommonHeaders.NEXT_CACHE] === "STALE") {
266
+ if (headers[NEXTJS_CACHE_HEADER] === "STALE") {
275
267
  // If the URL is rewritten, revalidation needs to be done on the rewritten URL.
276
268
  // - Link to Next.js doc: https://nextjs.org/docs/pages/building-your-application/data-fetching/incremental-static-regeneration#on-demand-revalidation
277
269
  // - Link to NextInternalRequestMeta: https://github.com/vercel/next.js/blob/57ab2818b93627e91c937a130fb56a36c41629c3/packages/next/src/server/request-meta.ts#L11
@@ -315,37 +307,35 @@ export async function revalidateIfRequired(host, rawPath, headers, req) {
315
307
  */
316
308
  export function fixISRHeaders(headers) {
317
309
  const sMaxAgeRegex = /s-maxage=(\d+)/;
318
- const match = headers[CommonHeaders.CACHE_CONTROL]?.match(sMaxAgeRegex);
310
+ const match = headers[CACHE_CONTROL_HEADER]?.match(sMaxAgeRegex);
319
311
  const sMaxAge = match ? Number.parseInt(match[1]) : undefined;
320
312
  // We only apply the fix if the cache-control header contains s-maxage
321
313
  if (!sMaxAge) {
322
314
  return;
323
315
  }
324
- if (headers[CommonHeaders.NEXT_CACHE] === "REVALIDATED") {
325
- headers[CommonHeaders.CACHE_CONTROL] =
326
- "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;
327
318
  return;
328
319
  }
329
320
  const _lastModified = globalThis.__openNextAls.getStore()?.lastModified ?? 0;
330
- if (headers[CommonHeaders.NEXT_CACHE] === "HIT" && _lastModified > 0) {
331
- 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());
332
323
  // 31536000 is the default s-maxage value for SSG pages
333
324
  if (sMaxAge && sMaxAge !== 31536000) {
334
325
  // calculate age
335
326
  const age = Math.round((Date.now() - _lastModified) / 1000);
336
327
  const remainingTtl = Math.max(sMaxAge - age, 1);
337
- headers[CommonHeaders.CACHE_CONTROL] =
328
+ headers[CACHE_CONTROL_HEADER] =
338
329
  `s-maxage=${remainingTtl}, stale-while-revalidate=2592000`;
339
330
  }
340
331
  }
341
- if (headers[CommonHeaders.NEXT_CACHE] !== "STALE")
332
+ if (headers[NEXTJS_CACHE_HEADER] !== "STALE")
342
333
  return;
343
334
  // If the cache is stale, we revalidate in the background
344
335
  // In order for CloudFront SWR to work, we set the stale-while-revalidate value to 2 seconds
345
336
  // This will cause CloudFront to cache the stale data for a short period of time while we revalidate in the background
346
337
  // Once the revalidation is complete, CloudFront will serve the fresh data
347
- headers[CommonHeaders.CACHE_CONTROL] =
348
- "s-maxage=2, stale-while-revalidate=2592000";
338
+ headers[CACHE_CONTROL_HEADER] = "s-maxage=2, stale-while-revalidate=2592000";
349
339
  }
350
340
  /**
351
341
  *
@@ -371,9 +361,8 @@ export function createServerResponse(routingResult, headers, responseStream) {
371
361
  export async function invalidateCDNOnRequest(params, headers) {
372
362
  const { internalEvent, resolvedRoutes, initialURL } = params;
373
363
  const initialPath = new URL(initialURL).pathname;
374
- const isIsrRevalidation = internalEvent.headers["x-isr"] === "1";
375
- if (!isIsrRevalidation &&
376
- headers[CommonHeaders.NEXT_CACHE] === "REVALIDATED") {
364
+ const isIsrRevalidation = internalEvent.headers[ISR_HEADER] === "1";
365
+ if (!isIsrRevalidation && headers[NEXTJS_CACHE_HEADER] === "REVALIDATED") {
377
366
  await globalThis.cdnInvalidationHandler.invalidatePaths([
378
367
  {
379
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
  },
@@ -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
  *
@@ -90,8 +91,8 @@ export function getTagsFromValue(value) {
90
91
  }
91
92
  // The try catch is necessary for older version of next.js that may fail on this
92
93
  try {
93
- const cacheTags = value.meta?.headers?.["x-next-cache-tags"]?.split(",") ?? [];
94
- 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];
95
96
  return cacheTags;
96
97
  }
97
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.1",
6
+ "version": "4.1.3",
7
7
  "bin": {
8
8
  "open-next": "./dist/index.js"
9
9
  },