# At most one HTTP client

- **Pattern:** `ab-000225` (`ai-slop-code-drift.tooling-stack-drift.dual-http-client`)
- **Severity:** high
- **Lifecycle:** active
- **Last modified:** 2026-04-18
- **Canonical URL:** https://auditbuffet.com/patterns/ab-000225
- **License:** CC-BY-4.0 — attribute to AuditBuffet Pattern Catalog (https://auditbuffet.com/patterns/ab-000225)

## Why it matters

Two HTTP clients create two error-handling conventions, two interceptor pipelines, two retry policies, and two authentication header-injection paths. A token refresh that works in your axios interceptor silently skips every ky call, producing inconsistent 401 behavior that depends on which file made the request. Request/response logging captures only half your traffic, and timeout defaults differ (axios has no default timeout, ky defaults to 10 seconds) creating unpredictable production behavior.

## Severity rationale

High because inconsistent auth-header injection and timeout behavior produce hard-to-reproduce production incidents.

## Remediation

Wrap your chosen client once and import only that wrapper everywhere.

```ts
// src/lib/http.ts
import ky from 'ky'
export const http = ky.extend({
  prefixUrl: '/api',
  timeout: 10000,
  hooks: { beforeRequest: [addAuthHeader] },
})
```

Rewrite every axios call site to use `import { http } from '@/lib/http'`, then `npm uninstall axios`. Add an ESLint rule forbidding direct imports of both libraries outside `src/lib/http.ts`.

## Detection

- **ID:** `dual-http-client`
- **Severity:** `high`
- **What to look for:** Apply the three-condition rule against this exact HTTP client allowlist: `axios`, `ky`, `got`, `node-fetch`, `cross-fetch`, `isomorphic-fetch`, `superagent`, `request` (deprecated), `phin`, `undici`. Note that `fetch` (built into Node 18+ and all browsers) is NOT a package — do not count it. Count all REMAINING libraries from the allowlist. EXCEPT: if the project legitimately needs both browser fetch and a Node-side library (e.g., `undici` for HTTP/2 streaming), check if the Node-side library is isolated to a specific server-only directory like `app/api/` or `server/` AND is only imported by ≤3 files — if so, treat it as an escape hatch and pass.
- **Pass criteria:** 0 or 1 HTTP client libraries from the allowlist actively used. Report even on pass: "Canonical HTTP client: [name or 'fetch (built-in)'] ([N] importing files)."
- **Fail criteria:** 2 or more HTTP client libraries from the allowlist meet all three conditions (at least 1 non-escape-hatch importing file) and the escape-hatch exception does not apply.
- **Skip (N/A) when:** 0 HTTP client libraries from the allowlist appear in `RUNTIME_DEPS`.
- **Detail on fail:** `"2 active HTTP clients: 'axios' (15 files) AND 'ky' (6 files). Pick one and convert the rest."`
- **Remediation:** Two HTTP clients means two error-handling patterns, two interceptor systems, two retry strategies. Pick one and migrate:

  ```ts
  // Bad: some files use axios, others use ky
  // Good: a single src/lib/http.ts wrapper around your chosen client

  // src/lib/http.ts
  import ky from 'ky'
  export const http = ky.extend({ prefixUrl: '/api' })

  // Then everywhere else:
  import { http } from '@/lib/http'
  ```

  After migration, `npm uninstall axios`.

Taxons: dependency-coherence

HTML version: https://auditbuffet.com/patterns/ab-000225
