HTTP Client¶
Overview¶
Delta uses Alova as the HTTP client library, wrapped behind library-agnostic interfaces. Shared logic (Alova instance creation, response handling, error type guard) lives in common.ts. The server wrapper adds retry support; the client wrapper is a thin passthrough.
Key files:
- Types:
packages/app/src/types/httpClient.ts - Shared base:
packages/app/src/lib/httpClient/common.ts - Server wrapper:
packages/app/src/lib/httpClient/server/index.ts - Client wrapper:
packages/app/src/lib/httpClient/client/index.ts
Architecture¶
common.ts (shared)
├── isHttpError() — type guard
├── handleResponse() — throws HttpError on non-2xx, parses by content-type
└── createHttpAlova() — creates Alova instance with shared response handler
server/index.ts (Node.js only)
├── imports createHttpAlova from common
├── adds retry support (alova/server — Node-only)
│ ├── retryAfter — provider-specific callback
│ └── retry — generic exponential backoff
└── exports httpClientServer
client/index.ts (browser-safe)
├── imports createHttpAlova from common
└── exports httpClient (no retry, no Node-only deps)
Server-Side Usage¶
Always use httpClientServer — never the raw Alova instance directly.
import { httpClientServer } from '@/lib/httpClient/server';
// GET
const data = await httpClientServer.get<MyType>('/api/resource', {
headers: { Authorization: `Bearer ${token}` },
});
// POST
const result = await httpClientServer.post<MyType>('/api/resource', body, {
headers: { 'Content-Type': 'application/json' },
});
// PUT, DELETE follow the same pattern
Client-Side Usage¶
Use httpClient for browser-side requests. It shares the same response handling (throws HttpError on non-2xx) but has no retry support.
import { httpClient } from '@/lib/httpClient/client';
const data = await httpClient.post('/api/endpoint', payload, {
headers: { 'Content-Type': 'application/json' },
});
Error Handling¶
HttpError¶
Both server and client throw an HttpError on non-2xx responses with:
status— HTTP status code (e.g., 429, 404, 500)body— Parsed JSON response body (if available)responseHeaders— The fullHeadersobject from the responsemessage— e.g.,"HTTP 429: Too Many Requests"
isHttpError Type Guard¶
Import isHttpError from @/lib/httpClient/common:
import { isHttpError } from '@/lib/httpClient/common';
try {
await httpClientServer.get('/api/resource');
} catch (error) {
if (isHttpError(error)) {
console.error(`HTTP ${error.status}:`, error.body);
const retryAfter = error.responseHeaders.get('Retry-After');
}
}
Retry Mechanisms (Server Only)¶
The server wrapper supports two independent retry mechanisms, configured via HttpRequestConfig:
Layer 1: retryAfter (Provider-Specific)¶
An optional callback that receives an HttpError and returns a delay in seconds to wait before retrying once, or null to skip.
Use this when the API returns rate limit headers (e.g., X-Ratelimit-Reset epoch timestamp).
import type { HttpError } from '@/types/httpClient';
import { differenceInSeconds, fromUnixTime } from 'date-fns';
function myRetryAfter(error: HttpError): number | null {
if (error.status !== 429) return null;
const resetEpoch = error.responseHeaders.get('X-Ratelimit-Reset');
if (!resetEpoch) return null;
const resetDate = fromUnixTime(Number(resetEpoch));
const delaySec = differenceInSeconds(resetDate, new Date());
return delaySec > 0 ? delaySec : null;
}
await httpClientServer.get('/api/resource', {
retryAfter: myRetryAfter,
});
Layer 2: retry (Generic Exponential Backoff)¶
Alova's built-in retry with static exponential backoff. Use createProviderRetryConfig from processors/common.ts for a pre-configured 429 retry:
import { createProviderRetryConfig } from '@/services/integration/processors/common';
const retryConfig = createProviderRetryConfig();
// Default: retries on 429, 2s initial delay, 2x multiplier (2s, 4s, 8s)
await httpClientServer.get('/api/resource', {
retry: retryConfig,
});
Or configure manually:
await httpClientServer.get('/api/resource', {
retry: {
retry: 3, // or (error) => error.status === 429
backoff: { delay: 1000, multiplier: 2 },
},
});
Using Both Together¶
When both are provided, Alova's retry runs first (exponential backoff). If it exhausts all retries and the error is still a 429, retryAfter gets a final chance to wait for the exact reset time and retry once more.
// Companies House example: 600 req/5min, X-Ratelimit-Reset header
await httpClientServer.get(url, {
headers: { Authorization: authHeader },
retryAfter: companiesHouseRetryAfter, // reads X-Ratelimit-Reset epoch
retry: companiesHouseRetryConfig, // generic 429 backoff
});
HttpRequestConfig¶
| Property | Type | Description |
|---|---|---|
headers |
Record<string, string> |
Request headers |
transform |
(data, headers) => unknown |
Transform response data |
retryAfter |
(error: HttpError) => number \| null |
Provider-specific retry delay in seconds |
retry |
HttpRetryConfig |
Generic exponential backoff config (server only) |
Testing¶
Mocking the server HTTP client¶
jest.mock('@/lib/httpClient/common', () =>
jest.requireActual('@/lib/httpClient/__mocks__/common')
);
jest.mock('@/lib/httpClient/server', () =>
jest.requireActual('@/lib/httpClient/server/__mocks__/serverHttp')
);
import { isHttpError } from '@/lib/httpClient/common';
import { httpClientServer } from '@/lib/httpClient/server';
const mockHttpGet = httpClientServer.get as jest.MockedFunction<
typeof httpClientServer.get
>;
Mocking the client HTTP client¶
jest.mock('@/lib/httpClient/common', () =>
jest.requireActual('@/lib/httpClient/__mocks__/common')
);
jest.mock('@/lib/httpClient/client');
import { isHttpError } from '@/lib/httpClient/common';
import { httpClient } from '@/lib/httpClient/client';
Re-applying isHttpError after resetMocks¶
Jest's resetMocks: true strips jest.fn() implementations between tests. If your code calls isHttpError internally (e.g., createProviderRetryConfig), re-apply it:
beforeEach(() => {
const { isHttpError } = jest.requireMock<
typeof import('@/lib/httpClient/common')
>('@/lib/httpClient/common');
(isHttpError as unknown as jest.Mock).mockImplementation(
(error: unknown) =>
error instanceof Error && 'status' in error && 'responseHeaders' in error
);
});
Package Usage¶
| Package | HTTP Client | Notes |
|---|---|---|
| App (server) | httpClientServer |
API routes, server components |
| App (client) | httpClient |
Browser-side requests |
| AI Hub | Alova | External API calls, longer timeouts |
| Sana | Alova | WhatsApp API communication |
| Lambda Functions | AWS SDK | Do NOT use Alova |
Resources¶
- Alova Documentation
- Alova Server Hooks (retry, rate limiter)