fix(backend): restore the large-body limit on the voice route
POST /voice/extract returned 500 for any real recording. The threshold was
exactly 100 kb — Express's body-parser default — which is about 20 seconds of
audio, so the endpoint was unusable at its own 2-minute cap.
The scoped parser was registered as a path-mounted json() stacked in front of a
default one, which relied on two implicit behaviours: Express stripping the
mount path, and body-parser skipping a request another parser had already
handled. That coupling broke when the surrounding middleware order shifted, and
it broke silently — the parser was still registered, just no longer the one that
ran. Bisected by dumping the Express layer stack and confirming the raw error was
`entity.too.large` with `limit: 102400`.
Replaced with a single middleware that picks a parser by path. No mount-path
stripping, no dependence on parser ordering. Extracted to common/body-parsers.ts
so it is covered by a unit test rather than only reachable through main.ts, which
createTestingModule never executes.
The test is mutation-checked: forcing the default parser fails 2 of its 5 cases.
It also pins that the larger limit does not leak app-wide, and that a merely
similar path (/api/voice/extract/extra) does not get it.
Verified against the compiled server: 300 kb now reaches /api/voice/extract,
/api/auth/login still rejects it, and ordinary requests are unaffected.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-20 22:12:16 +03:30
|
|
|
import {
|
|
|
|
|
json,
|
|
|
|
|
type NextFunction,
|
|
|
|
|
type Request,
|
|
|
|
|
type RequestHandler,
|
|
|
|
|
type Response,
|
|
|
|
|
} from 'express';
|
|
|
|
|
|
|
|
|
|
/** The one route that accepts a large body, and how large. */
|
|
|
|
|
export const VOICE_EXTRACT_PATH = '/api/voice/extract';
|
|
|
|
|
export const VOICE_BODY_LIMIT = '10mb';
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* JSON body parsing for the whole app.
|
|
|
|
|
*
|
|
|
|
|
* Voice recordings are base64 JSON and pass Express's 100 kb default at roughly 20 seconds
|
|
|
|
|
* of audio, so that one route needs a larger limit while every other endpoint keeps the
|
|
|
|
|
* default — a large body should not become acceptable everywhere.
|
|
|
|
|
*
|
docs: cut the comments that were not earning their place
I wrote 731 comment lines on this branch against 4,530 lines of code — 14%,
where the rest of the repo runs at 1.8%. CLAUDE.md asks for code that reads
like its surroundings, and this did not.
Removed by genre rather than by taste:
- restating the code, e.g. "JS getUTCDay() numbering: Sunday = 0" above the
map that literally shows it, and a docblock on startOfWeek explaining that
it returns the start of the week;
- narrating history — "this used to rebuild the whole map", "left the bar
recording forever" — which the commit message and git blame already carry;
- saying the same thing in several places: the "cannot record is not a
denied microphone" reason appeared three times in one file, and the
"aborting stops a per-minute metered call" reason across three files. Each
now lives once, where the behaviour it explains lives;
- defending decisions nobody would question, like why toLatinDigits is its
own module;
- over-explaining defensive branches, three separate comments to distinguish
null from missing-kind from unrecognised-kind.
What stays is what the code cannot say: the patient-right convention in
toFdi, whose failure mode is a valid code for the wrong tooth; the
"this"-vs-"next" week anchoring; StrictMode re-arming mountedRef; Safari
accepting no mimeType hint; and the invariants whose violation already cost
a bug — the body parser's middleware ordering and the dispatch panel's
auto-fill rules.
Comments only. The diff contains no non-comment line.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-21 23:46:56 +08:00
|
|
|
* Deliberately one middleware that *chooses* a parser, not a path-mounted parser stacked in
|
|
|
|
|
* front of a default one: that arrangement depended on Express's mount-path stripping and on
|
|
|
|
|
* body-parser skipping an already-parsed request, and silently stopped applying whenever the
|
|
|
|
|
* middleware order shifted. One explicit branch has no such coupling.
|
fix(backend): restore the large-body limit on the voice route
POST /voice/extract returned 500 for any real recording. The threshold was
exactly 100 kb — Express's body-parser default — which is about 20 seconds of
audio, so the endpoint was unusable at its own 2-minute cap.
The scoped parser was registered as a path-mounted json() stacked in front of a
default one, which relied on two implicit behaviours: Express stripping the
mount path, and body-parser skipping a request another parser had already
handled. That coupling broke when the surrounding middleware order shifted, and
it broke silently — the parser was still registered, just no longer the one that
ran. Bisected by dumping the Express layer stack and confirming the raw error was
`entity.too.large` with `limit: 102400`.
Replaced with a single middleware that picks a parser by path. No mount-path
stripping, no dependence on parser ordering. Extracted to common/body-parsers.ts
so it is covered by a unit test rather than only reachable through main.ts, which
createTestingModule never executes.
The test is mutation-checked: forcing the default parser fails 2 of its 5 cases.
It also pins that the larger limit does not leak app-wide, and that a merely
similar path (/api/voice/extract/extra) does not get it.
Verified against the compiled server: 300 kb now reaches /api/voice/extract,
/api/auth/login still rejects it, and ordinary requests are unaffected.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-20 22:12:16 +03:30
|
|
|
*/
|
2026-08-21 04:02:49 +08:00
|
|
|
/**
|
docs: cut the comments that were not earning their place
I wrote 731 comment lines on this branch against 4,530 lines of code — 14%,
where the rest of the repo runs at 1.8%. CLAUDE.md asks for code that reads
like its surroundings, and this did not.
Removed by genre rather than by taste:
- restating the code, e.g. "JS getUTCDay() numbering: Sunday = 0" above the
map that literally shows it, and a docblock on startOfWeek explaining that
it returns the start of the week;
- narrating history — "this used to rebuild the whole map", "left the bar
recording forever" — which the commit message and git blame already carry;
- saying the same thing in several places: the "cannot record is not a
denied microphone" reason appeared three times in one file, and the
"aborting stops a per-minute metered call" reason across three files. Each
now lives once, where the behaviour it explains lives;
- defending decisions nobody would question, like why toLatinDigits is its
own module;
- over-explaining defensive branches, three separate comments to distinguish
null from missing-kind from unrecognised-kind.
What stays is what the code cannot say: the patient-right convention in
toFdi, whose failure mode is a valid code for the wrong tooth; the
"this"-vs-"next" week anchoring; StrictMode re-arming mountedRef; Safari
accepting no mimeType hint; and the invariants whose violation already cost
a bug — the body parser's middleware ordering and the dispatch panel's
auto-fill rules.
Comments only. The diff contains no non-comment line.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-21 23:46:56 +08:00
|
|
|
* Express routes case-insensitively and ignores exactly one trailing slash, so
|
|
|
|
|
* `/API/Voice/Extract/` reaches the same controller and must get the same limit — otherwise
|
|
|
|
|
* it 413s every real recording, which reads as a broken microphone rather than a route.
|
|
|
|
|
* Two slashes never route, so they must not buy a 10 MB buffer either.
|
2026-08-21 04:02:49 +08:00
|
|
|
*/
|
|
|
|
|
function isVoiceExtractPath(path: string): boolean {
|
2026-08-21 17:44:51 +08:00
|
|
|
return path.toLowerCase().replace(/\/$/, '') === VOICE_EXTRACT_PATH;
|
2026-08-21 04:02:49 +08:00
|
|
|
}
|
|
|
|
|
|
fix(backend): restore the large-body limit on the voice route
POST /voice/extract returned 500 for any real recording. The threshold was
exactly 100 kb — Express's body-parser default — which is about 20 seconds of
audio, so the endpoint was unusable at its own 2-minute cap.
The scoped parser was registered as a path-mounted json() stacked in front of a
default one, which relied on two implicit behaviours: Express stripping the
mount path, and body-parser skipping a request another parser had already
handled. That coupling broke when the surrounding middleware order shifted, and
it broke silently — the parser was still registered, just no longer the one that
ran. Bisected by dumping the Express layer stack and confirming the raw error was
`entity.too.large` with `limit: 102400`.
Replaced with a single middleware that picks a parser by path. No mount-path
stripping, no dependence on parser ordering. Extracted to common/body-parsers.ts
so it is covered by a unit test rather than only reachable through main.ts, which
createTestingModule never executes.
The test is mutation-checked: forcing the default parser fails 2 of its 5 cases.
It also pins that the larger limit does not leak app-wide, and that a merely
similar path (/api/voice/extract/extra) does not get it.
Verified against the compiled server: 300 kb now reaches /api/voice/extract,
/api/auth/login still rejects it, and ordinary requests are unaffected.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-20 22:12:16 +03:30
|
|
|
export function createJsonBodyParser(): RequestHandler {
|
|
|
|
|
const voiceParser = json({ limit: VOICE_BODY_LIMIT });
|
|
|
|
|
const defaultParser = json();
|
|
|
|
|
|
|
|
|
|
return (req: Request, res: Response, next: NextFunction) =>
|
2026-08-21 04:02:49 +08:00
|
|
|
isVoiceExtractPath(req.path)
|
fix(backend): restore the large-body limit on the voice route
POST /voice/extract returned 500 for any real recording. The threshold was
exactly 100 kb — Express's body-parser default — which is about 20 seconds of
audio, so the endpoint was unusable at its own 2-minute cap.
The scoped parser was registered as a path-mounted json() stacked in front of a
default one, which relied on two implicit behaviours: Express stripping the
mount path, and body-parser skipping a request another parser had already
handled. That coupling broke when the surrounding middleware order shifted, and
it broke silently — the parser was still registered, just no longer the one that
ran. Bisected by dumping the Express layer stack and confirming the raw error was
`entity.too.large` with `limit: 102400`.
Replaced with a single middleware that picks a parser by path. No mount-path
stripping, no dependence on parser ordering. Extracted to common/body-parsers.ts
so it is covered by a unit test rather than only reachable through main.ts, which
createTestingModule never executes.
The test is mutation-checked: forcing the default parser fails 2 of its 5 cases.
It also pins that the larger limit does not leak app-wide, and that a merely
similar path (/api/voice/extract/extra) does not get it.
Verified against the compiled server: 300 kb now reaches /api/voice/extract,
/api/auth/login still rejects it, and ordinary requests are unaffected.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-20 22:12:16 +03:30
|
|
|
? voiceParser(req, res, next)
|
|
|
|
|
: defaultParser(req, res, next);
|
|
|
|
|
}
|