HTTP clients
The Apify API client uses a pluggable HTTP layer. It ships with an axios-based default and accepts custom implementations.
Default HTTP client
When you create an ApifyClient, it sends its requests through the built-in AxiosHttpClient. This default client provides:
- Automatic retries with exponential backoff for network errors, HTTP 429 and HTTP 5xx responses.
- Configurable timeout tiers that grow with every retry.
- Request compression and preparation of API-compatible bodies, query parameters and headers, including authentication.
- Keep-alive connections and proxy support through the
HTTP_PROXY,HTTPS_PROXYandNO_PROXYenvironment variables in Node.js. - API error handling and request statistics.
You configure the default client through the ApifyClient constructor:
import { ApifyClient } from 'apify-client';
const client = new ApifyClient({
token: 'MY-APIFY-TOKEN',
maxRetries: 4,
minDelayBetweenRetriesMillis: 500,
timeoutMediumSecs: 60,
headers: { 'x-apify-integration-platform': 'my-platform' },
});
Anything beyond those options, such as axios request interceptors for a header computed per request, is configured on an AxiosHttpClient instance that you plug in yourself. Retries, timeouts and compression then live on the HTTP client too:
import { ApifyClient, AxiosHttpClient } from 'apify-client';
const httpClient = new AxiosHttpClient({
maxRetries: 4,
requestInterceptors: [
(config) => {
config.headers.set('X-Request-Id', crypto.randomUUID());
return config;
},
],
});
const client = ApifyClient.withCustomHttpClient({ token: 'MY-APIFY-TOKEN', httpClient });
Architecture
The HTTP client hierarchy has two layers:
HttpClientis the abstract base. It holds the shared request pipeline: header merging, body serialization and compression, query encoding, the retry loop, timeout growth, response body parsing, statistics and the conversion of error statuses toApifyApiError. It declares the transport hooks a concrete client implements.AxiosHttpClientextends it and adapts axios as the transport.
A custom client extends HttpClient the same way. It implements the transport hook and inherits everything else, so its requests get the same retries, timeouts, compression and error handling as the built-in client.
The HTTP clients and the request and response types are exported from the package root:
import { AxiosHttpClient, HttpClient } from 'apify-client';
import type { HttpRequest, HttpResponse } from 'apify-client';
HttpClient.isTimeoutError(error) is the transport-neutral way to tell whether an error is a timeout, so code built on the client doesn't need to know which transport threw it.
The transport contract
The public call() method provides the shared request pipeline. A concrete transport implements these hooks:
sendRequest(request)sends one prepared request and returns its response, error statuses included. It receives anHttpRequest: the URL with the query string already encoded into it, the headers with the client's default headers already merged in, the body already serialized and compressed, the timeout for this attempt in milliseconds (undefinedfor a request that runs without one), whether the caller wants the response body as a stream, and the caller'sAbortSignalwhen the call passed one. The inheritedcall()needs it, so every transport has to implement it. Let the HTTP library's errors propagate unwrapped, and leave status handling andApifyApiErrortocall().isRetryableTransportError(error)classifies transport failures for the shared retry loop. The default classifies nothing as retryable, so a transport that doesn't override it gives up on the first connection failure.isTimeoutError(error)identifies the transport's timeout errors. The default recognizes errors namedTimeoutError, which is whatAbortSignal.timeout()produces. Timeout classification is independent of retryability, so a timeout the retry loop should retry has to be covered byisRetryableTransportError()too.close()releases resources owned by the transport, such as a connection pool. The default does nothing.
Mark your implementations with the override keyword, as the built-in axios client does, so the TypeScript compiler catches a misspelled hook.
Request and response shapes
sendRequest() receives an HttpRequest:
| Field | Description |
|---|---|
method: HttpMethod | HTTP method, for example GET or POST |
url: string | Full URL, query string included |
headers: Record<string, string> | Final request headers |
body?: string | Buffer | ArrayBuffer | ArrayBufferView | Readable | Serialized and compressed body, or undefined |
timeoutMillis?: number | Timeout of this attempt, or undefined for a request that runs without one |
stream: boolean | Whether to return the body unread, as a Readable |
signal?: AbortSignal | The caller's abort signal, to end the request in flight with. The pipeline stops retrying once it aborts |
It returns an HttpResponse. It is an interface, not a class, so any object of this shape will do:
| Field | Description |
|---|---|
status: number | HTTP status code |
headers: Record<string, string | string[] | undefined> | Response headers keyed by lowercase name |
body: Buffer | ArrayBuffer | Readable | undefined | Raw body, a Readable for a streamed response, or undefined |
The pipeline decodes the body by its content type for the resource clients, so the transport returns raw bytes. For a streamed response, return the body unread and let the caller consume it.
Plugging it in
Use ApifyClient.withCustomHttpClient() to create a client with your implementation. The token you pass is set as the HTTP client's Authorization header, unless the client already has one configured:
import { ApifyClient, HttpClient } from 'apify-client';
import type { HttpRequest, HttpResponse } from 'apify-client';
class MyHttpClient extends HttpClient {
override async sendRequest(request: HttpRequest): Promise<HttpResponse> {
// Send the request with the HTTP library of your choice.
throw new Error('Not implemented');
}
override isRetryableTransportError(error: unknown): boolean {
// List the transport's transient failures here, such as its timeout and connection errors.
// Returning false for everything opts out of transport retries entirely.
return this.isTimeoutError(error);
}
}
const client = ApifyClient.withCustomHttpClient({
token: 'MY-APIFY-TOKEN',
httpClient: new MyHttpClient(),
});
After that, all API calls made through the client go through your HTTP client.
If you override call() itself, your implementation becomes responsible for request preparation, retries, timeouts, API error conversion and statistics. It also has to send the client's default headers with every request, otherwise the Authorization header never reaches the API. Implementing the transport hooks and inheriting call() keeps the shared behavior.
Use cases
Custom HTTP clients might be useful when the built-in axios client doesn't cover your requirements, for example when you need to:
- Use a different HTTP library - Integrate
fetch, undici, or another transport. - Route through a proxy - Add proxy support or request routing the environment variables can't express.
- Implement custom retry logic - Use different backoff strategies or retry conditions.
- Log requests and responses - Track API calls for debugging or auditing.
- Modify requests - Add custom fields, modify the body, or change headers.
- Collect custom metrics - Measure request latency, track error rates, or count API calls.
For a complete implementation over fetch, see Build a custom HTTP client. The HttpClient API reference documents the full contract.