From 930492058bfde9c4ac93963d75caded1ccefdbab Mon Sep 17 00:00:00 2001 From: arkon Date: Tue, 19 May 2026 10:37:26 +0200 Subject: [PATCH] fix(server): remove duplicate multipart parser conflicting with @fastify/multipart #90 added @fastify/multipart, which registers its own multipart/form-data content-type parser. Combined with the existing manual no-op parser in setupRoutes() (originally there so /api/screenshots could read req.raw directly), this raises "Content type parser 'multipart/form-data' already present" at server boot and the process exits. CI did not catch it because ci.yml runs typecheck + lint only. @fastify/multipart's parser is a no-op marker (sets req[kMultipart] = true and returns) and leaves the body on req.raw, so the legacy /api/screenshots handler that reads req.raw directly keeps working unchanged. The manual parser was redundant the moment the plugin was registered. Smoke-tested locally: server boots, /api/sessions/:id/paste-image returns 200 / 403-CSRF / 415-magic-mismatch / 413-oversize / 429-rate as designed; /api/screenshots upload still returns 200. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/web/server.ts | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/web/server.ts b/src/web/server.ts index e7e4f1bb..703c8c77 100644 --- a/src/web/server.ts +++ b/src/web/server.ts @@ -515,11 +515,10 @@ export class WebServer extends EventEmitter { } private async setupRoutes(): Promise { - // Allow multipart/form-data for screenshot uploads — skip Fastify's body parser - // so the route handler can read the raw stream directly. - this.app.addContentTypeParser('multipart/form-data', (_req, _payload, done) => { - done(null); - }); + // multipart/form-data: parser is provided by @fastify/multipart (registered + // below). Its parser is a no-op marker that leaves the body on req.raw, so + // legacy routes that read the raw stream directly (e.g. /api/screenshots) + // continue to work alongside routes that use req.file() (e.g. paste-image). // Enable gzip/brotli compression for all responses. // Massive win: 793KB uncompressed → ~120KB compressed for static assets.