openclaw/docs/infra/safe-fetch.md
Nick DiMoro 05dce537f7 fix(infra): Add safeFetch wrapper to prevent unhandled fetch() crashes
CRITICAL FIX: Gateway was crashing every 30-50 minutes due to unhandled
fetch() promise rejections causing process termination.

Problem:
- Network fetch() failures (DNS, timeout, connection refused) caused
  unhandled promise rejections
- Unhandled rejection handler in src/infra/unhandled-rejections.ts calls
  process.exit(1) to prevent silent failures
- Gateway crashed 3 times today (08:12:53, 08:41:31, 09:31:55)
- All agents stopped responding on each crash
- Required manual restart

Root Cause:
- 88+ fetch() calls in codebase without proper error handling
- TypeError: fetch failed at node:internal/deps/undici/undici:15422:13
- No try/catch or .catch() handlers on many network requests

Solution:
- Created safeFetch() wrapper that NEVER throws
- Always resolves to result object (ok: true | false)
- Classifies errors by type (network/abort/timeout/unknown)
- Logs errors with context but keeps gateway running
- Provides convenience helpers: safeFetchText, safeFetchJson

Implementation:
- New file: src/infra/safe-fetch.ts (core wrapper)
- New file: src/infra/safe-fetch.test.ts (comprehensive tests)
- New file: docs/infra/safe-fetch.md (usage guide & migration)
- Modified: src/infra/update-check.ts (example migration)

Testing:
- Full test suite covers success, failures, timeouts, aborts
- Migration verified with update-check.ts as proof of concept

Next Steps:
- Gradually migrate high-risk fetch() calls to safeFetch()
- Priority areas: agents/tools/web-*.ts, providers/*.ts
- Consider making safeFetch the default for new code

This fix prevents immediate crashes but doesn't migrate all calls yet.
Gateway stability should improve significantly with this foundation.
2026-01-26 02:52:38 -07:00

198 lines
4.9 KiB
Markdown

# Safe Fetch Utility
## Overview
The `safeFetch` utility provides a crash-resistant wrapper around native `fetch()` calls to prevent unhandled promise rejections from terminating the gateway process.
## Problem
Network-related `fetch()` failures can cause unhandled promise rejections that crash the entire gateway:
```
TypeError: fetch failed
at node:internal/deps/undici/undici:15422:13
```
When these errors aren't caught, they trigger the unhandled rejection handler in `src/infra/unhandled-rejections.ts`, which calls `process.exit(1)` to prevent silent failures.
## Solution
`safeFetch()` is a drop-in replacement for `fetch()` that **never throws** - it always resolves to a result object that you can check for success or failure.
## Usage
### Basic Pattern
```typescript
import { safeFetch } from "../infra/safe-fetch.js";
// Instead of:
// const response = await fetch(url); // ❌ Can crash on network failure
// Use:
const result = await safeFetch(url);
if (result.ok) {
const data = await result.response.json();
// handle success
} else {
console.error("Fetch failed:", result.message, result.type);
// handle error gracefully - gateway keeps running
}
```
### Convenience Helpers
For common patterns, use the provided helpers:
```typescript
import { safeFetchText, safeFetchJson } from "../infra/safe-fetch.js";
// Get text content (returns null on failure)
const text = await safeFetchText("https://api.example.com/status");
if (text) {
console.log("Status:", text);
}
// Get JSON content (returns null on failure)
const data = await safeFetchJson<{ version: string }>("https://api.example.com/info");
if (data) {
console.log("Version:", data.version);
}
```
## API Reference
### `safeFetch(input, init?)`
Main wrapper that never throws.
**Returns:** `Promise<SafeFetchResult>`
```typescript
type SafeFetchResult =
| {
ok: true;
response: Response;
error: null;
}
| {
ok: false;
response: null;
error: Error;
message: string;
type: "network" | "abort" | "timeout" | "unknown";
};
```
### `safeFetchText(input, init?)`
Convenience helper for text responses.
**Returns:** `Promise<string | null>`
### `safeFetchJson<T>(input, init?)`
Convenience helper for JSON responses.
**Returns:** `Promise<T | null>`
## Error Classification
Errors are automatically classified by type for better handling:
- **`network`**: Connection failures, DNS errors, refused connections
- **`abort`**: Explicitly aborted requests
- **`timeout`**: Request timeouts
- **`unknown`**: Other error types
## Migration Guide
### Before (Unsafe)
```typescript
async function checkUpdate() {
try {
const res = await fetch("https://registry.npmjs.org/clawdbot/latest");
if (!res.ok) {
return { version: null, error: `HTTP ${res.status}` };
}
const json = await res.json();
return { version: json.version };
} catch (err) {
// ⚠️ If this catch is missing, the gateway crashes!
return { version: null, error: String(err) };
}
}
```
### After (Safe)
```typescript
import { safeFetchJson } from "../infra/safe-fetch.js";
async function checkUpdate() {
const json = await safeFetchJson<{ version: string }>(
"https://registry.npmjs.org/clawdbot/latest"
);
if (!json) {
return { version: null, error: "Fetch failed" };
}
return { version: json.version };
}
```
## When to Use
### ✅ Use `safeFetch` for:
- External API calls where failures are expected
- Periodic background tasks (update checks, status pings)
- Non-critical operations that shouldn't crash the gateway
- Any fetch where error handling might be forgotten
### ❌ Don't use `safeFetch` when:
- You need to propagate errors up the call stack
- The failure should be fatal (though consider if this is really true)
- Performance is absolutely critical (minimal overhead, but it exists)
## Best Practices
1. **Log failures appropriately**: `safeFetch` logs errors automatically, but add context if needed
2. **Provide fallbacks**: Always have a plan for when the fetch fails
3. **Check `result.ok`**: Don't assume success
4. **Use helpers when possible**: `safeFetchText` and `safeFetchJson` reduce boilerplate
## Testing
The utility includes comprehensive tests covering:
- Successful fetches
- Network failures
- Abort signals
- Timeouts
- JSON parsing errors
- Response reading errors
- Concurrent failures
Run tests:
```bash
pnpm test src/infra/safe-fetch.test.ts
```
## Implementation Details
- Uses the existing `resolveFetch()` wrapper for consistency
- Classifies errors based on error message patterns
- Logs errors to console with URL context
- Zero dependencies beyond existing infra
- Type-safe with full TypeScript support
## Related
- **Unhandled Rejections**: See `src/infra/unhandled-rejections.ts` for the crash handler this prevents
- **Fetch Wrapper**: See `src/infra/fetch.ts` for the underlying fetch abstraction