History

Rebuild the stamped auth request with an explicit body

Rhys Sullivan committed 1 month ago

Commit d486c2b1 parent 0ff9d4f2 files · +57 −1Browse files

Unified · Split

2 files
  1. apps/host-selfhost/src/auth/client-ip.test.ts +32 −0
  2. apps/host-selfhost/src/auth/client-ip.ts +25 −1
apps/host-selfhost/src/auth/client-ip.test.ts+32−0
@@ -93,6 +93,38 @@ describe("makeClientIpStamper without a trusted proxy", () => { · 92 unmodified lines
9393 expect(out.headers.get("x-forwarded-for")).toBe("203.0.113.9");
9494 expect(await out.json()).toEqual({ email: "a@example.com", password: "pw" });
9595 });
96+
97+ // The Vite dev middleware builds the request over a stream the runtime did
98+ // not create itself (`Readable.toWeb(req)`); the stamped request must still
99+ // deliver it in full.
100+ it("passes a streamed body through", async () => {
101+ const payload = JSON.stringify({ email: "a@example.com", password: "pw" });
102+ const init: RequestInit & { duplex?: "half" } = {
103+ method: "POST",
104+ headers: { "content-type": "application/json" },
105+ body: new ReadableStream<Uint8Array>({
106+ start(controller) {
107+ controller.enqueue(new TextEncoder().encode(payload));
108+ controller.close();
109+ },
110+ }),
111+ duplex: "half",
112+ };
113+ const streamed = new Request("https://host.example/api/auth/sign-in/email", init);
114+ const out = stamp(streamed, Option.some("198.51.100.4"));
115+ expect(out.headers.get(CLIENT_IP_HEADER)).toBe("198.51.100.4");
116+ expect(await out.text()).toBe(payload);
117+ });
118+
119+ it("stamps a bodiless request without inventing a body", () => {
120+ const out = stamp(
121+ new Request("https://host.example/api/auth/get-session", { method: "GET" }),
122+ Option.some("198.51.100.4"),
123+ );
124+ expect(out.method).toBe("GET");
125+ expect(out.body).toBeNull();
126+ expect(out.headers.get(CLIENT_IP_HEADER)).toBe("198.51.100.4");
127+ });
96128 });
97129
98130 describe("makeClientIpStamper behind a trusted proxy", () => {
apps/host-selfhost/src/auth/client-ip.ts+25−1
@@ -107,6 +107,30 @@ export const clientIpAddressOptions = ( · 106 unmodified lines
107107 }
108108 : { ipAddressHeaders: [CLIENT_IP_HEADER] };
109109
110+/**
111+ * The request with its headers replaced, everything else passed through.
112+ *
113+ * Deliberately NOT `new Request(request, { headers })`: Bun's copy constructor
114+ * never delivers a body that is not one of its own native streams, and the
115+ * Vite dev middleware hands the handler `Readable.toWeb(req)`, so every
116+ * sign-in POST hung until the client gave up (the production Bun.serve path
117+ * was unaffected). Rebuilding from the parts with the body passed explicitly
118+ * behaves the same on both paths; `duplex: "half"` is what the Fetch spec
119+ * requires when a request body is a stream.
120+ */
121+const withHeaders = (request: Request, headers: Headers): Request => {
122+ const init: RequestInit & { duplex?: "half" } = {
123+ method: request.method,
124+ headers,
125+ signal: request.signal,
126+ };
127+ if (request.body) {
128+ init.body = request.body;
129+ init.duplex = "half";
130+ }
131+ return new Request(request.url, init);
132+};
133+
110134 export interface ClientIpStamperOptions {
111135 /** Where the one-time unconfigured-proxy warning goes. Defaults to console.warn. */
112136 readonly warn?: (message: string) => void;
@@ -153,6 +177,6 @@ export const makeClientIpStamper = ( · 40 unmodified lines
153177 warn(unconfiguredProxyWarning(hint));
154178 }
155179 }
156− return new Request(request, { headers });
180+ return withHeaders(request, headers);
157181 };
158182 };

Keyboard shortcuts

General

Show keyboard shortcuts
?
Focus the current search
/

Issues and pull requests

Open filters
f

Issues, pull requests and Inbox

Select next item
j↓
Select previous item
k↑
Open selected item
Enter
Dismiss filters or clear selection
Esc

Repository code

Go to file
t