diff --git a/.changeset/weak-hotels-thank.md b/.changeset/weak-hotels-thank.md new file mode 100644 index 000000000..d6505f557 --- /dev/null +++ b/.changeset/weak-hotels-thank.md @@ -0,0 +1,8 @@ +--- +"@opennextjs/aws": patch +--- + +Fix cloudflare env +Fix an issue with cookies and the node wrapper +Fix some issue with cookies being not properly set when set both in the routing layer and the route itself +Added option for headers priority diff --git a/packages/open-next/src/build.ts b/packages/open-next/src/build.ts index 72f8d555c..dbbf32fb7 100755 --- a/packages/open-next/src/build.ts +++ b/packages/open-next/src/build.ts @@ -754,12 +754,14 @@ async function createMiddleware() { overrides: config.middleware?.override, defaultConverter: "aws-cloudfront", includeCache: config.dangerous?.enableCacheInterception, + additionalExternals: config.edgeExternals, }); } else { await buildEdgeBundle({ entrypoint: path.join(__dirname, "core", "edgeFunctionHandler.js"), outfile: path.join(outputDir, ".build", "middleware.mjs"), ...commonMiddlewareOptions, + onlyBuildOnce: true, }); } } diff --git a/packages/open-next/src/build/createServerBundle.ts b/packages/open-next/src/build/createServerBundle.ts index 16772b962..fcafee61e 100644 --- a/packages/open-next/src/build/createServerBundle.ts +++ b/packages/open-next/src/build/createServerBundle.ts @@ -51,7 +51,7 @@ export async function createServerBundle( const routes = fnOptions.routes; routes.forEach((route) => foundRoutes.add(route)); if (fnOptions.runtime === "edge") { - await generateEdgeBundle(name, options, fnOptions); + await generateEdgeBundle(name, config, options, fnOptions); } else { await generateBundle(name, config, options, fnOptions); } diff --git a/packages/open-next/src/build/edge/createEdgeBundle.ts b/packages/open-next/src/build/edge/createEdgeBundle.ts index e846f935e..c8722dbc9 100644 --- a/packages/open-next/src/build/edge/createEdgeBundle.ts +++ b/packages/open-next/src/build/edge/createEdgeBundle.ts @@ -1,11 +1,13 @@ import { mkdirSync } from "node:fs"; import url from "node:url"; +import { build } from "esbuild"; import fs from "fs"; import path from "path"; import { MiddlewareInfo, MiddlewareManifest } from "types/next-types"; import { IncludedConverter, + OpenNextConfig, OverrideOptions, RouteTemplate, SplittedFunctionOptions, @@ -29,6 +31,8 @@ interface BuildEdgeBundleOptions { defaultConverter?: IncludedConverter; additionalInject?: string; includeCache?: boolean; + additionalExternals?: string[]; + onlyBuildOnce?: boolean; } export async function buildEdgeBundle({ @@ -41,7 +45,13 @@ export async function buildEdgeBundle({ overrides, additionalInject, includeCache, + additionalExternals, + onlyBuildOnce, }: BuildEdgeBundleOptions) { + const isInCloudfare = + typeof overrides?.wrapper === "string" + ? overrides.wrapper === "cloudflare" + : (await overrides?.wrapper?.())?.edgeRuntime; await esbuildAsync( { entryPoints: [entrypoint], @@ -93,7 +103,7 @@ export async function buildEdgeBundle({ "../../core", "edgeFunctionHandler.js", ), - isInCloudfare: overrides?.wrapper === "cloudflare", + isInCloudfare, }), ], treeShaking: true, @@ -106,8 +116,13 @@ export async function buildEdgeBundle({ mainFields: ["module", "main"], banner: { js: ` +import {Buffer} from "node:buffer"; +globalThis.Buffer = Buffer; + +import {AsyncLocalStorage} from "node:async_hooks"; +globalThis.AsyncLocalStorage = AsyncLocalStorage; ${ - overrides?.wrapper === "cloudflare" + isInCloudfare ? "" : ` const require = (await import("node:module")).createRequire(import.meta.url); @@ -129,12 +144,30 @@ export async function buildEdgeBundle({ }, options, ); + + if (!onlyBuildOnce) { + await build({ + entryPoints: [outfile], + outfile, + allowOverwrite: true, + bundle: true, + minify: true, + platform: "node", + format: "esm", + conditions: ["workerd", "worker", "browser"], + external: ["node:*", ...(additionalExternals ?? [])], + banner: { + js: 'import * as process from "node:process";', + }, + }); + } } export function copyMiddlewareAssetsAndWasm({}) {} export async function generateEdgeBundle( name: string, + config: OpenNextConfig, options: BuildOptions, fnOptions: SplittedFunctionOptions, ) { @@ -193,5 +226,6 @@ export async function generateEdgeBundle( outfile: path.join(outputPath, "index.mjs"), options, overrides: fnOptions.override, + additionalExternals: config.edgeExternals, }); } diff --git a/packages/open-next/src/core/createMainHandler.ts b/packages/open-next/src/core/createMainHandler.ts index faebad189..deb5ecda9 100644 --- a/packages/open-next/src/core/createMainHandler.ts +++ b/packages/open-next/src/core/createMainHandler.ts @@ -25,6 +25,7 @@ declare global { requestId: string; pendingPromiseRunner: DetachedPromiseRunner; isISRRevalidation?: boolean; + mergeHeadersPriority?: "middleware" | "handler"; }>; } diff --git a/packages/open-next/src/core/requestHandler.ts b/packages/open-next/src/core/requestHandler.ts index c34b9e46d..60a2bd0f6 100644 --- a/packages/open-next/src/core/requestHandler.ts +++ b/packages/open-next/src/core/requestHandler.ts @@ -19,6 +19,7 @@ globalThis.__als = new AsyncLocalStorage<{ requestId: string; pendingPromiseRunner: DetachedPromiseRunner; isISRRevalidation?: boolean; + mergeHeadersPriority?: "middleware" | "handler"; }>(); patchAsyncStorage(); @@ -103,8 +104,19 @@ export async function openNextHandler( const pendingPromiseRunner: DetachedPromiseRunner = new DetachedPromiseRunner(); const isISRRevalidation = headers["x-isr"] === "1"; + const mergeHeadersPriority = globalThis.openNextConfig.dangerous + ?.headersAndCookiesPriority + ? globalThis.openNextConfig.dangerous.headersAndCookiesPriority( + preprocessedEvent, + ) + : "middleware"; const internalResult = await globalThis.__als.run( - { requestId, pendingPromiseRunner, isISRRevalidation }, + { + requestId, + pendingPromiseRunner, + isISRRevalidation, + mergeHeadersPriority, + }, async () => { const preprocessedResult = preprocessResult as MiddlewareOutputEvent; const req = new IncomingMessage(reqProps); diff --git a/packages/open-next/src/http/openNextResponse.ts b/packages/open-next/src/http/openNextResponse.ts index 3ad18a044..9e4d2d717 100644 --- a/packages/open-next/src/http/openNextResponse.ts +++ b/packages/open-next/src/http/openNextResponse.ts @@ -82,11 +82,6 @@ export class OpenNextNodeResponse extends Transform implements ServerResponse { private initialHeaders?: OutgoingHttpHeaders, ) { super(); - if (initialHeaders && initialHeaders[SET_COOKIE_HEADER]) { - this._cookies = parseCookies( - initialHeaders[SET_COOKIE_HEADER] as string | string[], - ) as string[]; - } this.once("finish", () => { if (!this.headersSent) { this.flushHeaders(); @@ -164,28 +159,43 @@ export class OpenNextNodeResponse extends Transform implements ServerResponse { this.headersSent = true; // Initial headers should be merged with the new headers // These initial headers are the one created either in the middleware or in next.config.js - // We choose to override response headers with middleware headers - // This is different than the default behavior in next.js, but it allows more customization - // TODO: We probably want to change this behavior in the future to follow next - // We could add a prefix header that would allow to force the middleware headers - // Something like open-next-force-cache-control would override the cache-control header + const mergeHeadersPriority = + globalThis.__als?.getStore()?.mergeHeadersPriority ?? "middleware"; if (this.initialHeaders) { - this.headers = { - ...this.headers, - ...this.initialHeaders, - }; + this.headers = + mergeHeadersPriority === "middleware" + ? { + ...this.headers, + ...this.initialHeaders, + } + : { + ...this.initialHeaders, + ...this.headers, + }; + const initialCookies = parseCookies( + (this.initialHeaders[SET_COOKIE_HEADER] as string | string[]) ?? [], + ) as string[]; + this._cookies = + mergeHeadersPriority === "middleware" + ? [...this._cookies, ...initialCookies] + : [...initialCookies, ...this._cookies]; } this.fixHeaders(this.headers); - if (this._cookies.length > 0) { - // For cookies we cannot do the same as for other headers - this.headers[SET_COOKIE_HEADER] = this._cookies; - } + + // We need to fix the set-cookie header here + this.headers[SET_COOKIE_HEADER] = this._cookies; + + const parsedHeaders = parseHeaders(this.headers); + + // We need to remove the set-cookie header from the parsed headers because + // it does not handle multiple set-cookie headers properly + delete parsedHeaders[SET_COOKIE_HEADER]; if (this.streamCreator) { this.responseStream = this.streamCreator?.writeHeaders({ statusCode: this.statusCode ?? 200, cookies: this._cookies, - headers: parseHeaders(this.headers), + headers: parsedHeaders, }); this.pipe(this.responseStream); } diff --git a/packages/open-next/src/plugins/edge.ts b/packages/open-next/src/plugins/edge.ts index 2422421c3..98861a7fa 100644 --- a/packages/open-next/src/plugins/edge.ts +++ b/packages/open-next/src/plugins/edge.ts @@ -92,16 +92,8 @@ export function openNextEdgePlugins({ contents = ` globalThis._ENTRIES = {}; globalThis.self = globalThis; -if(!globalThis.process){ - globalThis.process = {env: {}}; -} globalThis._ROUTES = ${JSON.stringify(routes)}; -import {Buffer} from "node:buffer"; -globalThis.Buffer = Buffer; - -import {AsyncLocalStorage} from "node:async_hooks"; -globalThis.AsyncLocalStorage = AsyncLocalStorage; ${ isInCloudfare ? `` diff --git a/packages/open-next/src/types/open-next.ts b/packages/open-next/src/types/open-next.ts index d09120279..5efb27de5 100644 --- a/packages/open-next/src/types/open-next.ts +++ b/packages/open-next/src/types/open-next.ts @@ -49,6 +49,14 @@ export interface DangerousOptions { * @default false */ enableCacheInterception?: boolean; + /** + * Function to determine which headers or cookies takes precedence. + * By default, the middleware headers and cookies will override the handler headers and cookies. + * This is executed for every request and after next config headers and middleware has executed. + */ + headersAndCookiesPriority?: ( + event: InternalEvent, + ) => "middleware" | "handler"; } export type BaseOverride = { @@ -83,6 +91,7 @@ export type Wrapper< > = BaseOverride & { wrapper: WrapperHandler; supportStreaming: boolean; + edgeRuntime?: boolean; }; export type Warmer = BaseOverride & { diff --git a/packages/open-next/src/wrappers/aws-lambda-streaming.ts b/packages/open-next/src/wrappers/aws-lambda-streaming.ts index 891246a38..698940a54 100644 --- a/packages/open-next/src/wrappers/aws-lambda-streaming.ts +++ b/packages/open-next/src/wrappers/aws-lambda-streaming.ts @@ -35,8 +35,6 @@ const handler: WrapperHandler = async (handler, converter) => return; } - let headersWritten = false; - const internalEvent = await converter.convertFrom(event); //Handle compression @@ -84,14 +82,12 @@ const handler: WrapperHandler = async (handler, converter) => "application/vnd.awslambda.http-integration-response", ); _prelude.headers["content-encoding"] = contentEncoding; - // We need to remove the set-cookie header as otherwise it will be set twice, once with the cookies in the prelude, and a second time with the set-cookie headers - delete _prelude.headers["set-cookie"]; + const prelude = JSON.stringify(_prelude); responseStream.write(prelude); responseStream.write(new Uint8Array(8)); - headersWritten = true; return compressedStream ?? responseStream; }, diff --git a/packages/open-next/src/wrappers/cloudflare.ts b/packages/open-next/src/wrappers/cloudflare.ts index a9fcca445..bfeea939e 100644 --- a/packages/open-next/src/wrappers/cloudflare.ts +++ b/packages/open-next/src/wrappers/cloudflare.ts @@ -12,8 +12,16 @@ const handler: WrapperHandler< > = async (handler, converter) => async (event: Request, env: Record): Promise => { - //@ts-expect-error - process is not defined in cloudflare workers - globalThis.process = { env }; + globalThis.process = process; + + // Set the environment variables + // Cloudlare suggests to not override the process.env object but instead apply the values to it + for (const [key, value] of Object.entries(env)) { + if (typeof value === "string") { + process.env[key] = value; + } + } + const internalEvent = await converter.convertFrom(event); const response = await handler(internalEvent); @@ -27,4 +35,5 @@ export default { wrapper: handler, name: "cloudflare", supportStreaming: true, + edgeRuntime: true, }; diff --git a/packages/open-next/src/wrappers/node.ts b/packages/open-next/src/wrappers/node.ts index 2aeff3589..978cd0aef 100644 --- a/packages/open-next/src/wrappers/node.ts +++ b/packages/open-next/src/wrappers/node.ts @@ -10,7 +10,9 @@ const wrapper: WrapperHandler = async (handler, converter) => { const internalEvent = await converter.convertFrom(req); const _res: StreamCreator = { writeHeaders: (prelude) => { + res.setHeader("Set-Cookie", prelude.cookies); res.writeHead(prelude.statusCode, prelude.headers); + res.flushHeaders(); res.uncork(); return res; },