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.
4.9 KiB
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
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:
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>
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 connectionsabort: Explicitly aborted requeststimeout: Request timeoutsunknown: Other error types
Migration Guide
Before (Unsafe)
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)
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
- Log failures appropriately:
safeFetchlogs errors automatically, but add context if needed - Provide fallbacks: Always have a plan for when the fetch fails
- Check
result.ok: Don't assume success - Use helpers when possible:
safeFetchTextandsafeFetchJsonreduce 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:
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.tsfor the crash handler this prevents - Fetch Wrapper: See
src/infra/fetch.tsfor the underlying fetch abstraction