Unified · Split
| @@ -93,6 +93,38 @@ describe("makeClientIpStamper without a trusted proxy", () => { · 92 unmodified lines | |||
| 93 | expect(out.headers.get("x-forwarded-for")).toBe("203.0.113.9"); | 93 | expect(out.headers.get("x-forwarded-for")).toBe("203.0.113.9"); |
| 94 | expect(await out.json()).toEqual({ email: "a@example.com", password: "pw" }); | 94 | expect(await out.json()).toEqual({ email: "a@example.com", password: "pw" }); |
| 95 | }); | 95 | }); |
| 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 | }); | ||
| 96 | }); | 128 | }); |
| 97 | 129 | ||
| 98 | describe("makeClientIpStamper behind a trusted proxy", () => { | 130 | describe("makeClientIpStamper behind a trusted proxy", () => { |
| @@ -107,6 +107,30 @@ export const clientIpAddressOptions = ( · 106 unmodified lines | |||
| 107 | } | 107 | } |
| 108 | : { ipAddressHeaders: [CLIENT_IP_HEADER] }; | 108 | : { ipAddressHeaders: [CLIENT_IP_HEADER] }; |
| 109 | 109 | ||
| 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 | |||
| 110 | export interface ClientIpStamperOptions { | 134 | export interface ClientIpStamperOptions { |
| 111 | /** Where the one-time unconfigured-proxy warning goes. Defaults to console.warn. */ | 135 | /** Where the one-time unconfigured-proxy warning goes. Defaults to console.warn. */ |
| 112 | readonly warn?: (message: string) => void; | 136 | readonly warn?: (message: string) => void; |
| @@ -153,6 +177,6 @@ export const makeClientIpStamper = ( · 40 unmodified lines | |||
| 153 | warn(unconfiguredProxyWarning(hint)); | 177 | warn(unconfiguredProxyWarning(hint)); |
| 154 | } | 178 | } |
| 155 | } | 179 | } |
| 156 | return new Request(request, { headers }); | 180 | return withHeaders(request, headers); |
| 157 | }; | 181 | }; |
| 158 | }; | 182 | }; |