Fix empty-body JSON error on body-less admin requests
The shared cloud req() helper set content-type: application/json on every request, so body-less DELETE/revoke/rotate calls tripped Fastify's "Body cannot be empty" 400. Only send the header when there's a body, and (defensively) make the server parse an empty application/json body as undefined instead of erroring. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -38,6 +38,16 @@ export function buildServer() {
|
|||||||
// visitor shares one rate-limit bucket. Trust the proxy's X-Forwarded-For so
|
// 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.
|
// req.ip is the real client. Only Traefik can reach the container, so this is safe.
|
||||||
const app = Fastify({ bodyLimit: BODY_LIMIT, trustProxy: true });
|
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 hub = new RoomHub();
|
||||||
const accounts = new AccountStore(DATA_DIR, undefined, ADMIN_USERS, MAX_USERS);
|
const accounts = new AccountStore(DATA_DIR, undefined, ADMIN_USERS, MAX_USERS);
|
||||||
void accounts.load();
|
void accounts.load();
|
||||||
|
|||||||
@@ -16,7 +16,11 @@ export interface CloudCharInfo { id: string; name: string; ownerUserId: string;
|
|||||||
export async function req<T>(path: string, init?: RequestInit): Promise<T> {
|
export async function req<T>(path: string, init?: RequestInit): Promise<T> {
|
||||||
let res: Response;
|
let res: Response;
|
||||||
try {
|
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 {
|
} catch {
|
||||||
throw new CloudError('Could not reach the server.');
|
throw new CloudError('Could not reach the server.');
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user