Require Timeouts and Bounded Retries on Node.js HTTP Calls
Flags fetch, axios, and http.request calls with no timeout, and retry loops without a cap, backoff, or idempotency key in Node.js services.
What it flags
type Coordinates = { lat: number; lng: number };
const GEOCODER_URL = 'https://geocoder.internal/v1/lookup';
export async function geocode(address: string): Promise<Coordinates> {
const url = `${GEOCODER_URL}?q=${encodeURIComponent(address)}`;
while (true) {
// No deadline, no attempt cap, no backoff: a slow upstream stalls every caller
const res = await fetch(url);
if (res.ok) return (await res.json()) as Coordinates;
}
}import { setTimeout as sleep } from 'node:timers/promises';
type Coordinates = { lat: number; lng: number };
const GEOCODER_URL = 'https://geocoder.internal/v1/lookup';
const MAX_ATTEMPTS = 3;
export async function geocode(address: string): Promise<Coordinates> {
const url = `${GEOCODER_URL}?q=${encodeURIComponent(address)}`;
for (let attempt = 1; ; attempt++) {
const res = await fetch(url, { signal: AbortSignal.timeout(2_000) });
if (res.ok) return (await res.json()) as Coordinates;
await res.body?.cancel();
const retryable = res.status === 429 || res.status >= 500;
if (!retryable || attempt === MAX_ATTEMPTS) {
throw new Error(`Geocoder responded ${res.status}`);
}
await sleep(200 * 2 ** attempt + Math.random() * 100);
}
}Why it matters
An upstream service that stops responding without closing the connection makes each call hold a socket, memory, and often a database connection while it waits. Node.js fetch applies no overall deadline by default and axios defaults to no timeout, so slow calls pile up until the service runs out of connections or memory. Retry loops without a cap or backoff then multiply the load on the dependency that is already struggling. The missing option is invisible in a diff, and tests pass against a fast local mock.
Agent instructions
cubic checks every pull request against these instructions. Paste them into AGENTS.md or Cursor rules and your AI coding tool follows them while it writes code.
AGENTS.md
Add this section to AGENTS.md at the repository root. Codex, Cursor, and other AI coding tools read it.
## Require Timeouts and Bounded Retries on Node.js HTTP Calls Applies to `**/*.js`, `**/*.mjs`, `**/*.cjs`, `**/*.ts`, `**/*.mts`, `**/*.cts`. Review Node.js outbound HTTP calls for missing deadlines and unsafe retries. Flag changes that: - Call `fetch` in request handlers, jobs, or queue consumers without a `signal`, such as `AbortSignal.timeout(ms)`. - Create axios instances or make axios requests without a `timeout` or `signal`; axios applies no timeout by default. - Call `http.request` or `https.request` without a `timeout` or `signal`, or set a `timeout` or call `req.setTimeout()` without destroying the request in the `'timeout'` handler; the option only emits an event. - Clear a manual abort timer as soon as `fetch` resolves, which leaves `res.json()`, `res.text()`, or body streaming with no overall deadline. - Retry in a loop without a maximum attempt count, or without backoff and jitter between attempts. - Retry non-idempotent requests, such as payments, order creation, or emails, without an idempotency key, or retry 4xx responses other than 408 and 429. Do not flag: - Calls through a shared client or wrapper that already sets a timeout. - Long downloads or streaming requests with a deliberately large, explicit timeout. - SDK clients configured with their own timeout option at construction.
Checks files matching **/*.js, **/*.mjs, **/*.cjs, **/*.ts, **/*.mts, **/*.cts.
More Node.js agents
See all Node.js code review standards
Run this agent on every pull request
cubic reviews every pull request against the agents you turn on and comments on the lines that break them.