From 5a64bba40a6721b152dd4084f2d9b641edcc15c9 Mon Sep 17 00:00:00 2001 From: azad-technext Date: Fri, 12 Jun 2026 15:42:20 +0600 Subject: [PATCH] fix(app): remove duplicate pre-parser mount of bypassRateLimit modules MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The bypassRateLimit modules (/api/posts, /api/admin/posts) and globalLimiter were each mounted twice — once BEFORE express.json() and once after. Express dispatches to the first match, so every posts write hit the pre-parser copy with req.body === undefined, causing validate() to throw "expected object, received undefined" (400) on PATCH/POST/DELETE while reads worked fine. Restore the single documented ordering: rawBody (auth) -> body parsers -> bypassRateLimit -> globalLimiter -> regular modules. This also moves the auth catch-all above the global limiter again (it had been double-counted against the per-IP limit). Verified against the live app: express.json (layer 8) now precedes the /api/posts (14) and /api/admin/posts (15) routers, with auth (7) above the parser. Co-Authored-By: Claude Opus 4.8 --- src/http/app.ts | 17 +++++------------ 1 file changed, 5 insertions(+), 12 deletions(-) diff --git a/src/http/app.ts b/src/http/app.ts index 89d9eac..b10a06a 100644 --- a/src/http/app.ts +++ b/src/http/app.ts @@ -94,18 +94,11 @@ export function createApp(): Express { }), ); - // ── 7. Modules that bypass the rate limiter (health probes) ── - // Mounted BEFORE globalLimiter so k8s probes are never throttled and - // /readyz can still report on Redis health when Redis is the thing - // that's failing. - for (const m of modules) { - if (m.bypassRateLimit) app.use(m.mountPath, m.router); - } - - // ── 8. Global rate limit (Redis-backed) ── - app.use(globalLimiter); - - // ── 9. Modules that need raw bodies (mounted BEFORE body parsers) ── + // ── 7. Modules that need raw bodies (mounted BEFORE body parsers) ── + // Better Auth's catch-all reads the raw request stream itself, so it MUST + // sit above the body parsers. It carries its own Redis brute-force limiter + // (authLimiter, in its router), so sitting above the global limiter here + // costs it no protection. for (const m of modules) { if (m.rawBody) app.use(m.mountPath, m.router); }