Repository navigation
Refuse with 406, retry cut-off uploads, and log arrival lag - #32
Merged
Merged
Conversation
Mailgun retries every non-200 response except 406, on a fixed schedule over 8 hours. The handler treated 400 as "don't retry", so a malformed or oversize request was in fact resent for hours. Permanent refusals now return 406. A body that fails to parse is now classified instead of reported as a bare "malformed request body". The rest of the body is drained first, since the parser stops at the first error: a body that came up short of its Content-Length, or whose read failed, was cut off in transit and gets 500 so Mailgun retries it; one that arrived whole and still would not parse gets 406. Either way the underlying error is logged. On 2026-10-28 Mailgun gave up on an upload after its 10-second timeout and the log could not say why the body was rejected. The accepted line now ends with sent= (the email's Date header, in UTC), posted= (Mailgun's signed timestamp) and lag= (from sending to arrival here), so a slow-reply report can be measured from our own log. A missing or unparseable Date logs sent=unknown and never blocks the email. Date is read from the top-level field, falling back to the message-headers JSON. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Go's multipart parser takes a clean end-of-stream between parts as the end of the form, so a body cut short there parses without error. The completeness check ran only after a parse failure, so such a body went on to the signature check; the cut-off test caught it about a quarter of the time, depending on map order in the test helper. The body is now drained and judged after every parse. A real dropped connection reads as unexpected EOF, which the parser never accepts, so production was not exposed, but the byte count is the reliable signal. The test now tries every cut point instead of one. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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.
Why
On 2026-10-28 two emails to iwk@ took 10 and 21 minutes to get replies. Mailgun's logs showed its forwards to the webhook timing out at 10 seconds. One attempt reached FileMill mid-upload and was logged only as
400 malformed request body, and the log had no way to show how long any email took to arrive.Mailgun's docs also show that 400 doesn't do what the code assumed. Mailgun retries every non-200 response except 406, so our "don't retry" 400s were being resent for 8 hours.
Changes
Content-Length, or one whose read failed. Mailgun retries these. The handler reads the rest of the body before deciding, because the parser stops at the first error and an unread remainder would otherwise look like a cut-off.request body incomplete: got 181234 of 363441 bytes: unexpected EOFormalformed request body: <parser error>.sent=posted=lag=. These are the email'sDateheader in UTC, Mailgun's signed timestamp, and the time from sending to arrival. IfDateis missing or won't parse, the line sayssent=unknownand the email is still processed.Testing
go vet ./...andgo test ./...pass.After deploy
accepted:line to confirm Mailgun postsDatethe way this expects. Asent=unknownthere means it doesn't.🤖 Generated with Claude Code