diff --git a/src/servers/sidecar/create-proxy.ts b/src/servers/sidecar/create-proxy.ts index a3984412..6e688646 100644 --- a/src/servers/sidecar/create-proxy.ts +++ b/src/servers/sidecar/create-proxy.ts @@ -28,6 +28,28 @@ export type SidecarProxyParams = { name: string; /** Public mount path, e.g. '/api/wallet'. Stripped from the path before forwarding. */ prefix: string; + /** + * The sub-path this proxy actually forwards, when it is narrower than `prefix`. + * + * ── Why a plugin needs this ── + * + * `registerProxiedPrefix` tells `bodyParser` to leave a prefix's request bodies alone. That is correct + * for forwarded traffic and load-bearing for uploads — a parsed multipart body is re-serialised with a + * new boundary while the original content-type header is still forwarded, and every upload fails. + * + * But a plugin mounts its OWN routes under the same prefix as its proxy, and those routes are not + * forwarded: they are served here and they need their bodies. Claiming the whole prefix means every + * field on them reads as missing. Caldav hit this the day it was extracted — `POST /api/caldav/ + * provision/ios` answered "deviceLabel is required" for a request that carried one — because its app + * passwords used to live at `/api/dav`, a second prefix that was not proxied, and one prefix per + * plugin ended that arrangement. + * + * So a plugin whose sidecar answers one namespace says which: `proxiedSubPath: '/_officer'` claims + * `/_officer` and leaves the rest of the plugin's routes parsed normally. + * + * Omitted means the whole prefix, which is right for every sidecar that has no routes of its own. + */ + proxiedSubPath?: string; /** Extra work when the sidecar reports in. Rare — most sidecars only need the port remembered. */ onRegister?: (port: number) => void; /** @@ -110,6 +132,7 @@ const TIMEOUTS_ON = false; export function createSidecarProxy({ name, prefix, + proxiedSubPath, onRegister, timeoutSeconds, bufferRequestBody, @@ -118,9 +141,10 @@ export function createSidecarProxy({ let serverPort: number | null = null; let producerSecret: string | 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); + // Tells bodyParser to keep its hands off these request bodies. Load-bearing for uploads, not an + // optimisation: see proxied-prefixes.ts. Narrowed to `proxiedSubPath` where a plugin serves its own + // routes under the same prefix — see the field's own comment for what claiming too much costs. + registerProxiedPrefix(proxiedSubPath ? `${prefix}${proxiedSubPath}` : prefix); sidecar.on(`${name}:server`, (msg) => { const { port, secret } = msg as { port?: number; secret?: string };