From 647ff65b00c054885710b9f5d634f6c1341336b5 Mon Sep 17 00:00:00 2001 From: Amin Mousavi Date: Fri, 21 Aug 2026 04:02:49 +0800 Subject: [PATCH] fix(backend): apply the large-body limit to every spelling Express routes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit req.path was compared to the canonical '/api/voice/extract' only, but Express routes case-insensitively and ignores a trailing slash by default. '/api/voice/extract/' therefore reached the controller with the 100 kb parser, and 413'd every recording past ~20 seconds — a failure that reads as a broken microphone rather than a routing detail. Co-Authored-By: Claude Opus 5 (1M context) --- backend/src/common/body-parsers.spec.ts | 14 ++++++++++++++ backend/src/common/body-parsers.ts | 12 +++++++++++- 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/backend/src/common/body-parsers.spec.ts b/backend/src/common/body-parsers.spec.ts index ecde076..d64b859 100644 --- a/backend/src/common/body-parsers.spec.ts +++ b/backend/src/common/body-parsers.spec.ts @@ -74,6 +74,20 @@ describe('createJsonBodyParser', () => { expect((res.body as ProbeBody).keys).toBe(1); }); + it('widens the limit for the spellings Express itself accepts', async () => { + // Express routes case-insensitively and ignores a trailing slash by default, so these + // all reach the voice controller. Any of them taking the 100 kb parser would 413 a + // real recording and read as a broken microphone. + for (const path of [ + '/api/voice/extract/', + '/API/Voice/Extract', + '/api/Voice/extract/', + ]) { + const res = await request(buildApp()).post(path).send(bodyOfKb(300)); + expect(res.status).toBe(200); + } + }); + it('does not widen the limit for a path that merely looks similar', async () => { for (const path of [ '/api/voice/extract/extra', diff --git a/backend/src/common/body-parsers.ts b/backend/src/common/body-parsers.ts index c845d91..97278da 100644 --- a/backend/src/common/body-parsers.ts +++ b/backend/src/common/body-parsers.ts @@ -24,12 +24,22 @@ export const VOICE_BODY_LIMIT = '10mb'; * the endpoint rejected every real recording with a 500. One explicit branch has no such * coupling, and is covered by body-parsers.spec.ts. */ +/** + * Express routes case-insensitively and ignores a trailing slash unless configured + * otherwise, so `/API/Voice/Extract/` reaches the same controller. Matching only the + * canonical spelling would hand those requests the 100 kb parser and 413 every real + * recording — a failure that looks like a broken microphone, not a routing detail. + */ +function isVoiceExtractPath(path: string): boolean { + return path.toLowerCase().replace(/\/+$/, '') === VOICE_EXTRACT_PATH; +} + export function createJsonBodyParser(): RequestHandler { const voiceParser = json({ limit: VOICE_BODY_LIMIT }); const defaultParser = json(); return (req: Request, res: Response, next: NextFunction) => - req.path === VOICE_EXTRACT_PATH + isVoiceExtractPath(req.path) ? voiceParser(req, res, next) : defaultParser(req, res, next); }