Skip to content

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 full Headers object from the response
  • message — 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