fix(conv): admit 30 MiB relay messages and drop the relay per-IP throttle - #892
Merged
kmonsoe merged 1 commit intoSep 4, 2026
Merged
Conversation
…ttle
Two defects in POST /v1/conversations/email/relay, both found against the
deployed 5.14 backend.
The 30 MiB relay limit was unreachable. createApp registers the global JSON
body parser with a 4mb limit, and a relay envelope carries the message as
base64, so the JSON is ~1.37x the message size: anything over roughly 3 MB
was answered by Express with {"statusCode":413,"message":"request entity
too large"} before the controller ran. Verified by POSTing 5 MB to the live
API — Nest answers 413 while the platform ingress passes 35 MB through, so
the limit was ours. The relay path now gets its own express.json parser
mounted ahead of the global useBodyParser call, sized from the same
constant the controller enforces (EMAIL_RELAY_BODY_LIMIT_BYTES: the base64
expansion of EMAIL_RELAY_MAX_RAW_BYTES plus 1 MiB of envelope, ≈41 MiB) and
setting req.rawBody exactly as Nest's rawBody: true verify hook does, so
HMAC verification over the raw bytes is unchanged. body-parser 2.x skips a
request whose stream is already finished, so the global parser does not
double-parse, and the 4mb limit still applies to every other route. A POST
on that path with no x-munin-relay-signature header is refused 401 by a
tiny middleware before the body is read, so unsigned traffic cannot make
the server buffer 40 MB per request.
The controller was declared with throttle: true, which applies the public
per-client-IP throttle (60/min, 1000/hour). Every customer's relayed mail
arrives from the operator's one or two MX addresses, so that was a
platform-wide ceiling of 1000 inbound emails per hour. The endpoint
authenticates every request by HMAC before touching the database, so the
IP throttle added nothing and is removed.
The integration test now boots through createApp so the route-scoped
parser is under test, and adds: a ~6 MiB message (text part plus a large
base64 attachment) is ingested; an unsigned oversized post is 401 before
the controller; a message over the relay maximum still gets the
controller's own 413; and 65 signed posts in a row all return 201. All
four fail on the previous commit (413 from Express, 429 from the
throttler).
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
kmonsoe
disabled the stack merge
September 4, 2026 10:29
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Stacked on #890. Two defects found while planning the hosted inbound MX that will feed
POST /v1/conversations/email/relay.1. The 30 MiB relay cap was unreachable
bootstrap-app.tsregisters the global JSON parser at4mb. A base64 JSON envelope is ~1.37× the message, so any email over ~3 MB got Express's{"statusCode":413,"message":"request entity too large"}before the controller ran. Verified against the deployed 5.14 API: a 5 MB POST is refused by Nest, while a 35 MB POST passes the platform ingress untouched, so the global parser was the only limit.Fix: a new
email-relay.constants.tsis the single source for the path, signature header and limits (EMAIL_RELAY_MAX_RAW_BYTES= 30 MiB,EMAIL_RELAY_BODY_LIMIT_BYTES≈ 41 MiB derived from it). Bootstrap mounts, on exactly that path and ahead of the global parser:401 relay signature missingfor a POST withoutx-munin-relay-signaturebefore any body is read, so unsigned traffic cannot make the server buffer 40 MB per request;express.jsonwith the larger limit whoseverifysetsreq.rawBodyexactly as Nest's own raw-body parser does.body-parser 2.3.0 skips a request whose body has already been read, so the global
4mbparser never re-parses this route and stays untouched for everything else.2. Per-IP throttling capped the whole platform's inbound mail
The controller was
@PublicController(..., { throttle: true }): 60/min and 1000/hour per client IP. Every customer's forwarded mail arrives from the operator's one or two MX addresses, so that was a platform-wide ceiling of 1000 emails an hour. The endpoint is HMAC-authenticated, so the throttle added nothing; it is removed. Signature verification still precedes any DB work.Tests
The integration test now boots via
createApp(AppModule)— the previousNestFactory.createnever exercised bootstrap's parsers. New cases:ingested, row presentrelay signature missing, no rowmessage too largefrom the controller, not ExpressNegative control: with
{ throttle: true }restored and the route-scoped mount removed, exactly these four fail. Full backend-core suite green (153 files, 1980 tests).Changeset:
@getmunin/backend-corepatch.🤖 Generated with Claude Code