Repository navigation
Description
createBackendProxyHandler is fine for JSON APIs but cannot carry binary payloads. Three defects,
all in the same function (src/server/route-handlers.ts, v0.7.0):
const headers = new Headers();
const contentType = req.headers.get("Content-Type");
if (contentType) headers.set("Content-Type", contentType);
const accept = req.headers.get("Accept");
if (accept) headers.set("Accept", accept);
if (session?.accessToken) {
headers.set("Authorization", `Bearer ${session.accessToken}`);
}
const hasBody = req.method !== "GET" && req.method !== "HEAD";
const response = await fetch(url.toString(), {
method: req.method,
headers,
body: hasBody ? await req.arrayBuffer() : undefined,
});1. The request body is fully buffered in memory
await req.arrayBuffer() materialises the entire upload in the Next.js container's heap before a
single byte reaches the backend. For a JSON payload that is irrelevant; for a file upload it is the
difference between a constant-memory proxy and one whose peak memory equals the largest file a user
can send. A 500 MB audio import means 500 MB resident, per concurrent upload.
Suggested fix — stream the body instead:
const response = await fetch(url.toString(), {
method: req.method,
headers,
body: hasBody ? req.body : undefined,
// @ts-expect-error — undici requires this when body is a stream
duplex: "half",
});duplex: "half" is mandatory in undici (Node's fetch) whenever body is a ReadableStream;
without it the call throws. Worth a comment in the code, because it looks removable and is not.
2. Range is not forwarded
Only Content-Type and Accept are copied. A client asking for a byte range gets the whole
resource, so range requests are effectively unsupported end to end even when the backend implements
them properly. This breaks seeking in <audio> and <video> elements: browsers seek by issuing
Range requests, and without them the player must download the entire file before it can jump.
The response direction already works — new Response(response.body, { headers: response.headers })
streams and passes Accept-Ranges / Content-Range through — so only the request direction is
missing.
Suggested fix: forward Range, and alongside it the conditional headers that belong to the same
mechanism: If-Range, If-None-Match, If-Modified-Since.
3. No way to forward application-specific headers
The header handling is a fixed two-entry allowlist. An application that needs to send anything else
(an idempotency key, a client-generated id, a checksum) has no way to do it through the proxy and
must move the value into the path or the body.
Suggested fix: an optional forwardRequestHeaders?: readonly string[] in BackendProxyConfig,
merged with the built-in list.
Deliberately not "forward everything". The current allowlist is doing real security work: it
keeps Cookie out of the upstream request, so the Auth.js session cookie never reaches the backend,
which authenticates by bearer token only. A blanket forward would also pass Host,
Content-Length (wrong after re-encoding) and hop-by-hop headers. The fix should extend the
allowlist, not remove it — and that reasoning is worth a comment so the next change does not
"simplify" it away.
Impact
- open-crm / open-tasks: no effect. Both send JSON only; the buffering is invisible at their
payload sizes and neither serves ranged media. - open-transcript: blocking. The application uploads meeting audio and streams it back to an
<audio>element. Its backend streams to and from S3-compatible storage with constant memory
precisely so that hour-long recordings are possible — a proxy that buffers in front of it defeats
the design, and withoutRangethe player cannot seek.
Acceptance
- a request body of arbitrary size passes through with the proxy's memory staying flat
-
Rangeon a request produces206 Partial Contentwith the expectedContent-Range -
forwardRequestHeadersadds the named headers and nothing else -
CookieandHostare still never forwarded - a regression test asserts the
Cookieexclusion, so the allowlist cannot be widened by
accident
Found while specifying the upload and management API for open-transcript.
Activity
herbie-bot commentedon Sep 3, 2026
A fix is up in #11.
It streams the request body (
req.body+duplex: "half") instead of buffering viaarrayBuffer(), always forwards the range/conditional headers (Range,If-Range,If-Match,If-None-Match,If-Modified-Since,If-Unmodified-Since), and adds an optional case-insensitiveforwardRequestHeadersallowlist with a hard deny-set (Cookie,Host,Authorization,Content-Length,Connection). Includes tests covering streaming,206passthrough, custom-header forwarding, and theCookie/Hostexclusion regression.Merge pull request #11 from OpenElementsLabs/fix/proxy-stream-body-an…