diff --git a/server/src/index.ts b/server/src/index.ts index d108839..439365b 100644 --- a/server/src/index.ts +++ b/server/src/index.ts @@ -38,6 +38,16 @@ export function buildServer() { // visitor shares one rate-limit bucket. Trust the proxy's X-Forwarded-For so // req.ip is the real client. Only Traefik can reach the container, so this is safe. const app = Fastify({ bodyLimit: BODY_LIMIT, trustProxy: true }); + // Tolerate a body-less request that still sets content-type: application/json + // (browsers/fetch wrappers often do this for DELETE/POST-with-no-body). The + // default parser 400s on an empty JSON body; every handler treats a missing + // body as {}, so parse empty → undefined instead. + app.addContentTypeParser('application/json', { parseAs: 'string' }, (_req, body, done) => { + const s = (body as string).trim(); + if (!s) return done(null, undefined); + try { done(null, JSON.parse(s)); } + catch { const err = new Error('Invalid JSON') as Error & { statusCode?: number }; err.statusCode = 400; done(err, undefined); } + }); const hub = new RoomHub(); const accounts = new AccountStore(DATA_DIR, undefined, ADMIN_USERS, MAX_USERS); void accounts.load(); diff --git a/src/lib/cloud/campaigns.ts b/src/lib/cloud/campaigns.ts index 92d284f..d5e9015 100644 --- a/src/lib/cloud/campaigns.ts +++ b/src/lib/cloud/campaigns.ts @@ -16,7 +16,11 @@ export interface CloudCharInfo { id: string; name: string; ownerUserId: string; export async function req(path: string, init?: RequestInit): Promise { let res: Response; try { - res = await fetch(`${base()}${path}`, { ...init, headers: { 'content-type': 'application/json', ...authHeaders(), ...(init?.headers ?? {}) } }); + // Only declare a JSON content-type when we actually send a body — Fastify + // rejects an empty body that claims content-type: application/json, which + // broke the body-less DELETE/revoke/rotate calls. + const jsonType = init?.body != null ? { 'content-type': 'application/json' } : {}; + res = await fetch(`${base()}${path}`, { ...init, headers: { ...jsonType, ...authHeaders(), ...(init?.headers ?? {}) } }); } catch { throw new CloudError('Could not reach the server.'); }