Skip to content

Commit 5b7a07b

Browse files
authored
Merge pull request #287 from framesjs/fix/pages-router-support
fix: pages router support
2 parents 7b9caab + 597c848 commit 5b7a07b

File tree

11 files changed

+508
-169
lines changed

11 files changed

+508
-169
lines changed

.changeset/mean-dolls-hope.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"frames.js": patch
3+
---
4+
5+
fix: page router adapter for next.js node.js request handling
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import { createFrames } from "frames.js/next/pages-router";
2+
3+
export const frames = createFrames({
4+
basePath: "/api/frames",
5+
});

examples/framesjs-starter/pages/api/frames.tsx examples/framesjs-starter/pages/api/frames/index.tsx

+5-6
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,7 @@
11
/* eslint-disable react/jsx-key */
2-
import { createFrames, Button } from "frames.js/next/pages-router";
2+
import { Button } from "frames.js/next/pages-router";
3+
import { frames } from "./frames";
34

4-
const frames = createFrames({
5-
basePath: "/api/frames",
6-
});
75
const handleRequest = frames(async (ctx) => {
86
return {
97
image: (
@@ -13,8 +11,9 @@ const handleRequest = frames(async (ctx) => {
1311
</span>
1412
),
1513
buttons: [
16-
<Button action="post" target="/">
17-
Click me
14+
<Button action="post">Click me</Button>,
15+
<Button action="post" target="/next">
16+
Next frame
1817
</Button>,
1918
],
2019
textInput: "Type something!",
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
/* eslint-disable react/jsx-key */
2+
import { Button } from "frames.js/next/pages-router";
3+
import { frames } from "../frames";
4+
5+
const handleRequest = frames(async (ctx) => {
6+
return {
7+
image: (
8+
<span>
9+
This is next frame and you clicked button:{" "}
10+
{ctx.pressedButton ? "✅" : "❌"}
11+
</span>
12+
),
13+
buttons: [
14+
<Button action="post" target="/">
15+
Previous frame
16+
</Button>,
17+
],
18+
};
19+
});
20+
21+
export default handleRequest;

packages/frames.js/src/express/index.test.tsx

+30-7
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import express from "express";
1+
import express, { json } from "express";
22
import request from "supertest";
33
import { FRAMES_META_TAGS_HEADER } from "../core";
44
import * as lib from ".";
@@ -96,13 +96,13 @@ describe("express adapter", () => {
9696
.expect(200)
9797
.expect("Content-type", "application/json")
9898
.expect((res) => {
99-
expect((res.body as Record<string, string>)["fc:frame:button:1:target"]).toMatch(
100-
/http:\/\/127\.0\.0\.1:\d+\/api\/nested/
101-
);
99+
expect(
100+
(res.body as Record<string, string>)["fc:frame:button:1:target"]
101+
).toMatch(/http:\/\/127\.0\.0\.1:\d+\/api\/nested/);
102102
});
103103
});
104104

105-
it('works properly with state', async () => {
105+
it("works properly with state", async () => {
106106
type State = {
107107
test: boolean;
108108
};
@@ -117,15 +117,38 @@ describe("express adapter", () => {
117117
expect(ctx.state).toEqual({ test: false });
118118

119119
return {
120-
image: 'http://test.png',
120+
image: "http://test.png",
121121
state: ctx.state satisfies State,
122122
};
123123
});
124124

125125
app.use("/", expressHandler);
126126

127+
await request(app).get("/").expect("Content-Type", "text/html").expect(200);
128+
});
129+
130+
it("works properly with body parser", async () => {
131+
const app = express();
132+
const frames = lib.createFrames();
133+
const expressHandler = frames(async ({ request: req }) => {
134+
await expect(req.clone().json()).resolves.toEqual({ test: "test" });
135+
136+
return {
137+
image: <span>Nehehe</span>,
138+
buttons: [
139+
<lib.Button action="post" key="1">
140+
Click me
141+
</lib.Button>,
142+
],
143+
};
144+
});
145+
146+
app.use("/", json(), expressHandler);
147+
127148
await request(app)
128-
.get("/")
149+
.post("/")
150+
.set("Host", "localhost:3000")
151+
.send({ test: "test" })
129152
.expect("Content-Type", "text/html")
130153
.expect(200);
131154
});
+10-74
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,15 @@
1-
import type { IncomingHttpHeaders } from "node:http";
21
import type {
32
Handler as ExpressHandler,
43
Request as ExpressRequest,
54
Response as ExpressResponse,
65
} from "express";
76
import type { types } from "../core";
87
import { createFrames as coreCreateFrames } from "../core";
9-
import {
10-
createReadableStreamFromReadable,
11-
writeReadableStreamToWritable,
12-
} from "../lib/stream-pump";
138
import type { CoreMiddleware } from "../middleware";
9+
import {
10+
convertNodeJSRequestToWebAPIRequest,
11+
sendWebAPIResponseToNodeJSResponse,
12+
} from "../lib/node-server-helpers";
1413

1514
export { Button, type types } from "../core";
1615

@@ -60,10 +59,14 @@ export const createFrames: CreateFramesForExpress =
6059
res: ExpressResponse
6160
) {
6261
// convert express.js req to Web API Request
63-
const response = framesHandler(createRequest(req, res));
62+
const response = framesHandler(
63+
convertNodeJSRequestToWebAPIRequest(req, res)
64+
);
6465

6566
Promise.resolve(response)
66-
.then((resolvedResponse) => sendResponse(res, resolvedResponse))
67+
.then((resolvedResponse) =>
68+
sendWebAPIResponseToNodeJSResponse(res, resolvedResponse)
69+
)
6770
.catch((error) => {
6871
// eslint-disable-next-line no-console -- provide feedback
6972
console.error(error);
@@ -73,70 +76,3 @@ export const createFrames: CreateFramesForExpress =
7376
};
7477
};
7578
};
76-
77-
function createRequest(req: ExpressRequest, res: ExpressResponse): Request {
78-
// req.hostname doesn't include port information so grab that from
79-
// `X-Forwarded-Host` or `Host`
80-
const [, hostnamePort] = req.get("X-Forwarded-Host")?.split(":") ?? [];
81-
const [, hostPort] = req.get("Host")?.split(":") ?? [];
82-
const port = hostnamePort || hostPort;
83-
// Use req.hostname here as it respects the "trust proxy" setting
84-
const resolvedHost = `${req.hostname}${port ? `:${port}` : ""}`;
85-
// Use `req.originalUrl` so Remix is aware of the full path
86-
const url = new URL(`${req.protocol}://${resolvedHost}${req.originalUrl}`);
87-
88-
// Abort action/loaders once we can no longer write a response
89-
const controller = new AbortController();
90-
res.on("close", () => {
91-
controller.abort();
92-
});
93-
94-
const init: RequestInit = {
95-
method: req.method,
96-
headers: convertIncomingHTTPHeadersToHeaders(req.headers),
97-
signal: controller.signal,
98-
};
99-
100-
if (req.method !== "GET" && req.method !== "HEAD") {
101-
init.body = createReadableStreamFromReadable(req);
102-
(init as { duplex: "half" }).duplex = "half";
103-
}
104-
105-
return new Request(url.href, init);
106-
}
107-
108-
function convertIncomingHTTPHeadersToHeaders(
109-
incomingHeaders: IncomingHttpHeaders
110-
): Headers {
111-
const headers = new Headers();
112-
113-
for (const [key, value] of Object.entries(incomingHeaders)) {
114-
if (Array.isArray(value)) {
115-
for (const item of value) {
116-
headers.append(key, item);
117-
}
118-
} else if (value != null) {
119-
headers.append(key, value);
120-
}
121-
}
122-
123-
return headers;
124-
}
125-
126-
async function sendResponse(
127-
res: ExpressResponse,
128-
response: Response
129-
): Promise<void> {
130-
res.statusMessage = response.statusText;
131-
res.status(response.status);
132-
133-
for (const [key, value] of response.headers.entries()) {
134-
res.setHeader(key, value);
135-
}
136-
137-
if (response.body) {
138-
await writeReadableStreamToWritable(response.body, res);
139-
} else {
140-
res.end();
141-
}
142-
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
import { IncomingMessage, ServerResponse } from "node:http";
2+
import { Socket } from "node:net";
3+
import { TLSSocket } from "node:tls";
4+
import {
5+
convertNodeJSRequestToWebAPIRequest,
6+
sendWebAPIResponseToNodeJSResponse,
7+
} from "./node-server-helpers";
8+
9+
describe("convertNodeJSRequestToWebAPIRequest", () => {
10+
it("properly detects url from host header (with custom port)", () => {
11+
const req = new IncomingMessage(new Socket());
12+
req.headers.host = "framesjs.org:3000";
13+
req.url = "/test";
14+
15+
const res = new ServerResponse(req);
16+
const request = convertNodeJSRequestToWebAPIRequest(req, res);
17+
18+
expect(request.url).toBe("http://framesjs.org:3000/test");
19+
});
20+
21+
it("properly detects url from host header (without port)", () => {
22+
const req = new IncomingMessage(new Socket());
23+
req.headers.host = "framesjs.org";
24+
req.url = "/test";
25+
26+
const res = new ServerResponse(req);
27+
const request = convertNodeJSRequestToWebAPIRequest(req, res);
28+
29+
expect(request.url).toBe("http://framesjs.org/test");
30+
});
31+
32+
it("properly detects protocol from socket", () => {
33+
const socket = new TLSSocket(new Socket());
34+
const req = new IncomingMessage(socket);
35+
req.headers.host = "framesjs.org";
36+
req.url = "/test";
37+
38+
const res = new ServerResponse(req);
39+
const request = convertNodeJSRequestToWebAPIRequest(req, res);
40+
41+
expect(request.url).toBe("https://framesjs.org/test");
42+
});
43+
44+
it("uses x-forwarded-host if available", () => {
45+
const req = new IncomingMessage(new Socket());
46+
req.headers["x-forwarded-host"] = "framesjs.org:3000";
47+
req.url = "/test";
48+
49+
const res = new ServerResponse(req);
50+
const request = convertNodeJSRequestToWebAPIRequest(req, res);
51+
52+
expect(request.url).toBe("http://framesjs.org:3000/test");
53+
});
54+
55+
it("uses x-forwarded-host over host header", () => {
56+
const req = new IncomingMessage(new Socket());
57+
req.headers["x-forwarded-host"] = "framesjs.org:3000";
58+
req.headers.host = "framesjs.org";
59+
req.url = "/test";
60+
61+
const res = new ServerResponse(req);
62+
const request = convertNodeJSRequestToWebAPIRequest(req, res);
63+
64+
expect(request.url).toBe("http://framesjs.org:3000/test");
65+
});
66+
67+
it("uses x-forwarded-proto if available", () => {
68+
const req = new IncomingMessage(new Socket());
69+
70+
req.headers["x-forwarded-proto"] = "https";
71+
req.headers.host = "framesjs.org";
72+
req.url = "/test";
73+
74+
const res = new ServerResponse(req);
75+
const request = convertNodeJSRequestToWebAPIRequest(req, res);
76+
77+
expect(request.url).toBe("https://framesjs.org/test");
78+
});
79+
80+
it("uses x-forwarded-proto over encrypted socket", () => {
81+
const socket = new TLSSocket(new Socket());
82+
const req = new IncomingMessage(socket);
83+
84+
req.headers["x-forwarded-proto"] = "http";
85+
req.headers.host = "framesjs.org";
86+
req.url = "/test";
87+
88+
const res = new ServerResponse(req);
89+
const request = convertNodeJSRequestToWebAPIRequest(req, res);
90+
91+
expect(request.url).toBe("http://framesjs.org/test");
92+
});
93+
94+
it("passes all headers to Request", () => {
95+
const req = new IncomingMessage(new Socket());
96+
req.headers.host = "framesjs.org";
97+
req.url = "/test";
98+
req.headers["x-test"] = "test";
99+
req.headers["content-type"] = "test";
100+
101+
const res = new ServerResponse(req);
102+
const request = convertNodeJSRequestToWebAPIRequest(req, res);
103+
104+
expect(request.headers.get("x-test")).toBe("test");
105+
expect(request.headers.get("content-type")).toBe("test");
106+
});
107+
});
108+
109+
describe("sendWebAPIResponseToNodeJSResponse", () => {
110+
it("sends response with headers to node.js response", async () => {
111+
const response = Response.json(
112+
{ test: "test" },
113+
{ headers: { "x-test": "test" }, statusText: "OK" }
114+
);
115+
const res = new ServerResponse(new IncomingMessage(new Socket()));
116+
117+
await sendWebAPIResponseToNodeJSResponse(res, response);
118+
119+
expect(res.statusCode).toBe(200);
120+
expect(res.statusMessage).toBe("OK");
121+
expect(res.getHeader("content-type")).toBe("application/json");
122+
});
123+
});

0 commit comments

Comments
 (0)