In the previous Unified Client Factory chapter, we built a "Universal Travel Adapter" to connect to Claude regardless of the provider (AWS, Google, or Direct).
Now we have a connection, but connections can be flaky. The internet hiccups, servers get overloaded, and sometimes wifi drops.
Imagine you send a text message to a friend, but your signal drops for one second.
We want our API calls to be like the Resilient approach.
The Resilient Request Executor is a wrapper function called withRetry. It wraps around your API call and acts like a smart autopilot.
ECONNRESET or SSL_ERROR) into English.
Using the executor is simple. Instead of calling the client directly, you pass your "task" to the withRetry function.
import { withRetry } from './withRetry.js'
import { getAnthropicClient } from './client.js'
// 1. Define your task
const myTask = async (client, attempt) => {
return client.messages.create({
model: 'claude-3-opus',
messages: [{ role: 'user', content: 'Hello!' }]
});
};
// 2. Run it safely
const result = await withRetry(getAnthropicClient, myTask, {
maxRetries: 5,
model: 'claude-3-opus'
});
What happens here?
If the API fails on the first try (e.g., Server Overloaded), withRetry catches the error, waits a bit, and calls myTask again with a fresh client.
Let's look at the lifecycle of a request when things go wrong.
The magic happens in withRetry.ts. We will simplify the code to show the core logic.
The core of the system is a simple loop that runs until we run out of attempts.
// inside withRetry.ts
for (let attempt = 1; attempt <= maxRetries + 1; attempt++) {
try {
// Get a fresh client (handles token refreshes if needed)
const client = await getClient();
// Try to run the user's operation
return await operation(client, attempt, context);
} catch (error) {
// If it fails, we land here...
lastError = error;
// Decision logic happens next
}
}
Not all errors are retryable. If the error is "Invalid API Key" (401), retrying won't fix it. If the error is "Server Overloaded" (529), retrying helps.
We use helper functions to make this decision.
// inside the catch block...
// 1. Check if it's a transient capacity error (Rate Limit or Overload)
if (isTransientCapacityError(error)) {
// These are safe to retry!
}
// 2. Check if it's a fatal error (like 400 Bad Request)
else if (!shouldRetry(error)) {
// Give up immediately
throw new CannotRetryError(error);
}
We calculate how long to sleep. We add "Jitter" (randomness) so that if 1,000 users fail at once, they don't all retry at the exact same millisecond.
function getRetryDelay(attempt) {
// Base delay grows exponentially: 500ms, 1000ms, 2000ms...
const baseDelay = 500 * Math.pow(2, attempt - 1);
// Add random jitter (up to 25%)
const jitter = Math.random() * 0.25 * baseDelay;
return baseDelay + jitter;
}
Sometimes a request fails because an OAuth token expired. withRetry is smart enough to detect this and refresh credentials before the next attempt.
if (error.status === 401 || isOAuthTokenRevokedError(error)) {
// Force a token refresh logic
await handleOAuth401Error(failedToken);
// The loop continues, and getClient() will fetch a NEW valid token
client = await getClient();
}
If all retries fail, or if the error is fatal, we need to tell the user what happened. Raw errors like ERR_TLS_CERT_ALTNAME_INVALID are scary.
We use errorUtils.ts to translate them.
// inside errorUtils.ts
export function formatAPIError(error: APIError): string {
// Check for SSL errors (common in corporate VPNs)
if (error.code === 'UNABLE_TO_VERIFY_LEAF_SIGNATURE') {
return 'SSL verification failed. Check your corporate proxy settings.';
}
// Check for Timeouts
if (error.code === 'ETIMEDOUT') {
return 'Request timed out. Check your internet connection.';
}
return error.message;
}
In this chapter, we learned about the Resilient Request Executor.
Now that we can reliably send requests, we need to know who is sending them and what they are allowed to do.
Next Chapter: Account & Entitlements
Generated by Code IQ