drop the jsonBody workaround — the platform parses these bodies now

this plugin read ctx.req.json() directly because registerProxiedPrefix claimed
the whole of /api/caldav, so bodyParser skipped the plugin's OWN routes and every
field read as missing.

the platform's proxy takes a proxiedSubPath now, so this claims only
/api/caldav/_officer — what the sidecar actually answers — and /passwords and
/provision/ios get their bodies the ordinary way.

verified end to end: POST /api/caldav/passwords with a label came back with the
label, and _officer/collections still reaches the sidecar.

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:53 +00:00
co-authored by Claude Opus 5
parent 4adb925bc7
commit bfb12c2dde
2 changed files with 8 additions and 33 deletions
+2 -32
View File
@@ -1,4 +1,3 @@
import type { Context } from 'hono';
import { createRouter } from '@@/create-router'; import { createRouter } from '@@/create-router';
import { import {
listDavAppPasswords, listDavAppPasswords,
@@ -19,35 +18,6 @@ import { stashIosProfile } from './ios-profile';
export const appPasswordsRouter = createRouter(); export const appPasswordsRouter = createRouter();
/**
* Read this request's JSON body.
*
* `ctx.get('body')` is the platform's parsed body, and it is EMPTY here — deliberately, and for a reason
* that only became true when caldav became a plugin.
*
* `createSidecarProxy` registers `/api/caldav` with `registerProxiedPrefix`, which tells `bodyParser` to
* leave those request streams untouched. That is right for forwarded traffic and load-bearing for
* multipart uploads: a body parsed here is re-serialised with a new boundary while the old content-type
* header is still forwarded, and every upload fails. But the prefix covers this plugin's OWN routes too,
* so they get `{}` and every field reads as missing.
*
* Before the extraction these routes lived under `/api/dav`, a second top-level prefix that was not
* proxied, so their bodies parsed. One prefix per plugin is the rule that ended that arrangement.
*
* Read here rather than fixed in the platform because the platform's rule is correct — the narrower fix
* is for the proxy to register only the sub-path it actually serves (`_officer/*`), which is a change to
* shared machinery every sidecar depends on and not one to make while a phone is waiting.
*/
async function jsonBody<T>(ctx: Context): Promise<T> {
try {
return (await ctx.req.json()) as T;
} catch {
// No body, or not JSON. Every caller below treats a missing field as a bad request anyway.
return {} as T;
}
}
appPasswordsRouter.get('/passwords', async (ctx) => { appPasswordsRouter.get('/passwords', async (ctx) => {
const user = ctx.get('user'); const user = ctx.get('user');
return ctx.json({ passwords: await listDavAppPasswords(user.id) }); return ctx.json({ passwords: await listDavAppPasswords(user.id) });
@@ -55,7 +25,7 @@ appPasswordsRouter.get('/passwords', async (ctx) => {
appPasswordsRouter.post('/passwords', async (ctx) => { appPasswordsRouter.post('/passwords', async (ctx) => {
const user = ctx.get('user'); const user = ctx.get('user');
const body = await jsonBody<{ label?: string }>(ctx); const body = (ctx.get('body') ?? {}) as { label?: string };
const label = body?.label?.trim(); const label = body?.label?.trim();
if (!label) throw errors.BAD_REQUEST('label is required'); if (!label) throw errors.BAD_REQUEST('label is required');
@@ -72,7 +42,7 @@ appPasswordsRouter.post('/passwords', async (ctx) => {
// exists because the alternative is the owner typing a 25-character secret into a phone keyboard twice. // exists because the alternative is the owner typing a 25-character secret into a phone keyboard twice.
appPasswordsRouter.post('/provision/ios', async (ctx) => { appPasswordsRouter.post('/provision/ios', async (ctx) => {
const user = ctx.get('user'); const user = ctx.get('user');
const body = await jsonBody<{ deviceLabel?: string; only?: 'caldav' | 'carddav' }>(ctx); const body = (ctx.get('body') ?? {}) as { deviceLabel?: string; only?: 'caldav' | 'carddav' };
const deviceLabel = body?.deviceLabel?.trim(); const deviceLabel = body?.deviceLabel?.trim();
if (!deviceLabel) throw errors.BAD_REQUEST('deviceLabel is required'); if (!deviceLabel) throw errors.BAD_REQUEST('deviceLabel is required');
+6 -1
View File
@@ -11,7 +11,12 @@ import { createSidecarProxy } from '@@/sidecar/create-proxy';
// //
// Hence one registration, two forwarders. The factory's own comment says it must never grow per-app // Hence one registration, two forwarders. The factory's own comment says it must never grow per-app
// logic, so the DAV-specific half is a separate file rather than a branch inside it. // logic, so the DAV-specific half is a separate file rather than a branch inside it.
const proxy = createSidecarProxy({ name: 'caldav', prefix: '/api/caldav' }); // `proxiedSubPath`: this sidecar answers `_officer/*` and nothing else, so the proxy claims only that.
//
// Claiming the whole prefix is what stopped this plugin's OWN routes from seeing a request body —
// `registerProxiedPrefix` tells bodyParser to leave the prefix alone, which is right for forwarded
// traffic and wrong for `/passwords` and `/provision/ios`, which are served here.
const proxy = createSidecarProxy({ name: 'caldav', prefix: '/api/caldav', proxiedSubPath: '/_officer' });
export const caldavRouter = proxy.router; export const caldavRouter = proxy.router;
export const getCaldavUrl = proxy.getHttpUrl; export const getCaldavUrl = proxy.getHttpUrl;