a sidecar proxy claims only the sub-path it forwards

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, and those are not
forwarded — they are served in-process and they need their bodies. claiming the
whole prefix meant every field on them read as missing.

caldav hit it the day it was extracted: POST /api/caldav/provision/ios answered
"deviceLabel is required" for a request that carried one. 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 `proxiedSubPath` — caldav's sidecar answers `_officer/*` and nothing else, so
it claims `/api/caldav/_officer` and the plugin's own routes are parsed normally.
omitted means the whole prefix, which is right for every sidecar with no routes
of its own, so nothing else changes.

this affects any plugin mixing its own routes with a proxy, which is most of
them. they have not hit it because their own routes are GETs.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Officer-Session: 656f5c85-e49a-4f1f-b9f3-042a04b4d043
This commit is contained in:
2026-08-27 08:48:40 +00:00
co-authored by Claude Opus 5
parent 6762a32ea6
commit f00abe816e
+27 -3
View File
@@ -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
* `<prefix>/_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 };