cloudflare
references/email-routing/gotchas.md
.md 197 lines
Content
# Gotchas & Troubleshooting
## Critical Pitfalls
### Stream Consumption (MOST COMMON)
**Problem:** "stream already consumed" or worker hangs
**Cause:** `message.raw` is `ReadableStream` - consume once only
**Solution:**
```typescript
// ❌ 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:
```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:**
```typescript
// 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:**
```typescript
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:**
```typescript
// ❌ 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
```bash
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
```bash
npx wrangler tail
```
### Pattern
```typescript
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
```typescript
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:**
```dns
; ✅ 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
1. Consume `message.raw` immediately
2. Verify destinations
3. Handle missing headers (`?.`)
4. Use envelope for routing
5. Check spam scores
6. Test locally first
7. Use `ctx.waitUntil` for background work
8. Size-check early