@opennextjs/aws 3.7.7 → 3.8.1

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.
@@ -75,7 +75,9 @@ const defaultHandler = async (internalEvent, options) => {
75
75
  };
76
76
  }
77
77
  }
78
- result.headers[INTERNAL_EVENT_REQUEST_ID] = requestId;
78
+ if (process.env.OPEN_NEXT_REQUEST_ID_HEADER || globalThis.openNextDebug) {
79
+ result.headers[INTERNAL_EVENT_REQUEST_ID] = requestId;
80
+ }
79
81
  debug("Middleware response", result);
80
82
  return result;
81
83
  });
@@ -1,3 +1,4 @@
1
+ export declare function isExcluded(srcPath: string): boolean;
1
2
  interface CopyTracedFilesOptions {
2
3
  buildOutputPath: string;
3
4
  packagePath: string;
@@ -18,8 +18,13 @@ const EXCLUDED_PACKAGES = [
18
18
  "next/dist/compiled/babel-packages",
19
19
  "next/dist/compiled/amphtml-validator",
20
20
  ];
21
- function isExcluded(srcPath) {
22
- return EXCLUDED_PACKAGES.some((excluded) => srcPath.match(getCrossPlatformPathRegex(`/node_modules/${excluded}/`)));
21
+ export function isExcluded(srcPath) {
22
+ return EXCLUDED_PACKAGES.some((excluded) =>
23
+ // `pnpm` can create a symbolic link that points to the pnpm store folder
24
+ // This will live under `/node_modules/sharp`. We need to handle this in our regex
25
+ srcPath.match(getCrossPlatformPathRegex(`/node_modules/${excluded}(?:/|$)`, {
26
+ escape: false,
27
+ })));
23
28
  }
24
29
  function copyPatchFile(outputDir) {
25
30
  const patchFile = path.join(__dirname, "patch", "patchedAsyncStorage.js");
@@ -63,7 +63,7 @@ export function installDependencies(outputDir, installOptions) {
63
63
  logger.info(`Dependencies installed for ${name}`);
64
64
  }
65
65
  catch (e) {
66
- logger.error(e.stdout.toString());
66
+ logger.error(e.toString());
67
67
  logger.error("Could not install dependencies");
68
68
  }
69
69
  }
@@ -31,7 +31,7 @@ export async function openNextHandler(internalEvent, options) {
31
31
  initialHeaders.host = initialHeaders["x-forwarded-host"];
32
32
  }
33
33
  debug("internalEvent", internalEvent);
34
- // These 2 will get overwritten by the routing handler if not using an external middleware
34
+ // These 3 will get overwritten by the routing handler if not using an external middleware
35
35
  const internalHeaders = {
36
36
  initialPath: initialHeaders[INTERNAL_HEADER_INITIAL_URL] ?? internalEvent.rawPath,
37
37
  resolvedRoutes: initialHeaders[INTERNAL_HEADER_RESOLVED_ROUTES]
@@ -1,2 +1,2 @@
1
- import type { InternalEvent, InternalResult } from "../../types/open-next";
2
- export declare function cacheInterceptor(event: InternalEvent): Promise<InternalEvent | InternalResult>;
1
+ import type { InternalEvent, InternalResult, MiddlewareEvent } from "../../types/open-next";
2
+ export declare function cacheInterceptor(event: MiddlewareEvent): Promise<InternalEvent | InternalResult>;
@@ -103,8 +103,12 @@ async function generateResult(event, localizedPath, cachedValue, lastModified) {
103
103
  const cacheControl = await computeCacheControl(localizedPath, body, event.headers.host, cachedValue.revalidate, lastModified);
104
104
  return {
105
105
  type: "core",
106
- // sometimes other status codes can be cached, like 404. For these cases, we should return the correct status code
107
- statusCode: cachedValue.meta?.status ?? 200,
106
+ // Sometimes other status codes can be cached, like 404. For these cases, we should return the correct status code
107
+ // Also set the status code to the rewriteStatusCode if defined
108
+ // This can happen in handleMiddleware in routingHandler.
109
+ // `NextResponse.rewrite(url, { status: xxx})
110
+ // The rewrite status code should take precedence over the cached one
111
+ statusCode: event.rewriteStatusCode ?? cachedValue.meta?.status ?? 200,
108
112
  body: toReadableStream(body, false),
109
113
  isBase64Encoded: false,
110
114
  headers: {
@@ -209,7 +213,7 @@ export async function cacheInterceptor(event) {
209
213
  const isBinary = isBinaryContentType(String(cachedData.value.meta?.headers?.["content-type"]));
210
214
  return {
211
215
  type: "core",
212
- statusCode: cachedData.value.meta?.status ?? 200,
216
+ statusCode: event.rewriteStatusCode ?? cachedData.value.meta?.status ?? 200,
213
217
  body: toReadableStream(cachedData.value.body, isBinary),
214
218
  headers: {
215
219
  ...cacheControl,
@@ -1,9 +1,4 @@
1
- import type { InternalEvent, InternalResult } from "../../types/open-next.js";
2
- type MiddlewareEvent = InternalEvent & {
3
- responseHeaders?: Record<string, string | string[]>;
4
- isExternalRewrite?: boolean;
5
- rewriteStatusCode?: number;
6
- };
1
+ import type { InternalEvent, InternalResult, MiddlewareEvent } from "../../types/open-next.js";
7
2
  type Middleware = (request: Request) => Response | Promise<Response>;
8
3
  type MiddlewareLoader = () => Promise<{
9
4
  default: Middleware;
@@ -151,6 +151,6 @@ export async function handleMiddleware(internalEvent, initialSearch, middlewareL
151
151
  cookies: internalEvent.cookies,
152
152
  remoteAddress: internalEvent.remoteAddress,
153
153
  isExternalRewrite,
154
- rewriteStatusCode: statusCode,
154
+ rewriteStatusCode: rewriteUrl && !isExternalRewrite ? statusCode : undefined,
155
155
  };
156
156
  }
@@ -3,6 +3,7 @@ import type { IncomingMessage } from "../../http/index.js";
3
3
  import { OpenNextNodeResponse } from "../../http/openNextResponse.js";
4
4
  import type { FunctionsConfigManifest, MiddlewareManifest } from "../../types/next-types";
5
5
  import type { InternalEvent, InternalResult, RoutingResult, StreamCreator } from "../../types/open-next.js";
6
+ import { ReadableStream } from "node:stream/web";
6
7
  /**
7
8
  *
8
9
  * @__PURE__
@@ -1,9 +1,9 @@
1
1
  import crypto from "node:crypto";
2
2
  import { parse as parseQs, stringify as stringifyQs } from "node:querystring";
3
- import { Readable } from "node:stream";
4
3
  import { BuildId, HtmlPages, NextConfig } from "../../adapters/config/index.js";
5
4
  import { OpenNextNodeResponse } from "../../http/openNextResponse.js";
6
5
  import { getQueryFromIterator, parseHeaders } from "../../http/util.js";
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
9
  import { localizePath } from "./i18n/index.js";
@@ -84,9 +84,15 @@ export function convertRes(res) {
84
84
  const headers = parseHeaders(res.getFixedHeaders());
85
85
  const isBase64Encoded = isBinaryContentType(headers["content-type"]) ||
86
86
  !!headers["content-encoding"];
87
- // We cannot convert the OpenNextNodeResponse to a ReadableStream directly
88
- // You can look in the `aws-lambda.ts` file for some context
89
- const body = Readable.toWeb(Readable.from(res.getBody()));
87
+ const body = new ReadableStream({
88
+ pull(controller) {
89
+ if (!res._chunks || res._chunks.length === 0) {
90
+ controller.close();
91
+ return;
92
+ }
93
+ controller.enqueue(res._chunks.shift());
94
+ },
95
+ });
90
96
  return {
91
97
  type: "core",
92
98
  statusCode,
@@ -79,10 +79,20 @@ export default async function routingHandler(event, { assetResolver }) {
79
79
  if (isInternalResult(middlewareEventOrResult)) {
80
80
  return middlewareEventOrResult;
81
81
  }
82
- headers = {
83
- ...middlewareEventOrResult.responseHeaders,
84
- ...headers,
85
- };
82
+ const middlewareHeadersPrioritized = globalThis.openNextConfig.dangerous
83
+ ?.middlewareHeadersOverrideNextConfigHeaders ?? false;
84
+ if (middlewareHeadersPrioritized) {
85
+ headers = {
86
+ ...headers,
87
+ ...middlewareEventOrResult.responseHeaders,
88
+ };
89
+ }
90
+ else {
91
+ headers = {
92
+ ...middlewareEventOrResult.responseHeaders,
93
+ ...headers,
94
+ };
95
+ }
86
96
  let isExternalRewrite = middlewareEventOrResult.isExternalRewrite ?? false;
87
97
  eventOrResult = middlewareEventOrResult;
88
98
  if (!isExternalRewrite) {
@@ -11,10 +11,11 @@ export declare class OpenNextNodeResponse extends Transform implements ServerRes
11
11
  statusCode: number;
12
12
  statusMessage: string;
13
13
  headers: OutgoingHttpHeaders;
14
- private _cookies;
15
- private responseStream?;
16
14
  headersSent: boolean;
17
15
  _chunks: Buffer[];
16
+ private _cookies;
17
+ private responseStream?;
18
+ private bodyLength;
18
19
  strictContentLength: boolean;
19
20
  assignSocket(_socket: Socket): void;
20
21
  detachSocket(_socket: Socket): void;
@@ -12,10 +12,11 @@ export class OpenNextNodeResponse extends Transform {
12
12
  statusCode;
13
13
  statusMessage = "";
14
14
  headers = {};
15
- _cookies = [];
16
- responseStream;
17
15
  headersSent = false;
18
16
  _chunks = [];
17
+ _cookies = [];
18
+ responseStream;
19
+ bodyLength = 0;
19
20
  // To comply with the ServerResponse interface :
20
21
  strictContentLength = false;
21
22
  assignSocket(_socket) {
@@ -221,7 +222,12 @@ export class OpenNextNodeResponse extends Transform {
221
222
  return Buffer.concat(this._chunks);
222
223
  }
223
224
  _internalWrite(chunk, encoding) {
224
- this._chunks.push(Buffer.from(chunk, encoding));
225
+ const buffer = Buffer.from(chunk, encoding);
226
+ this.bodyLength += buffer.length;
227
+ if (this.streamCreator?.retainChunks !== false) {
228
+ // Avoid keeping chunks around when the `StreamCreator` supports it to save memory
229
+ this._chunks.push(buffer);
230
+ }
225
231
  this.push(chunk, encoding);
226
232
  this.streamCreator?.onWrite?.();
227
233
  }
@@ -237,19 +243,18 @@ export class OpenNextNodeResponse extends Transform {
237
243
  this.flushHeaders();
238
244
  }
239
245
  // In some cases we might not have a store i.e. for example in the image optimization function
240
- // We may want to reconsider this in the future, it might be intersting to have access to this store everywhere
246
+ // We may want to reconsider this in the future, it might be interesting to have access to this store everywhere
241
247
  globalThis.__openNextAls
242
248
  ?.getStore()
243
249
  ?.pendingPromiseRunner.add(this.onEnd(this.headers));
244
- const bodyLength = this.getBody().length;
245
- this.streamCreator?.onFinish?.(bodyLength);
250
+ this.streamCreator?.onFinish?.(this.bodyLength);
246
251
  //This is only here because of aws broken streaming implementation.
247
252
  //Hopefully one day they will be able to give us a working streaming implementation in lambda for everyone
248
253
  //If you're lucky you have a working streaming implementation in your aws account and don't need this
249
254
  //If not you can set the OPEN_NEXT_FORCE_NON_EMPTY_RESPONSE env variable to true
250
255
  //BE CAREFUL: Aws keeps rolling out broken streaming implementations even on accounts that had working ones before
251
256
  //This is not dependent on the node runtime used
252
- if (bodyLength === 0 &&
257
+ if (this.bodyLength === 0 &&
253
258
  // We use an env variable here because not all aws account have the same behavior
254
259
  // On some aws accounts the response will hang if the body is empty
255
260
  // We are modifying the response body here, this is not a good practice
@@ -283,8 +288,10 @@ export class OpenNextNodeResponse extends Transform {
283
288
  return (Array.isArray(values) ? values : [values]).map((value) => value.toString());
284
289
  }
285
290
  send() {
286
- const body = this.getBody();
287
- this.end(body);
291
+ for (const chunk of this._chunks) {
292
+ this.write(chunk);
293
+ }
294
+ this.end();
288
295
  }
289
296
  body(value) {
290
297
  this.write(value);
package/dist/http/util.js CHANGED
@@ -1,3 +1,4 @@
1
+ import logger from "../logger";
1
2
  export const parseHeaders = (headers) => {
2
3
  const result = {};
3
4
  if (!headers) {
@@ -7,7 +8,26 @@ export const parseHeaders = (headers) => {
7
8
  if (value === undefined) {
8
9
  continue;
9
10
  }
10
- result[key.toLowerCase()] = convertHeader(value);
11
+ const keyLower = key.toLowerCase();
12
+ /**
13
+ * Next can return an Array for the Location header when you return null from a get in the cacheHandler on a page that has a redirect()
14
+ * We dont want to merge that into a comma-separated string
15
+ * If they are the same just return one of them
16
+ * Otherwise return the last one
17
+ * See: https://github.com/opennextjs/opennextjs-cloudflare/issues/875#issuecomment-3258248276
18
+ * and https://github.com/opennextjs/opennextjs-aws/pull/977#issuecomment-3261763114
19
+ */
20
+ if (keyLower === "location" && Array.isArray(value)) {
21
+ if (value[0] === value[1]) {
22
+ result[keyLower] = value[0];
23
+ }
24
+ else {
25
+ logger.warn("Multiple different values for Location header found. Using the last one");
26
+ result[keyLower] = value[value.length - 1];
27
+ }
28
+ continue;
29
+ }
30
+ result[keyLower] = convertHeader(value);
11
31
  }
12
32
  return result;
13
33
  };
@@ -42,6 +42,8 @@ const handler = async (handler, converter) => async (request, env, ctx, abortSig
42
42
  // Ensures that the response we pass to NextServer is aborted if the request is aborted
43
43
  // By doing this `request.signal.onabort` will work in route handlers
44
44
  abortSignal: abortSignal,
45
+ // There is no need to retain the chunks that were pushed to the response stream.
46
+ retainChunks: false,
45
47
  };
46
48
  ctx.waitUntil(handler(internalEvent, {
47
49
  streamCreator,
@@ -15,6 +15,11 @@ export type InternalEvent = {
15
15
  readonly cookies: Record<string, string>;
16
16
  readonly remoteAddress: string;
17
17
  } & BaseEventOrResult<"core">;
18
+ export type MiddlewareEvent = InternalEvent & {
19
+ responseHeaders?: Record<string, string | string[]>;
20
+ isExternalRewrite?: boolean;
21
+ rewriteStatusCode?: number;
22
+ };
18
23
  export type InternalResult = {
19
24
  statusCode: number;
20
25
  headers: Record<string, string | string[]>;
@@ -31,6 +36,18 @@ export interface StreamCreator {
31
36
  onWrite?: () => void;
32
37
  onFinish?: (length: number) => void;
33
38
  abortSignal?: AbortSignal;
39
+ /**
40
+ * Normally there is no need to retain the chunks that have been pushed to the response stream.
41
+ *
42
+ * However some implementations use a fake `StreamCreator` and expect the chunks to be retained.
43
+ * When your stream controller implementation doesn't need to retain the chunk, you can set this
44
+ * to `false` to reduce memory usage.
45
+ *
46
+ * @see https://github.com/opennextjs/opennextjs-aws/blob/main/packages/open-next/src/overrides/wrappers/aws-lambda.ts
47
+ *
48
+ * @default true for backward compatibility.
49
+ */
50
+ retainChunks?: boolean;
34
51
  }
35
52
  export type WaitUntil = (promise: Promise<void>) => void;
36
53
  export interface DangerousOptions {
@@ -58,6 +75,16 @@ export interface DangerousOptions {
58
75
  * This is executed for every request and after next config headers and middleware has executed.
59
76
  */
60
77
  headersAndCookiesPriority?: (event: InternalEvent) => "middleware" | "handler";
78
+ /**
79
+ * Configuration option to prioritize headers set via middleware over headers set via the option in the Next config.
80
+ *
81
+ * The default will change to 'true' in v4.
82
+ *
83
+ * See also {@link https://nextjs.org/docs/app/api-reference/file-conventions/middleware#execution-order}
84
+ *
85
+ * @default false
86
+ */
87
+ middlewareHeadersOverrideNextConfigHeaders?: boolean;
61
88
  }
62
89
  export type BaseOverride = {
63
90
  name: string;
@@ -13,7 +13,9 @@ export async function hasBeenRevalidated(key, tags, cacheEntry) {
13
13
  }
14
14
  const lastModified = cacheEntry.lastModified ?? Date.now();
15
15
  if (globalThis.tagCache.mode === "nextMode") {
16
- return await globalThis.tagCache.hasBeenRevalidated(tags, lastModified);
16
+ return tags.length === 0
17
+ ? false
18
+ : await globalThis.tagCache.hasBeenRevalidated(tags, lastModified);
17
19
  }
18
20
  // TODO: refactor this, we should introduce a new method in the tagCache interface so that both implementations use hasBeenRevalidated
19
21
  const _lastModified = await globalThis.tagCache.getLastModified(key, lastModified);
@@ -25,7 +27,9 @@ export function getTagsFromValue(value) {
25
27
  }
26
28
  // The try catch is necessary for older version of next.js that may fail on this
27
29
  try {
28
- return value.meta?.headers?.["x-next-cache-tags"]?.split(",") ?? [];
30
+ const cacheTags = value.meta?.headers?.["x-next-cache-tags"]?.split(",") ?? [];
31
+ delete value.meta?.headers?.["x-next-cache-tags"];
32
+ return cacheTags;
29
33
  }
30
34
  catch (e) {
31
35
  return [];
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "access": "public"
4
4
  },
5
5
  "name": "@opennextjs/aws",
6
- "version": "3.7.7",
6
+ "version": "3.8.1",
7
7
  "bin": {
8
8
  "open-next": "./dist/index.js"
9
9
  },