setting up a phone should be one link inside officer — open Calendar, the screen already knows this phone is not set up, click, done. the endpoint for it exists (POST /api/caldav/provision/ios) and nothing in the UI calls it, so the smooth path is built and hidden while the path a new person walks has four fields. it also does not currently work: the profile is refused by iOS 26.6 as "Invalid Profile". the todo records what was RULED OUT with evidence rather than reasoning, because that is the expensive part to redo — delivery works, the phone really received it (the claim is logged now), the server side is correct, and removing PayloadScope did not fix it. signing is untested and is the first experiment. second: a plugin with a sidecar proxy loses the request body on its own routes. createSidecarProxy registers the whole mount prefix and bodyParser skips it, which is right for forwarded traffic and load-bearing for uploads — but it covers the plugin's own routes too. caldav works around it locally; the real fix is the proxy claiming only the sub-path it serves, which is shared machinery and not a thing to change with a phone waiting.
2.3 KiB
id, title, theme, status, created, date_source, order
| id | title | theme | status | created | date_source | order |
|---|---|---|---|---|---|---|
| plugin-infrastructure/a-plugin-s-own-routes-lose-their-request-body | A plugin that has a sidecar proxy loses the request body on its OWN routes | plugin-infrastructure | open | 2026-08-27 | stated | 9 |
createSidecarProxy registers the plugin's whole mount prefix with registerProxiedPrefix, and
bodyParser then skips every request under it — including routes the plugin serves itself. Those
routes get ctx.get('body') === {} and every field reads as missing.
Found on 2026-08-27 extracting caldav: POST /api/caldav/provision/ios answered
{"error":"deviceLabel is required"} for a request that carried one. It had never happened before
because app passwords lived at /api/dav — a second top-level prefix that was not proxied — and one
prefix per plugin is the rule that ended that arrangement.
The skip itself is correct and must not be removed. Parsing a body that is only passing through is
wasted for JSON and corrupting for multipart/form-data: Hono caches the parsed FormData and
re-serialises it with a NEW boundary while the proxy forwards the ORIGINAL content-type header naming
the old one, so every upload fails with "Multipart: Unexpected end of form". That is why the mechanism
exists.
The narrow fix is that the proxy should register only the sub-path it actually serves. Caldav's
sidecar answers _officer/* and nothing else, so /api/caldav/_officer is the honest claim; the rest of
the prefix is the plugin's own and should be parsed normally. That means createSidecarProxy taking the
proxied sub-path rather than assuming the whole mount.
Not done on the night it was found: it is shared machinery every sidecar depends on, and the change was
diagnosed while a phone was waiting. The caldav plugin works around it locally — jsonBody() in
api/app-passwords.ts reads ctx.req.json() directly, with the reasoning inline.
Who else is affected: every plugin that mixes its own routes with a sidecar proxy under one prefix, which is most of them. They have not hit it because their own routes are GETs, or because their writes go to the sidecar. A plugin adding its first non-proxied POST will hit it, and the symptom — a required field reported missing on a request that plainly has it — points at the caller rather than at this.