stop parsing request bodies on sidecar proxy routes
every multipart upload through /api/<sidecar>/* arrived corrupted. bodyParser ran on
proxy routes and called parseBody for multipart, so hono cached a FormData on the
request; when the proxy then asked for the bytes hono re-serialised them from that
cache with a NEW boundary, while the proxy still forwarded the ORIGINAL content-type
header. header and body disagreed and the far side rejected it with
"Multipart: Unexpected end of form".
bodyParser now skips prefixes owned by createSidecarProxy, which register themselves
so a new sidecar cannot forget. the proxy also forwards the body as a stream instead
of buffering it, which drops the second in-memory copy of every upload.
note the bug report proposed skipping multipart in bodyParser outright; that would
have broken /upload, /file-browser upload and /bug-report, which do read a multipart
body from ctx.get('body').
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -2,6 +2,7 @@ import type { Hono } from 'hono';
|
||||
import type { HonoVariables } from '../create-router';
|
||||
import { createRouter } from '../create-router';
|
||||
import * as sidecar from '../sidecar-registry';
|
||||
import { registerProxiedPrefix } from './proxied-prefixes';
|
||||
|
||||
// The shared auth-and-forward proxy every HTTP sidecar needs.
|
||||
//
|
||||
@@ -48,6 +49,10 @@ export type SidecarProxy = {
|
||||
export function createSidecarProxy({ name, prefix, onRegister, timeoutSeconds }: SidecarProxyParams): SidecarProxy {
|
||||
let serverPort: number | null = null;
|
||||
|
||||
// Tells bodyParser to keep its hands off this prefix's request bodies. Load-bearing for uploads, not an
|
||||
// optimisation: see proxied-prefixes.ts.
|
||||
registerProxiedPrefix(prefix);
|
||||
|
||||
sidecar.on(`${name}:server`, (msg) => {
|
||||
const port = (msg as { port?: number }).port;
|
||||
if (typeof port !== 'number') return;
|
||||
@@ -95,13 +100,22 @@ export function createSidecarProxy({ name, prefix, onRegister, timeoutSeconds }:
|
||||
|
||||
const hasBody = method !== 'GET' && method !== 'HEAD';
|
||||
|
||||
// Forwarded as a STREAM, not a buffer. Buffering held the whole body in memory here on top of the copy
|
||||
// the sidecar holds — two copies of a 4K video per upload — and bought nothing, since this layer never
|
||||
// looks at the bytes. `duplex: 'half'` is required by the fetch spec whenever the body is a stream; it
|
||||
// is absent from TypeScript's RequestInit, hence the widened type.
|
||||
// This is only safe because bodyParser skips proxied prefixes: a body consumed upstream would arrive
|
||||
// here as an already-locked stream.
|
||||
const init: RequestInit & { duplex?: 'half' } = {
|
||||
method,
|
||||
headers,
|
||||
body: hasBody ? ctx.req.raw.body : undefined,
|
||||
duplex: 'half',
|
||||
};
|
||||
|
||||
let upstream: Response;
|
||||
try {
|
||||
upstream = await fetch(target, {
|
||||
method,
|
||||
headers,
|
||||
body: hasBody ? await ctx.req.arrayBuffer() : undefined,
|
||||
});
|
||||
upstream = await fetch(target, init);
|
||||
} catch (err) {
|
||||
// Target path only — never the body, which may carry a passphrase or a credential.
|
||||
console.error(`[${name}] proxy fetch failed`, { target, error: String(err) });
|
||||
|
||||
Reference in New Issue
Block a user