Curated Skills
by lstudlo

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