Skip to content

Commit b88ae13

Browse files
authored
Feat readable body (#486)
* make body a readableStream instead of a string * force non empty when necessry * fix error lambda streaming * fix some issue * changeset
1 parent 1b91708 commit b88ae13

File tree

16 files changed

+146
-70
lines changed

16 files changed

+146
-70
lines changed

.changeset/lucky-dots-obey.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"open-next": minor
3+
---
4+
5+
Replace InternalResult body from string to ReadableStream

packages/open-next/src/adapters/edge-adapter.ts

Lines changed: 12 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
1-
import { InternalEvent, InternalResult } from "types/open-next";
1+
import type { ReadableStream } from "node:stream/web";
2+
3+
import type { InternalEvent, InternalResult } from "types/open-next";
4+
import { emptyReadableStream } from "utils/stream";
25

36
// We import it like that so that the edge plugin can replace it
47
import { NextConfig } from "../adapters/config";
@@ -8,11 +11,14 @@ import {
811
convertToQueryString,
912
} from "../core/routing/util";
1013

14+
declare global {
15+
var isEdgeRuntime: true;
16+
}
17+
1118
const defaultHandler = async (
1219
internalEvent: InternalEvent,
1320
): Promise<InternalResult> => {
14-
// TODO: We need to handle splitted function here
15-
// We should probably create an host resolver to redirect correctly
21+
globalThis.isEdgeRuntime = true;
1622

1723
const host = internalEvent.headers.host
1824
? `https://${internalEvent.headers.host}`
@@ -35,10 +41,6 @@ const defaultHandler = async (
3541
url,
3642
body: convertBodyToReadableStream(internalEvent.method, internalEvent.body),
3743
});
38-
39-
const arrayBuffer = await response.arrayBuffer();
40-
const buffer = Buffer.from(arrayBuffer);
41-
4244
const responseHeaders: Record<string, string | string[]> = {};
4345
response.headers.forEach((value, key) => {
4446
if (key.toLowerCase() === "set-cookie") {
@@ -49,9 +51,9 @@ const defaultHandler = async (
4951
responseHeaders[key] = value;
5052
}
5153
});
52-
// console.log("responseHeaders", responseHeaders);
53-
const body = buffer.toString();
54-
// console.log("body", body);
54+
55+
const body =
56+
(response.body as ReadableStream<Uint8Array>) ?? emptyReadableStream();
5557

5658
return {
5759
type: "core",

packages/open-next/src/adapters/image-optimization-adapter.ts

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import {
1919
// @ts-ignore
2020
import type { NextUrlWithParsedQuery } from "next/dist/server/request-meta";
2121
import { InternalEvent, InternalResult } from "types/open-next.js";
22+
import { emptyReadableStream, toReadableStream } from "utils/stream.js";
2223

2324
import { createGenericHandler } from "../core/createGenericHandler.js";
2425
import { resolveImageLoader } from "../core/resolve.js";
@@ -82,7 +83,7 @@ export async function defaultHandler(
8283
return {
8384
statusCode: 304,
8485
headers: {},
85-
body: "",
86+
body: emptyReadableStream(),
8687
isBase64Encoded: false,
8788
type: "core",
8889
};
@@ -169,7 +170,7 @@ function buildSuccessResponse(
169170
return {
170171
type: "core",
171172
statusCode: 200,
172-
body: result.buffer.toString("base64"),
173+
body: toReadableStream(result.buffer, true),
173174
isBase64Encoded: true,
174175
headers,
175176
};
@@ -191,7 +192,7 @@ function buildFailureResponse(
191192
"Cache-Control": `public,max-age=60,immutable`,
192193
"Content-Type": "application/json",
193194
});
194-
response.end(e?.message || e?.toString() || e);
195+
response.end(e?.message || e?.toString() || "An error occurred");
195196
}
196197
return {
197198
type: "core",
@@ -203,7 +204,7 @@ function buildFailureResponse(
203204
"Cache-Control": `public,max-age=60,immutable`,
204205
"Content-Type": "application/json",
205206
},
206-
body: e?.message || e?.toString() || e,
207+
body: toReadableStream(e?.message || e?.toString() || "An error occurred"),
207208
};
208209
}
209210

packages/open-next/src/converters/aws-apigw-v1.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { APIGatewayProxyEvent, APIGatewayProxyResult } from "aws-lambda";
22
import type { Converter, InternalEvent, InternalResult } from "types/open-next";
3+
import { fromReadableStream } from "utils/stream";
34

45
import { debug } from "../adapters/logger";
56
import { removeUndefinedFromQuery } from "./utils";
@@ -74,9 +75,9 @@ async function convertFromAPIGatewayProxyEvent(
7475
};
7576
}
7677

77-
function convertToApiGatewayProxyResult(
78+
async function convertToApiGatewayProxyResult(
7879
result: InternalResult,
79-
): APIGatewayProxyResult {
80+
): Promise<APIGatewayProxyResult> {
8081
const headers: Record<string, string> = {};
8182
const multiValueHeaders: Record<string, string[]> = {};
8283
Object.entries(result.headers).forEach(([key, value]) => {
@@ -91,10 +92,12 @@ function convertToApiGatewayProxyResult(
9192
}
9293
});
9394

95+
const body = await fromReadableStream(result.body, result.isBase64Encoded);
96+
9497
const response: APIGatewayProxyResult = {
9598
statusCode: result.statusCode,
9699
headers,
97-
body: result.body,
100+
body,
98101
isBase64Encoded: result.isBase64Encoded,
99102
multiValueHeaders,
100103
};

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

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { APIGatewayProxyEventV2, APIGatewayProxyResultV2 } from "aws-lambda";
22
import { parseCookies } from "http/util";
33
import type { Converter, InternalEvent, InternalResult } from "types/open-next";
4+
import { fromReadableStream } from "utils/stream";
45

56
import { debug } from "../adapters/logger";
67
import { convertToQuery } from "../core/routing/util";
@@ -85,9 +86,9 @@ async function convertFromAPIGatewayProxyEventV2(
8586
};
8687
}
8788

88-
function convertToApiGatewayProxyResultV2(
89+
async function convertToApiGatewayProxyResultV2(
8990
result: InternalResult,
90-
): APIGatewayProxyResultV2 {
91+
): Promise<APIGatewayProxyResultV2> {
9192
const headers: Record<string, string> = {};
9293
Object.entries(result.headers)
9394
.filter(
@@ -104,11 +105,13 @@ function convertToApiGatewayProxyResultV2(
104105
headers[key] = Array.isArray(value) ? value.join(", ") : `${value}`;
105106
});
106107

108+
const body = await fromReadableStream(result.body, result.isBase64Encoded);
109+
107110
const response: APIGatewayProxyResultV2 = {
108111
statusCode: result.statusCode,
109112
headers,
110113
cookies: parseCookies(result.headers["set-cookie"]),
111-
body: result.body,
114+
body,
112115
isBase64Encoded: result.isBase64Encoded,
113116
};
114117
debug(response);

packages/open-next/src/converters/aws-cloudfront.ts

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import {
88
import { OutgoingHttpHeader } from "http";
99
import { parseCookies } from "http/util";
1010
import type { Converter, InternalEvent, InternalResult } from "types/open-next";
11+
import { fromReadableStream } from "utils/stream";
1112

1213
import { debug } from "../adapters/logger";
1314
import {
@@ -159,14 +160,18 @@ async function convertToCloudFrontRequestResult(
159160
const serverResponse = createServerResponse(result.internalEvent, {});
160161
await proxyRequest(result.internalEvent, serverResponse);
161162
const externalResult = convertRes(serverResponse);
163+
const body = await fromReadableStream(
164+
externalResult.body,
165+
externalResult.isBase64Encoded,
166+
);
162167
const cloudfrontResult = {
163168
status: externalResult.statusCode.toString(),
164169
statusDescription: "OK",
165170
headers: convertToCloudfrontHeaders(externalResult.headers, true),
166171
bodyEncoding: externalResult.isBase64Encoded
167172
? ("base64" as const)
168173
: ("text" as const),
169-
body: externalResult.body,
174+
body,
170175
};
171176
debug("externalResult", cloudfrontResult);
172177
return cloudfrontResult;
@@ -208,12 +213,14 @@ async function convertToCloudFrontRequestResult(
208213
return response;
209214
}
210215

216+
const body = await fromReadableStream(result.body, result.isBase64Encoded);
217+
211218
const response: CloudFrontRequestResult = {
212219
status: result.statusCode.toString(),
213220
statusDescription: "OK",
214221
headers: convertToCloudfrontHeaders(responseHeaders, true),
215222
bodyEncoding: result.isBase64Encoded ? "base64" : "text",
216-
body: result.body,
223+
body,
217224
};
218225
debug(response);
219226
return response;

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,7 @@ const converter: Converter<
9090
for (const [key, value] of Object.entries(result.headers)) {
9191
headers.set(key, Array.isArray(value) ? value.join(",") : value);
9292
}
93-
return new Response(result.body, {
93+
return new Response(result.body as ReadableStream, {
9494
status: result.statusCode,
9595
headers: headers,
9696
});

packages/open-next/src/converters/node.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ const converter: Converter = {
4444
};
4545
},
4646
// Nothing to do here, it's streaming
47-
convertTo: (internalResult: InternalResult) => ({
47+
convertTo: async (internalResult: InternalResult) => ({
4848
body: internalResult.body,
4949
headers: internalResult.headers,
5050
statusCode: internalResult.statusCode,

packages/open-next/src/core/requestHandler.ts

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import {
77
} from "http/index.js";
88
import { InternalEvent, InternalResult } from "types/open-next";
99
import { DetachedPromiseRunner } from "utils/promise";
10+
import { fromReadableStream } from "utils/stream";
1011

1112
import { debug, error, warn } from "../adapters/logger";
1213
import { convertRes, createServerResponse, proxyRequest } from "./routing/util";
@@ -63,12 +64,22 @@ export async function openNextHandler(
6364
}, {});
6465

6566
if ("type" in preprocessResult) {
66-
// res is used only in the streaming case
67-
const res = createServerResponse(internalEvent, headers, responseStreaming);
68-
res.statusCode = preprocessResult.statusCode;
69-
res.flushHeaders();
70-
res.write(preprocessResult.body);
71-
res.end();
67+
// // res is used only in the streaming case
68+
if (responseStreaming) {
69+
const res = createServerResponse(
70+
internalEvent,
71+
headers,
72+
responseStreaming,
73+
);
74+
res.statusCode = preprocessResult.statusCode;
75+
res.flushHeaders();
76+
const body = await fromReadableStream(
77+
preprocessResult.body,
78+
preprocessResult.isBase64Encoded,
79+
);
80+
res.write(body);
81+
res.end();
82+
}
7283
return preprocessResult;
7384
} else {
7485
const preprocessedEvent = preprocessResult.internalEvent;

packages/open-next/src/core/routing/matcher.ts

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import type {
1414
RouteHas,
1515
} from "types/next-types";
1616
import { InternalEvent, InternalResult } from "types/open-next";
17+
import { emptyReadableStream, toReadableStream } from "utils/stream";
1718

1819
import { debug } from "../../adapters/logger";
1920
import { localizePath } from "./i18n";
@@ -243,8 +244,11 @@ export function handleRewrites<T extends RewriteDefinition>(
243244
};
244245
}
245246

246-
function handleTrailingSlashRedirect(event: InternalEvent) {
247+
function handleTrailingSlashRedirect(
248+
event: InternalEvent,
249+
): false | InternalResult {
247250
const url = new URL(event.url, "http://localhost");
251+
const emptyBody = emptyReadableStream();
248252

249253
if (
250254
// Someone is trying to redirect to a different origin, let's not do that
@@ -270,7 +274,7 @@ function handleTrailingSlashRedirect(event: InternalEvent) {
270274
headersLocation[1] ? `?${headersLocation[1]}` : ""
271275
}`,
272276
},
273-
body: "",
277+
body: emptyBody,
274278
isBase64Encoded: false,
275279
};
276280
// eslint-disable-next-line sonarjs/elseif-without-else
@@ -288,7 +292,7 @@ function handleTrailingSlashRedirect(event: InternalEvent) {
288292
headersLocation[1] ? `?${headersLocation[1]}` : ""
289293
}`,
290294
},
291-
body: "",
295+
body: emptyBody,
292296
isBase64Encoded: false,
293297
};
294298
} else return false;
@@ -311,7 +315,7 @@ export function handleRedirects(
311315
headers: {
312316
Location: internalEvent.url,
313317
},
314-
body: "",
318+
body: emptyReadableStream(),
315319
isBase64Encoded: false,
316320
};
317321
}
@@ -328,7 +332,7 @@ export function fixDataPage(
328332
return {
329333
type: internalEvent.type,
330334
statusCode: 404,
331-
body: "{}",
335+
body: toReadableStream("{}"),
332336
headers: {
333337
"Content-Type": "application/json",
334338
},

0 commit comments

Comments
 (0)