fix(emails): reject empty/unparseable POST /api/emails bodies (no silent footer-only sends)#729
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
Warning Review limit reached
Next review available in: 9 minutes Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available. How can I continue?After more reviews become available, a review can be triggered using the To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews. How do review limits work?CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability. For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window. Please refer docs for additional details. Review details⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Run ID: ⛔ Files ignored due to path filters (1)
📒 Files selected for processing (1)
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
No issues found across 2 files
Confidence score: 5/5
- Automated review surfaced no issues in the provided summaries.
- No files require special attention.
Architecture diagram
sequenceDiagram
participant Client
participant Route as POST /api/emails
participant Validator as validateSendEmailBody()
participant Parser as safeParseJson
participant Schema as sendEmailBodySchema (Zod)
Note over Client,Schema: NEW: Reject empty/unparseable bodies (no silent footer-only sends)
Client->>Route: POST /api/emails<br/>with JSON body
Route->>Validator: validate request + auth
Validator->>Parser: parse body JSON
alt Malformed JSON
Parser-->>Validator: {} (unchanged contract)
else Valid JSON
Parser-->>Validator: parsed payload
end
Validator->>Schema: validate parsed body
Schema->>Schema: .refine(): non-empty html|text after trim()?
alt BOTH html and text empty/missing (incl. whitespace-only)
Note over Schema: Guard rejects empty/footer-only
Schema-->>Validator: ZodError: "a non-empty html or text body is required"
Validator-->>Route: 400 Bad Request
Route-->>Client: 400 + error message
else html or text is non-empty
Schema-->>Validator: validated SendEmailBody
Validator-->>Route: valid payload + auth
Route-->>Client: 200 + success
end
Auto-approved: Tightens email validation to reject empty/unparseable bodies, preventing silent footer-only sends. Low-risk schema change with updated tests.
Re-trigger cubic
ed184ac to
7c62579
Compare
…ent footer-only sends)
Guard sendEmailBodySchema with a refine requiring a non-empty html or text body,
and drop the html "" default. A malformed/empty body parses to {} and now returns
400 instead of silently sending a "Message from Recoup" footer-only email.
Rebased onto main after #731 (email_send_log observability) rewrote
validateSendEmailBody: reconciled to the new { rawBody, error } | { rawBody, data }
return shape and the readRawBody read path (comment updated; safeParseJson no
longer used here). With #731 in place, a guarded 400 is also recorded as a
`rejected` row in email_send_log.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
7c62579 to
b64728b
Compare
Rebased onto
|
| # | Request | Auth | HTTP | result |
|---|---|---|---|---|
| 1 | {to, subject} — no html/text |
none | 400 a non-empty html or text body is required (missing_fields:["html"]) |
guard rejects |
| 2 | html:" " (whitespace), no text |
none | 400 same | guard rejects |
| 3 | not json … (unparseable) |
none | 400 same (parses to {}) |
guard rejects |
| 4 | valid html body |
none | 401 auth | guard passed |
| 5 | valid html body, to omitted |
Bearer | 200 sent to sweetmantech@gmail.com, id 9822160e… |
guard allows real sends |
Synergy with #731 confirmed in email_send_log — every rejection is now a logged rejected row with the full raw_body (cases 1–4), and the unparseable not json … body is captured verbatim (15 B). Case 5 logged sent with matching resend_id. All 5 test rows deleted afterward.
Net effect: the empty footer-only "Message from Recoup" send is now a 400 + an observable rejected row instead of a silent delivery.
What
POST /api/emailsaccepted a body with nohtml/textand silently delivered a footer-only "Message from Recoup" email withsuccess:true. A malformed JSON body hit the same path —safeParseJsonreturns{}on parse failure, and the all-optional schema accepted{}. In a 2026-06-30 production sample, ~38% of delivered report emails were empty footer-only this way (incl. to a WMG label exec).Change
Tighten
sendEmailBodySchemainlib/emails/validateSendEmailBody.ts:html: z.string().default("").refine()requiring a non-emptyhtmlORtext(aftertrim())Malformed (→
{}) and empty bodies now fail validation → 400, instead of sending.Deliberately NOT changed:
safeParseJson(it has ~30 callers — changing its{}-on-failure contract is out of scope/risky), andresolveEmailSubject(it keeps deriving the subject from the body's first line; with a body now required, the generic"Message from Recoup"default no longer ships an empty email).TDD
lib/emailssuite: 147 passing. Lint clean. (tscshows pre-existing, unrelated errors inlib/trigger/__tests__Trigger.dev mocks — not touched here.)Verify (maps to the issue's REPRO-1/2/3)
{}→ refine fails){"html":""}→ 400 ✓html→ 200 ✓Preview verification against the deployed URL to follow as a comment once the deployment is Ready.
Docs
Contract gains a
400for empty/unparseable bodies — paireddocsPR to add it to the/api/emailsOpenAPI (docs → api order).Part of recoupable/chat#1829 (task-email pipeline). Base
testper repo convention.🤖 Generated with Claude Code
Summary by cubic
Rejects empty or unparseable POST
/api/emailsbodies to stop footer-only sends. Requests now return 400 unlesshtmlortexthas non-whitespace content, and rejections are logged asrejectedinemail_send_log.Bug Fixes
sendEmailBodySchemawith a refine that requires non-emptyhtmlortext; removed thehtmldefault.readRawBody) now validates as{}and returns 400;safeParseJsonis no longer used here.email_send_logasrejected.Migration
htmlortext.Written for commit b64728b. Summary will update on new commits.