4.0 KiB
Gotchas & Troubleshooting
Critical Pitfalls
Stream Consumption (MOST COMMON)
Problem: "stream already consumed" or worker hangs
Cause: message.raw is ReadableStream - consume once only
Solution:
// ❌ WRONG
const email1 = await parser.parse(await message.raw.arrayBuffer());
const email2 = await parser.parse(await message.raw.arrayBuffer()); // FAILS
// ✅ CORRECT
const raw = await message.raw.arrayBuffer();
const email = await parser.parse(raw);
Consume message.raw immediately before any async operations.
Destination Verification
Problem: Emails not forwarding
Cause: Destination unverified
Solution: Add destination, check inbox for verification email, click link. Verify status: GET /zones/{id}/email/routing/addresses
Mail Authentication
Problem: Legitimate emails rejected
Cause: Missing SPF/DKIM/DMARC on sender domain
Solution: Configure sender DNS:
example.com. IN TXT "v=spf1 include:_spf.example.com ~all"
selector._domainkey.example.com. IN TXT "v=DKIM1; k=rsa; p=..."
_dmarc.example.com. IN TXT "v=DMARC1; p=quarantine"
Envelope vs Header
Problem: Filtering on wrong address
Solution:
// Routing/auth: envelope
if (message.from === "trusted@example.com") { }
// Display: headers
const display = message.headers.get("from");
SendEmail Limits
| Issue | Limit | Solution |
|---|---|---|
| From domain | Must own | Use Email Routing domain |
| Volume | ~100/min Free | Upgrade or throttle |
| Attachments | Not supported | Link to R2 |
| Type | Transactional | No bulk |
Common Errors
CPU Time Exceeded
Cause: Heavy parsing, large emails
Solution:
const size = parseInt(message.headers.get("content-length") || "0") / 1024 / 1024;
if (size > 20) {
message.setReject("Too large");
return;
}
ctx.waitUntil(expensiveWork());
await message.forward("dest@example.com");
Rule Not Triggering
Causes: Priority conflict, matcher error, catch-all override
Solution: Check priority (lower=first), verify exact match, confirm destination verified
Undefined Property
Cause: Missing header
Solution:
// ❌ WRONG
const subj = message.headers.get("subject").toLowerCase();
// ✅ CORRECT
const subj = message.headers.get("subject")?.toLowerCase() || "";
Limits
| Resource | Free | Paid |
|---|---|---|
| Email size | 25 MB | 25 MB |
| Rules | 200 | 200 |
| Destinations | 200 | 200 |
| CPU time | 10ms | 50ms |
| SendEmail | ~100/min | Higher |
Debugging
Local
npx wrangler dev
curl -X POST 'http://localhost:8787/__email' \
--header 'content-type: message/rfc822' \
--data 'From: test@example.com
To: you@yourdomain.com
Subject: Test
Body'
Production
npx wrangler tail
Pattern
export default {
async email(message, env, ctx) {
try {
console.log("From:", message.from);
await process(message, env);
} catch (err) {
console.error(err);
message.setReject(err.message);
}
}
} satisfies ExportedHandler;
Auth Troubleshooting
Check Status
const auth = message.headers.get("authentication-results") || "";
console.log({
spf: auth.includes("spf=pass"),
dkim: auth.includes("dkim=pass"),
dmarc: auth.includes("dmarc=pass")
});
if (!auth.includes("pass")) {
message.setReject("Failed auth");
return;
}
SPF Issues
Causes: Forwarding breaks SPF, too many lookups (>10), missing includes
Solution:
; ✅ Good
example.com. IN TXT "v=spf1 include:_spf.google.com ~all"
; ❌ Bad - too many
example.com. IN TXT "v=spf1 include:a.com include:b.com ... ~all"
DMARC Alignment
Cause: From domain must match SPF/DKIM domain
Best Practices
- Consume
message.rawimmediately - Verify destinations
- Handle missing headers (
?.) - Use envelope for routing
- Check spam scores
- Test locally first
- Use
ctx.waitUntilfor background work - Size-check early