Skip to content

Commit 0ac604e

Browse files
authored
feat: add a cloudflare-streaming wrapper (#642)
1 parent 4a500ee commit 0ac604e

File tree

8 files changed

+101
-11
lines changed

8 files changed

+101
-11
lines changed

.changeset/ten-trainers-boil.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@opennextjs/aws": minor
3+
---
4+
5+
feat: add a cloudflare-streaming wrapper

packages/open-next/src/build/createServerBundle.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -117,7 +117,6 @@ async function generateBundle(
117117
// `node_modules` inside `.next/standalone`, and others inside
118118
// `.next/standalone/package/path` (ie. `.next`, `server.js`).
119119
// We need to output the handler file inside the package path.
120-
const isMonorepo = monorepoRoot !== appPath;
121120
const packagePath = path.relative(monorepoRoot, appBuildOutputPath);
122121
fs.mkdirSync(path.join(outputPath, packagePath), { recursive: true });
123122

@@ -244,6 +243,7 @@ async function generateBundle(
244243
options,
245244
);
246245

246+
const isMonorepo = monorepoRoot !== appPath;
247247
if (isMonorepo) {
248248
addMonorepoEntrypoint(outputPath, packagePath);
249249
}
@@ -301,7 +301,7 @@ function addMonorepoEntrypoint(outputPath: string, packagePath: string) {
301301
const packagePosixPath = packagePath.split(path.sep).join(path.posix.sep);
302302
fs.writeFileSync(
303303
path.join(outputPath, "index.mjs"),
304-
[`export * from "./${packagePosixPath}/index.mjs";`].join(""),
304+
`export * from "./${packagePosixPath}/index.mjs";`,
305305
);
306306
}
307307

packages/open-next/src/build/validateConfig.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ const compatibilityMatrix: Record<IncludedWrapper, IncludedConverter[]> = {
1717
],
1818
"aws-lambda-streaming": ["aws-apigw-v2"],
1919
cloudflare: ["edge"],
20+
"cloudflare-streaming": ["edge"],
2021
node: ["node"],
2122
dummy: [],
2223
};
@@ -99,7 +100,7 @@ export function validateConfig(config: OpenNextConfig) {
99100
}
100101
if (config.dangerous?.disableTagCache) {
101102
logger.warn(
102-
`You've disabled tag cache.
103+
`You've disabled tag cache.
103104
This means that revalidatePath and revalidateTag from next/cache will not work.
104105
It is safe to disable if you only use page router`,
105106
);

packages/open-next/src/overrides/converters/aws-apigw-v2.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,8 @@ import { debug } from "../../adapters/logger";
1111
import { convertToQuery } from "../../core/routing/util";
1212
import { removeUndefinedFromQuery } from "./utils";
1313

14-
// Not sure which one is reallly needed as this is not documented anywhere but server actions redirect are not working without this, it causes a 500 error from cloudfront itself with a 'x-amzErrortype: InternalFailure' header
14+
// Not sure which one is really needed as this is not documented anywhere but server actions redirect are not working without this,
15+
// it causes a 500 error from cloudfront itself with a 'x-amzErrortype: InternalFailure' header
1516
const CloudFrontBlacklistedHeaders = [
1617
"connection",
1718
"expect",

packages/open-next/src/overrides/converters/edge.ts

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,9 @@ const converter: Converter<
1616
InternalResult | ({ type: "middleware" } & MiddlewareOutputEvent)
1717
> = {
1818
convertFrom: async (event: Request) => {
19-
const searchParams = new URL(event.url).searchParams;
19+
const url = new URL(event.url);
20+
21+
const searchParams = url.searchParams;
2022
const query: Record<string, string | string[]> = {};
2123
for (const [key, value] of searchParams.entries()) {
2224
if (key in query) {
@@ -29,13 +31,13 @@ const converter: Converter<
2931
query[key] = value;
3032
}
3133
}
32-
//Transform body into Buffer
34+
// Transform body into Buffer
3335
const body = await event.arrayBuffer();
3436
const headers: Record<string, string> = {};
3537
event.headers.forEach((value, key) => {
3638
headers[key] = value;
3739
});
38-
const rawPath = new URL(event.url).pathname;
40+
const rawPath = url.pathname;
3941
const method = event.method;
4042
const shouldHaveBody = method !== "GET" && method !== "HEAD";
4143
const cookies: Record<string, string> = Object.fromEntries(
@@ -100,9 +102,10 @@ const converter: Converter<
100102
for (const [key, value] of Object.entries(result.headers)) {
101103
headers.set(key, Array.isArray(value) ? value.join(",") : value);
102104
}
105+
103106
return new Response(result.body as ReadableStream, {
104107
status: result.statusCode,
105-
headers: headers,
108+
headers,
106109
});
107110
},
108111
name: "edge",
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
import type { InternalEvent, InternalResult } from "types/open-next";
2+
import type { WrapperHandler } from "types/overrides";
3+
4+
import { Writable } from "node:stream";
5+
import type { StreamCreator } from "http/index";
6+
import type { MiddlewareOutputEvent } from "../../core/routingHandler";
7+
8+
const handler: WrapperHandler<
9+
InternalEvent,
10+
InternalResult | ({ type: "middleware" } & MiddlewareOutputEvent)
11+
> =
12+
async (handler, converter) =>
13+
async (
14+
request: Request,
15+
env: Record<string, string>,
16+
ctx: any,
17+
): Promise<Response> => {
18+
globalThis.process = process;
19+
globalThis.openNextWaitUntil = ctx.waitUntil.bind(ctx);
20+
21+
// Set the environment variables
22+
// Cloudflare suggests to not override the process.env object but instead apply the values to it
23+
for (const [key, value] of Object.entries(env)) {
24+
if (typeof value === "string") {
25+
process.env[key] = value;
26+
}
27+
}
28+
29+
const internalEvent = await converter.convertFrom(request);
30+
31+
// TODO:
32+
// The edge converter populate event.url with the url including the origin.
33+
// This is required for middleware to keep track of the protocol (i.e. http with wrangler dev).
34+
// However the server expects that the origin is not included.
35+
const url = new URL(internalEvent.url);
36+
(internalEvent.url as string) = url.href.slice(url.origin.length);
37+
38+
const { promise: promiseResponse, resolve: resolveResponse } =
39+
Promise.withResolvers<Response>();
40+
41+
const streamCreator: StreamCreator = {
42+
writeHeaders(prelude: {
43+
statusCode: number;
44+
cookies: string[];
45+
headers: Record<string, string>;
46+
}): Writable {
47+
const { statusCode, cookies, headers } = prelude;
48+
49+
const responseHeaders = new Headers(headers);
50+
for (const cookie of cookies) {
51+
responseHeaders.append("Set-Cookie", cookie);
52+
}
53+
54+
const { readable, writable } = new TransformStream();
55+
const response = new Response(readable, {
56+
status: statusCode,
57+
headers: responseHeaders,
58+
});
59+
resolveResponse(response);
60+
61+
return Writable.fromWeb(writable);
62+
},
63+
onWrite: () => {},
64+
onFinish: (_length: number) => {},
65+
};
66+
67+
ctx.waitUntil(handler(internalEvent, streamCreator));
68+
69+
return promiseResponse;
70+
};
71+
72+
export default {
73+
wrapper: handler,
74+
name: "cloudflare-streaming",
75+
supportStreaming: true,
76+
edgeRuntime: true,
77+
};

packages/open-next/src/overrides/wrappers/cloudflare.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -41,12 +41,14 @@ const handler: WrapperHandler<
4141
// Retrieve geo information from the cloudflare request
4242
// See https://developers.cloudflare.com/workers/runtime-apis/request
4343
// Note: This code could be moved to a cloudflare specific converter when one is created.
44-
const cfProperties = (request as any).cf as Record<string, string | null>;
44+
const cfProperties = (request as any).cf as
45+
| Record<string, string | null>
46+
| undefined;
4547
for (const [propName, headerName] of Object.entries(
4648
cfPropNameToHeaderName,
4749
)) {
48-
const propValue = cfProperties[propName];
49-
if (propValue !== null) {
50+
const propValue = cfProperties?.[propName];
51+
if (propValue != null) {
5052
internalEvent.headers[headerName] = propValue;
5153
}
5254
}

packages/open-next/src/types/open-next.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,7 @@ export type IncludedWrapper =
8181
| "aws-lambda-streaming"
8282
| "node"
8383
| "cloudflare"
84+
| "cloudflare-streaming"
8485
| "dummy";
8586

8687
export type IncludedConverter =

0 commit comments

Comments
 (0)