DEV Community

Cover image for Forging Steel: The Art of Crafting Resilient HTTP Clients
Alex Aslam
Alex Aslam

Posted on

Forging Steel: The Art of Crafting Resilient HTTP Clients

If you've been in the trenches of Node.js for a while, you know the feeling. You've architected a beautiful, event-driven masterpiece. Your database queries are optimized, your APIs are RESTful and elegant. Then, you deploy.

And the real world hits.

A third-party API takes a leisurely 45 seconds to respond. A network blip causes a momentary hiccup. A service you depend on decides to take an unplanned nap. Suddenly, your elegant system is a house of cards, toppling because of someone else's problem.

We've all been there. This isn't a story of failure; it's the beginning of a journey. The journey from building functional clients to forging resilient ones. This isn't just code; it's an artwork of stability in a chaotic digital landscape.

The Canvas: Acknowledging a Hostile World

The first step in our journey is acceptance. The network is not reliable. External services are not under your control. Latency is not a constant. Embrace this chaos. Our artwork isn't about preventing the storm; it's about building a shelter that can withstand it.

Our medium for today is the humble, yet powerful, HTTP client. And our primary tools are Timeouts and Retries.

The First Stroke: The Art of the Graceful Timeout

A request without a timeout is a promise with no expiration date. It's a resource leak waiting to happen, a potential zombie process clinging to your event loop.

In the early days, we might have used the basic http module. Now, as senior artisans, we choose our tools wisely. axios is a popular, feature-rich brush. But even simpler, modern fetch (with a bit of help) can do the job.

Let's paint with axios first:

// A naive client - a sketch on a napkin
const axios = require('axios');
const response = await axios.get('https://unreliable-api.com/data');
Enter fullscreen mode Exit fullscreen mode

This is fragile. Let's apply the first layer of resilience: timeouts.

// A client with a backbone - the first stroke of the master
const resilientClient = axios.create({
  timeout: 5000, // 5 seconds. Our patience is not infinite.
  timeoutErrorMessage: 'The cosmos of the remote API remains elusive. Our request has timed out.',
});
Enter fullscreen mode Exit fullscreen mode

This single, elegant line does wonders. It sets a boundary. It says, "I value my application's responsiveness more than your API's potential eventual response."

But why stop there? We can craft an even more nuanced timeout strategy.

// A symphony of timeouts
const { CancelToken } = axios;
const source = CancelToken.source();

// Set a timeout for the entire request
const timeout = setTimeout(() => {
  source.cancel('Operation canceled by the artist. The canvas dried.');
}, 5000);

try {
  const response = await axios.get('https://slow-api.com/data', {
    cancelToken: source.token,
    // You can also set a specific timeout for the socket connection phase
    // socketTimeout: 3000,
  });
  clearTimeout(timeout);
} catch (error) {
  if (axios.isCancel(error)) {
    console.log('Request gracefully canceled:', error.message);
  } else {
    // Handle other errors
  }
}
Enter fullscreen mode Exit fullscreen mode

This is the art of the graceful exit. We're not just letting the request hang; we're consciously deciding to end it, freeing up the socket and the memory, allowing our application to move on.

The Second Layer: The Dance of the Strategic Retry

A timeout is a final act. But what if the failure is transient? A blip, a momentary overload? This is where we introduce the dance: the retry.

Retries are not about brute force. They are a thoughtful, strategic dance. We must be careful not to become the very DDoS attack we're trying to survive.

Our weapon of choice here is the async-retry library, or the built-in resilience of got. Let's see the dance in action with axios-retry.

const axios = require('axios');
const axiosRetry = require('axios-retry');

// Configure our client artist
const client = axios.create({ timeout: 8000 });
axiosRetry(client, {
  retries: 3,
  retryDelay: axiosRetry.exponentialDelay, // The heart of the strategy
  retryCondition: (error) => {
    // A true artist is selective about their strokes.
    // Only retry on specific, likely transient errors.
    return axiosRetry.isNetworkOrIdempotentRequestError(error) || error.response?.status >= 500;
  },
  onRetry: (retryCount, error, requestConfig) => {
    console.log(`🔄 Strike ${retryCount}! We dance again because of: ${error.code}`);
  }
});

// Now, our request is a performance
await client.get('https://flaky-api.com/essential-data');
Enter fullscreen mode Exit fullscreen mode

Let's break down this choreography:

  1. exponentialDelay: This is the soul of the strategy. It doesn't retry immediately. It waits 100ms, then 200ms, then 400ms... This "backing off" gives the struggling service room to breathe and recover. It's polite and effective.
  2. retryCondition: We don't retry on a 4xx (client error). If the request is BAD_REQUEST, retrying is futile. We only retry on network errors or server errors (5xx), where the problem is likely not our fault and might be temporary.
  3. onRetry: This is our log, the record of our dance. It allows for observability, so we know when and why our resilience is being tested.

The Masterpiece: Composing the Full Artwork

Individually, timeouts and retries are powerful. Combined, they are a symphony. But a true senior developer thinks about the entire system.

What about idempotency? Are you retrying a POST request that might create two orders? Be careful. Use idempotency keys or only retry safe methods (GET, HEAD, OPTIONS) by default.

What about circuit breakers? After N failures, you should "trip the circuit" and stop making requests for a period, failing fast and giving the service a complete break. This is the next level of resilience, using libraries like opossum.

Here is a more complete, production-ready sketch:

const axios = require('axios');
const axiosRetry = require('axios-retry');

const createResilientClient = (baseURL) => {
  const client = axios.create({
    baseURL,
    timeout: 10000,
    headers: { 'User-Agent': 'MyResilientApp/1.0' }
  });

  axiosRetry(client, {
    retries: 3,
    retryDelay: (retryCount) => {
      const delay = Math.pow(2, retryCount) * 100; // 200, 400, 800ms
      console.log(`Retry #${retryCount}. Waiting ${delay}ms`);
      return delay;
    },
    retryCondition: (error) => {
      // No retry on 4xx, only on network issues or 5xx errors.
      return !error.response || error.response.status >= 500;
    },
  });

  return client;
}

// Using our forged steel client
const reliableApi = createResilientClient('https://critical-external-service.com');

const fetchData = async () => {
  try {
    const { data } = await reliableApi.get('/data');
    return data;
  } catch (error) {
    // This error could be a timeout AFTER 3 retries, or a 400, etc.
    // Handle it with the wisdom of someone who knew it might happen.
    console.error('All strategies exhausted. Gracefully degrading...');
    return getCachedData(); // Your fallback strategy
  }
};
Enter fullscreen mode Exit fullscreen mode

The Gallery Wall: Observability

Your artwork is useless in a dark room. You must illuminate it. Log your timeouts. Log your retries. Metric them. Graph them.

  • How many requests are timing out?
  • What's your retry rate?
  • Which endpoints are the most flaky?

This data isn't just for debugging; it's for convincing your team, your product manager, and even the third-party vendor, about the reality of the system's interactions.

The Journey's End is a New Beginning

Building resilient HTTP clients is not a task you check off. It is a mindset. It is the art of anticipating failure and weaving elegance into your response.

You are not just a coder; you are an artisan forging steel in the fire of distributed systems. You move from being a victim of the network's chaos to being a master of your application's destiny.

Now, go forth. Look at your clients. Are they naively hopeful, or are they resiliently graceful? It's time to pick up your tools and start forging.

Your masterpiece awaits.

Top comments (0)