apify-client 3.0.0-beta.2 → 3.0.0-beta.21
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +7 -3
- package/dist/apify_api_error.d.ts +60 -19
- package/dist/apify_api_error.js +88 -22
- package/dist/apify_client.d.ts +22 -3
- package/dist/apify_client.js +37 -31
- package/dist/base/api_client.d.ts +6 -6
- package/dist/base/api_client.js +24 -16
- package/dist/base/resource_client.d.ts +23 -9
- package/dist/base/resource_client.js +49 -25
- package/dist/base/resource_collection_client.d.ts +7 -4
- package/dist/base/resource_collection_client.js +21 -15
- package/dist/body_parser.d.ts +3 -4
- package/dist/body_parser.js +17 -24
- package/dist/bundle.js +38 -48
- package/dist/bundle.js.map +1 -1
- package/dist/generated/api.d.ts +287 -214
- package/dist/generated/schemas.d.ts +6699 -0
- package/dist/generated/schemas.js +1521 -0
- package/dist/http_client.d.ts +18 -51
- package/dist/http_client.js +90 -85
- package/dist/index.d.ts +3 -1
- package/dist/index.js +2 -1
- package/dist/interceptors.js +24 -6
- package/dist/lazy_schema.d.ts +8 -0
- package/dist/lazy_schema.js +11 -0
- package/dist/models.d.ts +32 -52
- package/dist/resource_clients/actor.d.ts +46 -31
- package/dist/resource_clients/actor.js +71 -53
- package/dist/resource_clients/actor_collection.d.ts +9 -5
- package/dist/resource_clients/actor_collection.js +12 -3
- package/dist/resource_clients/actor_env_var.d.ts +10 -3
- package/dist/resource_clients/actor_env_var.js +17 -6
- package/dist/resource_clients/actor_env_var_collection.d.ts +14 -18
- package/dist/resource_clients/actor_env_var_collection.js +18 -11
- package/dist/resource_clients/actor_version.d.ts +18 -3
- package/dist/resource_clients/actor_version.js +20 -9
- package/dist/resource_clients/actor_version_collection.d.ts +16 -17
- package/dist/resource_clients/actor_version_collection.js +20 -11
- package/dist/resource_clients/build.d.ts +18 -6
- package/dist/resource_clients/build.js +35 -17
- package/dist/resource_clients/build_collection.d.ts +3 -1
- package/dist/resource_clients/build_collection.js +5 -1
- package/dist/resource_clients/dataset.d.ts +23 -9
- package/dist/resource_clients/dataset.js +70 -48
- package/dist/resource_clients/dataset_collection.d.ts +6 -2
- package/dist/resource_clients/dataset_collection.js +13 -2
- package/dist/resource_clients/key_value_store.d.ts +48 -15
- package/dist/resource_clients/key_value_store.js +71 -40
- package/dist/resource_clients/key_value_store_collection.d.ts +6 -2
- package/dist/resource_clients/key_value_store_collection.js +13 -2
- package/dist/resource_clients/log.d.ts +9 -19
- package/dist/resource_clients/log.js +57 -45
- package/dist/resource_clients/request_queue.d.ts +56 -26
- package/dist/resource_clients/request_queue.js +178 -113
- package/dist/resource_clients/request_queue_collection.d.ts +6 -2
- package/dist/resource_clients/request_queue_collection.js +10 -3
- package/dist/resource_clients/run.d.ts +51 -17
- package/dist/resource_clients/run.js +85 -44
- package/dist/resource_clients/run_collection.d.ts +3 -1
- package/dist/resource_clients/run_collection.js +5 -1
- package/dist/resource_clients/schedule.d.ts +16 -7
- package/dist/resource_clients/schedule.js +31 -21
- package/dist/resource_clients/schedule_collection.d.ts +6 -2
- package/dist/resource_clients/schedule_collection.js +10 -3
- package/dist/resource_clients/store_collection.d.ts +3 -1
- package/dist/resource_clients/store_collection.js +5 -1
- package/dist/resource_clients/task.d.ts +28 -10
- package/dist/resource_clients/task.js +67 -44
- package/dist/resource_clients/task_collection.d.ts +6 -2
- package/dist/resource_clients/task_collection.js +10 -3
- package/dist/resource_clients/user.d.ts +17 -8
- package/dist/resource_clients/user.js +40 -39
- package/dist/resource_clients/webhook.d.ts +14 -5
- package/dist/resource_clients/webhook.js +30 -22
- package/dist/resource_clients/webhook_collection.d.ts +6 -2
- package/dist/resource_clients/webhook_collection.js +10 -3
- package/dist/resource_clients/webhook_dispatch.d.ts +4 -1
- package/dist/resource_clients/webhook_dispatch.js +8 -2
- package/dist/resource_clients/webhook_dispatch_collection.d.ts +3 -1
- package/dist/resource_clients/webhook_dispatch_collection.js +5 -1
- package/dist/response_validation_error.d.ts +26 -0
- package/dist/response_validation_error.js +37 -0
- package/dist/runtime/node.d.ts +6 -0
- package/dist/runtime/node.js +58 -0
- package/dist/runtime/types.d.ts +50 -0
- package/dist/runtime/types.js +1 -0
- package/dist/runtime/web.d.ts +5 -0
- package/dist/runtime/web.js +17 -0
- package/dist/schemas.d.ts +15 -0
- package/dist/schemas.js +15 -0
- package/dist/statistics.d.ts +1 -6
- package/dist/statistics.js +2 -2
- package/dist/timeouts.d.ts +72 -0
- package/dist/timeouts.js +31 -0
- package/dist/utils.d.ts +75 -55
- package/dist/utils.js +201 -180
- package/package.json +42 -35
- package/dist/argument_validation_error.d.ts +0 -17
- package/dist/argument_validation_error.js +0 -153
package/dist/http_client.d.ts
CHANGED
|
@@ -4,72 +4,36 @@ import type { AxiosInstance, AxiosRequestConfig, AxiosResponse, InternalAxiosReq
|
|
|
4
4
|
import type { Log } from '@apify/log';
|
|
5
5
|
import type { RequestInterceptorFunction } from './interceptors.js';
|
|
6
6
|
import type { Statistics } from './statistics.js';
|
|
7
|
+
import type { Timeout, TimeoutTier } from './timeouts.js';
|
|
7
8
|
export declare class HttpClient {
|
|
9
|
+
#private;
|
|
8
10
|
stats: Statistics;
|
|
9
11
|
maxRetries: number;
|
|
10
12
|
minDelayBetweenRetriesMillis: number;
|
|
11
13
|
userProvidedRequestInterceptors: RequestInterceptorFunction[];
|
|
12
14
|
logger: Log;
|
|
13
|
-
|
|
15
|
+
/** Duration of each timeout tier, in milliseconds. */
|
|
16
|
+
timeoutMillis: Record<TimeoutTier, number>;
|
|
17
|
+
/** Cap on the timeout of a single request attempt, in milliseconds. */
|
|
18
|
+
timeoutMaxMillis: number;
|
|
14
19
|
httpAgent?: http.Agent;
|
|
15
20
|
httpsAgent?: https.Agent;
|
|
16
21
|
axios: AxiosInstance;
|
|
17
22
|
workflowKey?: string;
|
|
18
|
-
private nodeInitPromise?;
|
|
19
|
-
private userAgentSuffix?;
|
|
20
23
|
constructor(options: HttpClientOptions);
|
|
21
|
-
private ensureNodeInit;
|
|
22
|
-
private initNode;
|
|
23
24
|
call<T = any>(config: ApifyRequestConfig): Promise<ApifyResponse<T>>;
|
|
24
|
-
private _informAboutStreamNoRetry;
|
|
25
|
-
/**
|
|
26
|
-
* Successful responses are returned, errors and unsuccessful
|
|
27
|
-
* status codes are retried. See the following functions for the
|
|
28
|
-
* retrying logic.
|
|
29
|
-
*/
|
|
30
|
-
private _createRequestHandler;
|
|
31
|
-
private _isStatusOk;
|
|
32
|
-
/**
|
|
33
|
-
* Handles all unexpected errors that can happen, but are not
|
|
34
|
-
* Apify API typed errors. E.g. network errors, timeouts and so on.
|
|
35
|
-
*/
|
|
36
|
-
private _handleRequestError;
|
|
37
|
-
/**
|
|
38
|
-
* Axios calls req.abort() on timeouts so timeout errors will
|
|
39
|
-
* have a code ECONNABORTED.
|
|
40
|
-
*/
|
|
41
|
-
private _isTimeoutError;
|
|
42
|
-
/**
|
|
43
|
-
* We don't want to retry every exception thrown from Axios.
|
|
44
|
-
* The common denominator for retryable errors are network issues.
|
|
45
|
-
* @param {Error} err
|
|
46
|
-
* @private
|
|
47
|
-
*/
|
|
48
|
-
private _isRetryableError;
|
|
49
|
-
/**
|
|
50
|
-
* When a network connection to our API is interrupted in the middle of streaming
|
|
51
|
-
* a response, the request often does not fail, but simply contains
|
|
52
|
-
* an incomplete response. This can often be fixed by retrying.
|
|
53
|
-
*/
|
|
54
|
-
private _isResponseBodyInvalid;
|
|
55
|
-
/**
|
|
56
|
-
* When a network request is attempted by axios and fails,
|
|
57
|
-
* it throws an AxiosError, which will have the request
|
|
58
|
-
* and config (and other) properties.
|
|
59
|
-
*/
|
|
60
|
-
private _isNetworkError;
|
|
61
|
-
/**
|
|
62
|
-
* We retry 429 (rate limit) and 500+.
|
|
63
|
-
* For status codes 300-499 (except 429) we do not retry the request,
|
|
64
|
-
* because it's probably caused by invalid url (redirect 3xx) or invalid user input (4xx).
|
|
65
|
-
*/
|
|
66
|
-
private _isStatusCodeRetryable;
|
|
67
|
-
private _onRequestRetry;
|
|
68
25
|
}
|
|
69
|
-
export interface ApifyRequestConfig extends AxiosRequestConfig {
|
|
26
|
+
export interface ApifyRequestConfig extends Omit<AxiosRequestConfig, 'timeout'> {
|
|
70
27
|
stringifyFunctions?: boolean;
|
|
71
28
|
forceBuffer?: boolean;
|
|
72
29
|
doNotRetryTimeouts?: boolean;
|
|
30
|
+
/**
|
|
31
|
+
* Timeout of the request: a tier name, a number of seconds, or `'noTimeout'`. The client resolves it to
|
|
32
|
+
* the axios `timeout` in milliseconds for each attempt, before axios runs its interceptors, so a request
|
|
33
|
+
* interceptor already sees a number of milliseconds.
|
|
34
|
+
* @default 'medium'
|
|
35
|
+
*/
|
|
36
|
+
timeoutSecs?: Timeout;
|
|
73
37
|
}
|
|
74
38
|
export interface ApifyResponse<T = any> extends AxiosResponse<T> {
|
|
75
39
|
config: ApifyRequestConfig & InternalAxiosRequestConfig;
|
|
@@ -79,7 +43,10 @@ export interface HttpClientOptions {
|
|
|
79
43
|
maxRetries: number;
|
|
80
44
|
minDelayBetweenRetriesMillis: number;
|
|
81
45
|
requestInterceptors: RequestInterceptorFunction[];
|
|
82
|
-
|
|
46
|
+
timeoutShortSecs: number;
|
|
47
|
+
timeoutMediumSecs: number;
|
|
48
|
+
timeoutLongSecs: number;
|
|
49
|
+
timeoutMaxSecs: number;
|
|
83
50
|
logger: Log;
|
|
84
51
|
token?: string;
|
|
85
52
|
workflowKey?: string;
|
package/dist/http_client.js
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import retry from 'async-retry';
|
|
2
2
|
import axios, { AxiosHeaders } from 'axios';
|
|
3
3
|
import { APIFY_ENV_VARS } from '@apify/consts';
|
|
4
|
+
import { concatStreamToBuffer } from '@apify/utilities';
|
|
4
5
|
import { ApifyApiError } from './apify_api_error.js';
|
|
5
6
|
import { InvalidResponseBodyError, requestInterceptors, responseInterceptors } from './interceptors.js';
|
|
6
|
-
import {
|
|
7
|
-
|
|
7
|
+
import { runtime } from '#runtime';
|
|
8
|
+
import { asArray, cast, getEnv, isStream, version } from './utils.js';
|
|
8
9
|
const RATE_LIMIT_EXCEEDED_STATUS_CODE = 429;
|
|
9
10
|
export class HttpClient {
|
|
10
11
|
stats;
|
|
@@ -12,24 +13,29 @@ export class HttpClient {
|
|
|
12
13
|
minDelayBetweenRetriesMillis;
|
|
13
14
|
userProvidedRequestInterceptors;
|
|
14
15
|
logger;
|
|
16
|
+
/** Duration of each timeout tier, in milliseconds. */
|
|
15
17
|
timeoutMillis;
|
|
18
|
+
/** Cap on the timeout of a single request attempt, in milliseconds. */
|
|
19
|
+
timeoutMaxMillis;
|
|
16
20
|
httpAgent;
|
|
17
21
|
httpsAgent;
|
|
18
22
|
axios;
|
|
19
23
|
workflowKey;
|
|
20
|
-
|
|
21
|
-
userAgentSuffix;
|
|
24
|
+
#httpAgentsPromise;
|
|
22
25
|
constructor(options) {
|
|
23
26
|
const { token } = options;
|
|
24
27
|
this.stats = options.apifyClientStats;
|
|
25
28
|
this.maxRetries = options.maxRetries;
|
|
26
29
|
this.minDelayBetweenRetriesMillis = options.minDelayBetweenRetriesMillis;
|
|
27
30
|
this.userProvidedRequestInterceptors = options.requestInterceptors;
|
|
28
|
-
this.timeoutMillis =
|
|
31
|
+
this.timeoutMillis = {
|
|
32
|
+
short: options.timeoutShortSecs * 1000,
|
|
33
|
+
medium: options.timeoutMediumSecs * 1000,
|
|
34
|
+
long: options.timeoutLongSecs * 1000,
|
|
35
|
+
};
|
|
36
|
+
this.timeoutMaxMillis = options.timeoutMaxSecs * 1000;
|
|
29
37
|
this.logger = options.logger;
|
|
30
|
-
this.workflowKey = options.workflowKey ||
|
|
31
|
-
this.userAgentSuffix = options.userAgentSuffix;
|
|
32
|
-
this._onRequestRetry = this._onRequestRetry.bind(this);
|
|
38
|
+
this.workflowKey = options.workflowKey || getEnv(APIFY_ENV_VARS.WORKFLOW_KEY);
|
|
33
39
|
this.axios = axios.create({
|
|
34
40
|
// Disable axios's built-in proxy handling since we're using custom agents
|
|
35
41
|
proxy: false,
|
|
@@ -46,11 +52,14 @@ export class HttpClient {
|
|
|
46
52
|
return new URLSearchParams(formattedParams).toString();
|
|
47
53
|
},
|
|
48
54
|
validateStatus: null,
|
|
49
|
-
//
|
|
50
|
-
|
|
51
|
-
|
|
55
|
+
// Interceptors serialize requests and parse responses instead. Empty arrays rather than `undefined`,
|
|
56
|
+
// which axios fills in with its default transforms.
|
|
57
|
+
transformRequest: [],
|
|
58
|
+
transformResponse: [],
|
|
52
59
|
responseType: 'arraybuffer',
|
|
53
|
-
timeout
|
|
60
|
+
// Every request sets its own timeout in `createRequestHandler`, so the default only backs a raw
|
|
61
|
+
// `axios.request()` call.
|
|
62
|
+
timeout: this.timeoutMaxMillis,
|
|
54
63
|
// maxBodyLength needs to be Infinity, because -1 falls back to a 10 MB default
|
|
55
64
|
// from an axios subdependency - 'follow-redirects'
|
|
56
65
|
maxBodyLength: Infinity,
|
|
@@ -68,73 +77,43 @@ export class HttpClient {
|
|
|
68
77
|
if (token) {
|
|
69
78
|
this.axios.defaults.headers.Authorization = `Bearer ${token}`;
|
|
70
79
|
}
|
|
80
|
+
// Browsers do not let a page set the header, so it is only sent where the runtime describes its platform.
|
|
81
|
+
if (runtime.platform) {
|
|
82
|
+
const isAtHome = !!getEnv(APIFY_ENV_VARS.IS_AT_HOME);
|
|
83
|
+
let userAgent = `ApifyClient/${version} (${runtime.platform}); isAtHome/${isAtHome}`;
|
|
84
|
+
if (options.userAgentSuffix) {
|
|
85
|
+
userAgent += `; ${asArray(options.userAgentSuffix).join('; ')}`;
|
|
86
|
+
}
|
|
87
|
+
this.axios.defaults.headers['User-Agent'] = userAgent;
|
|
88
|
+
}
|
|
71
89
|
requestInterceptors.forEach((i) => this.axios.interceptors.request.use(i));
|
|
72
90
|
this.userProvidedRequestInterceptors.forEach((i) => this.axios.interceptors.request.use(i));
|
|
73
91
|
responseInterceptors.forEach((i) => this.axios.interceptors.response.use(i));
|
|
74
92
|
}
|
|
75
|
-
async
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
this.nodeInitPromise ??= this.initNode();
|
|
79
|
-
return this.nodeInitPromise;
|
|
93
|
+
async #ensureHttpAgents() {
|
|
94
|
+
this.#httpAgentsPromise ??= this.#initHttpAgents();
|
|
95
|
+
return this.#httpAgentsPromise;
|
|
80
96
|
}
|
|
81
|
-
async
|
|
82
|
-
|
|
97
|
+
async #initHttpAgents() {
|
|
98
|
+
const agents = await runtime.createHttpAgents({ timeoutMillis: this.timeoutMaxMillis });
|
|
99
|
+
if (!agents)
|
|
83
100
|
return;
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
// Enhanced agent configuration based on agentkeepalive best practices:
|
|
87
|
-
// - Nagle's algorithm disabled for lower latency
|
|
88
|
-
// - Free socket timeout to prevent socket leaks
|
|
89
|
-
// - LIFO scheduling to reuse recent sockets
|
|
90
|
-
// - Socket TTL for connection freshness
|
|
91
|
-
const agentOptions = {
|
|
92
|
-
keepAlive: true,
|
|
93
|
-
// Timeout for inactive sockets
|
|
94
|
-
// Prevents socket leaks from idle connections
|
|
95
|
-
timeout: this.timeoutMillis,
|
|
96
|
-
// Keep alive timeout for free sockets (15 seconds)
|
|
97
|
-
// Node.js will close unused sockets after this period
|
|
98
|
-
keepAliveMsecs: 15_000,
|
|
99
|
-
// Maximum number of sockets per host
|
|
100
|
-
maxSockets: 256,
|
|
101
|
-
maxFreeSockets: 256,
|
|
102
|
-
// LIFO scheduling - reuse most recently used sockets for better performance
|
|
103
|
-
scheduling: 'lifo',
|
|
104
|
-
};
|
|
105
|
-
// Use ProxyAgent which automatically detects proxy from environment variables
|
|
106
|
-
// and supports CONNECT tunneling
|
|
107
|
-
const proxyAgent = new ProxyAgent(agentOptions);
|
|
108
|
-
this.httpAgent = proxyAgent;
|
|
109
|
-
this.httpsAgent = proxyAgent;
|
|
110
|
-
// Disable Nagle's algorithm for lower latency
|
|
111
|
-
// This sends data immediately instead of buffering small packets
|
|
112
|
-
const setNoDelay = (socket) => {
|
|
113
|
-
socket.setNoDelay(true);
|
|
114
|
-
};
|
|
115
|
-
this.httpAgent.on('socket', setNoDelay);
|
|
116
|
-
this.httpsAgent.on('socket', setNoDelay);
|
|
101
|
+
this.httpAgent = agents.httpAgent;
|
|
102
|
+
this.httpsAgent = agents.httpsAgent;
|
|
117
103
|
this.axios.defaults.httpAgent = this.httpAgent;
|
|
118
104
|
this.axios.defaults.httpsAgent = this.httpsAgent;
|
|
119
|
-
// Works only in Node. Cannot be set in browser
|
|
120
|
-
const isAtHome = !!process.env[APIFY_ENV_VARS.IS_AT_HOME];
|
|
121
|
-
let userAgent = `ApifyClient/${version} (${os.platform()}; Node/${process.version}); isAtHome/${isAtHome}`;
|
|
122
|
-
if (this.userAgentSuffix) {
|
|
123
|
-
userAgent += `; ${asArray(this.userAgentSuffix).join('; ')}`;
|
|
124
|
-
}
|
|
125
|
-
this.axios.defaults.headers['User-Agent'] = userAgent;
|
|
126
105
|
}
|
|
127
106
|
async call(config) {
|
|
128
|
-
await this
|
|
107
|
+
await this.#ensureHttpAgents();
|
|
129
108
|
this.stats.calls++;
|
|
130
|
-
const makeRequest = this
|
|
109
|
+
const makeRequest = this.#createRequestHandler(config);
|
|
131
110
|
return retry(makeRequest, {
|
|
132
111
|
retries: this.maxRetries,
|
|
133
112
|
minTimeout: this.minDelayBetweenRetriesMillis,
|
|
134
|
-
onRetry: this
|
|
113
|
+
onRetry: (error, attempt) => this.#onRequestRetry(error, attempt),
|
|
135
114
|
});
|
|
136
115
|
}
|
|
137
|
-
|
|
116
|
+
#informAboutStreamNoRetry() {
|
|
138
117
|
this.logger.warningOnce('Request body was a stream - retrying will not work, as part of it was already consumed.');
|
|
139
118
|
this.logger.warningOnce('If you want Apify client to handle retries for you, collect the stream into a buffer before sending it.');
|
|
140
119
|
}
|
|
@@ -143,7 +122,8 @@ export class HttpClient {
|
|
|
143
122
|
* status codes are retried. See the following functions for the
|
|
144
123
|
* retrying logic.
|
|
145
124
|
*/
|
|
146
|
-
|
|
125
|
+
#createRequestHandler(config) {
|
|
126
|
+
const { timeoutSecs = 'medium', ...axiosConfig } = config;
|
|
147
127
|
const makeRequest = async (stopTrying, attempt) => {
|
|
148
128
|
this.stats.requests++;
|
|
149
129
|
let response;
|
|
@@ -153,24 +133,31 @@ export class HttpClient {
|
|
|
153
133
|
// Handling redirects is not possible without buffering - part of the stream has already been sent and can't be recovered
|
|
154
134
|
// when server sends the redirect. Therefore we need to override this in Axios config to prevent it from buffering the body.
|
|
155
135
|
// see also axios/axios#1045
|
|
156
|
-
|
|
136
|
+
axiosConfig.maxRedirects = 0;
|
|
157
137
|
}
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
138
|
+
response = await this.axios.request({
|
|
139
|
+
...axiosConfig,
|
|
140
|
+
timeout: this.#computeTimeoutMillis(timeoutSecs, attempt),
|
|
141
|
+
});
|
|
142
|
+
if (this.#isStatusOk(response.status))
|
|
162
143
|
return response;
|
|
144
|
+
// A failed request with `responseType: 'stream'` carries the API error body in the stream. Read
|
|
145
|
+
// it so that `ApifyApiError` can parse it like any other error body. A body that cannot be read
|
|
146
|
+
// leaves the error without a message, which beats losing the status code to a stream error.
|
|
147
|
+
if (isStream(response.data)) {
|
|
148
|
+
response.data = await concatStreamToBuffer(response.data).catch(() => undefined);
|
|
149
|
+
}
|
|
163
150
|
}
|
|
164
151
|
catch (err) {
|
|
165
|
-
return cast(this
|
|
152
|
+
return cast(this.#handleRequestError(err, config, stopTrying));
|
|
166
153
|
}
|
|
167
154
|
if (response.status === RATE_LIMIT_EXCEEDED_STATUS_CODE) {
|
|
168
155
|
this.stats.addRateLimitError(attempt);
|
|
169
156
|
}
|
|
170
|
-
const apiError =
|
|
171
|
-
if (this
|
|
157
|
+
const apiError = ApifyApiError.fromResponse(response, attempt);
|
|
158
|
+
if (this.#isStatusCodeRetryable(response.status)) {
|
|
172
159
|
if (requestIsStream) {
|
|
173
|
-
this
|
|
160
|
+
this.#informAboutStreamNoRetry();
|
|
174
161
|
}
|
|
175
162
|
else {
|
|
176
163
|
// allow a retry
|
|
@@ -182,20 +169,38 @@ export class HttpClient {
|
|
|
182
169
|
};
|
|
183
170
|
return makeRequest;
|
|
184
171
|
}
|
|
185
|
-
|
|
172
|
+
#isStatusOk(statusCode) {
|
|
186
173
|
return statusCode < 300;
|
|
187
174
|
}
|
|
175
|
+
/**
|
|
176
|
+
* Resolves `timeoutSecs` to the number of milliseconds the given attempt gets. A tier name resolves to
|
|
177
|
+
* its configured duration, a number is taken as seconds, and `'noTimeout'` becomes `0`, which axios reads
|
|
178
|
+
* as no timeout. The result doubles with each attempt and is capped at `timeoutMaxMillis`. A requested
|
|
179
|
+
* value above the cap is capped too, which warns once, since it does not take effect in full.
|
|
180
|
+
*/
|
|
181
|
+
#computeTimeoutMillis(timeoutSecs, attempt) {
|
|
182
|
+
if (timeoutSecs === 'noTimeout')
|
|
183
|
+
return 0;
|
|
184
|
+
const requestedMillis = typeof timeoutSecs === 'number' ? timeoutSecs * 1000 : this.timeoutMillis[timeoutSecs];
|
|
185
|
+
if (requestedMillis > this.timeoutMaxMillis) {
|
|
186
|
+
// `warningOnce` keys by message, so each requested value warns once.
|
|
187
|
+
this.logger.warningOnce(`The requested timeout of ${requestedMillis / 1000}s exceeds timeoutMaxSecs ` +
|
|
188
|
+
`(${this.timeoutMaxMillis / 1000}s) and is capped at it. ` +
|
|
189
|
+
'Raise timeoutMaxSecs on the client to allow longer request timeouts.');
|
|
190
|
+
}
|
|
191
|
+
return Math.min(requestedMillis * 2 ** (attempt - 1), this.timeoutMaxMillis);
|
|
192
|
+
}
|
|
188
193
|
/**
|
|
189
194
|
* Handles all unexpected errors that can happen, but are not
|
|
190
195
|
* Apify API typed errors. E.g. network errors, timeouts and so on.
|
|
191
196
|
*/
|
|
192
|
-
|
|
193
|
-
if (this
|
|
197
|
+
#handleRequestError(err, config, stopTrying) {
|
|
198
|
+
if (this.#isTimeoutError(err) && config.doNotRetryTimeouts) {
|
|
194
199
|
return stopTrying(err);
|
|
195
200
|
}
|
|
196
|
-
if (this
|
|
201
|
+
if (this.#isRetryableError(err)) {
|
|
197
202
|
if (isStream(config.data)) {
|
|
198
|
-
this
|
|
203
|
+
this.#informAboutStreamNoRetry();
|
|
199
204
|
}
|
|
200
205
|
else {
|
|
201
206
|
throw err;
|
|
@@ -207,7 +212,7 @@ export class HttpClient {
|
|
|
207
212
|
* Axios calls req.abort() on timeouts so timeout errors will
|
|
208
213
|
* have a code ECONNABORTED.
|
|
209
214
|
*/
|
|
210
|
-
|
|
215
|
+
#isTimeoutError(err) {
|
|
211
216
|
return err.code === 'ECONNABORTED';
|
|
212
217
|
}
|
|
213
218
|
/**
|
|
@@ -216,15 +221,15 @@ export class HttpClient {
|
|
|
216
221
|
* @param {Error} err
|
|
217
222
|
* @private
|
|
218
223
|
*/
|
|
219
|
-
|
|
220
|
-
return this
|
|
224
|
+
#isRetryableError(err) {
|
|
225
|
+
return this.#isNetworkError(err) || this.#isResponseBodyInvalid(err);
|
|
221
226
|
}
|
|
222
227
|
/**
|
|
223
228
|
* When a network connection to our API is interrupted in the middle of streaming
|
|
224
229
|
* a response, the request often does not fail, but simply contains
|
|
225
230
|
* an incomplete response. This can often be fixed by retrying.
|
|
226
231
|
*/
|
|
227
|
-
|
|
232
|
+
#isResponseBodyInvalid(err) {
|
|
228
233
|
return err instanceof InvalidResponseBodyError;
|
|
229
234
|
}
|
|
230
235
|
/**
|
|
@@ -232,7 +237,7 @@ export class HttpClient {
|
|
|
232
237
|
* it throws an AxiosError, which will have the request
|
|
233
238
|
* and config (and other) properties.
|
|
234
239
|
*/
|
|
235
|
-
|
|
240
|
+
#isNetworkError(err) {
|
|
236
241
|
const hasRequest = err.request && typeof err.request === 'object';
|
|
237
242
|
const hasConfig = err.config && typeof err.config === 'object';
|
|
238
243
|
return hasRequest && hasConfig;
|
|
@@ -242,12 +247,12 @@ export class HttpClient {
|
|
|
242
247
|
* For status codes 300-499 (except 429) we do not retry the request,
|
|
243
248
|
* because it's probably caused by invalid url (redirect 3xx) or invalid user input (4xx).
|
|
244
249
|
*/
|
|
245
|
-
|
|
250
|
+
#isStatusCodeRetryable(statusCode) {
|
|
246
251
|
const isRateLimitError = statusCode === RATE_LIMIT_EXCEEDED_STATUS_CODE;
|
|
247
252
|
const isInternalError = statusCode >= 500;
|
|
248
253
|
return isRateLimitError || isInternalError;
|
|
249
254
|
}
|
|
250
|
-
|
|
255
|
+
#onRequestRetry(error, attempt) {
|
|
251
256
|
if (attempt === Math.round(this.maxRetries / 2)) {
|
|
252
257
|
this.logger.warning(`API request failed ${attempt} times. Max attempts: ${this.maxRetries + 1}.\nCause:${error.stack}`);
|
|
253
258
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -27,6 +27,8 @@ export * from './resource_clients/webhook_collection.js';
|
|
|
27
27
|
export * from './resource_clients/webhook_dispatch.js';
|
|
28
28
|
export * from './resource_clients/webhook_dispatch_collection.js';
|
|
29
29
|
export * from './apify_api_error.js';
|
|
30
|
-
export
|
|
30
|
+
export { ArgumentValidationError } from '@apify/validations';
|
|
31
|
+
export * from './response_validation_error.js';
|
|
31
32
|
export { InvalidResponseBodyError } from './interceptors.js';
|
|
32
33
|
export type { PaginatedList, Dictionary } from './utils.js';
|
|
34
|
+
export type { Timeout, TimeoutOptions, TimeoutTier } from './timeouts.js';
|
package/dist/index.js
CHANGED
|
@@ -27,5 +27,6 @@ export * from './resource_clients/webhook_collection.js';
|
|
|
27
27
|
export * from './resource_clients/webhook_dispatch.js';
|
|
28
28
|
export * from './resource_clients/webhook_dispatch_collection.js';
|
|
29
29
|
export * from './apify_api_error.js';
|
|
30
|
-
export
|
|
30
|
+
export { ArgumentValidationError } from '@apify/validations';
|
|
31
|
+
export * from './response_validation_error.js';
|
|
31
32
|
export { InvalidResponseBodyError } from './interceptors.js';
|
package/dist/interceptors.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import axios, { AxiosHeaders } from 'axios';
|
|
2
2
|
import contentTypeParser from 'content-type';
|
|
3
3
|
import { maybeParseBody } from './body_parser.js';
|
|
4
|
-
import {
|
|
4
|
+
import { isCompressibleContentType, maybeCompressValue } from './utils.js';
|
|
5
5
|
/**
|
|
6
6
|
* This error exists for the quite common situation, where only a partial JSON response is received and
|
|
7
7
|
* an attempt to parse the JSON throws an error. In most cases this can be resolved by retrying the
|
|
@@ -20,7 +20,22 @@ export class InvalidResponseBodyError extends Error {
|
|
|
20
20
|
this.cause = cause;
|
|
21
21
|
}
|
|
22
22
|
}
|
|
23
|
+
/**
|
|
24
|
+
* Reads a request header regardless of the casing it was set with, since HTTP header names are case-insensitive
|
|
25
|
+
* while the config keeps whatever casing the caller used.
|
|
26
|
+
*/
|
|
27
|
+
function getHeader(config, name) {
|
|
28
|
+
const wanted = name.toLowerCase();
|
|
29
|
+
const key = Object.keys(config.headers ?? {}).find((candidate) => candidate.toLowerCase() === wanted);
|
|
30
|
+
const value = key === undefined ? undefined : config.headers?.[key];
|
|
31
|
+
return typeof value === 'string' ? value : undefined;
|
|
32
|
+
}
|
|
23
33
|
function serializeRequest(config) {
|
|
34
|
+
// A string body with an explicit content type is already serialized and goes out as it is. The axios default
|
|
35
|
+
// transform would otherwise parse a JSON one in full just to check that it is valid, which for a body assembled
|
|
36
|
+
// from thousands of pre-serialized requests costs about as much as serializing them did.
|
|
37
|
+
if (typeof config.data === 'string' && getHeader(config, 'content-type'))
|
|
38
|
+
return config;
|
|
24
39
|
const [defaultTransform] = axios.defaults.transformRequest;
|
|
25
40
|
// The function not only serializes data, but it also adds correct headers.
|
|
26
41
|
const data = defaultTransform(config.data, config.headers);
|
|
@@ -30,9 +45,9 @@ function serializeRequest(config) {
|
|
|
30
45
|
// it's a small price to pay. The axios default transform does a lot
|
|
31
46
|
// of body type checks and we would have to copy all of them to the resource clients.
|
|
32
47
|
if (config.stringifyFunctions) {
|
|
33
|
-
const contentTypeHeader = config
|
|
48
|
+
const contentTypeHeader = getHeader(config, 'content-type');
|
|
34
49
|
try {
|
|
35
|
-
const
|
|
50
|
+
const type = contentTypeHeader ? contentTypeParser.parse(contentTypeHeader).type : undefined;
|
|
36
51
|
if (type === 'application/json' && typeof config.data === 'object') {
|
|
37
52
|
config.data = stringifyWithFunctions(config.data);
|
|
38
53
|
}
|
|
@@ -65,7 +80,10 @@ function stringifyWithFunctions(obj) {
|
|
|
65
80
|
});
|
|
66
81
|
}
|
|
67
82
|
async function maybeCompressRequest(config) {
|
|
68
|
-
|
|
83
|
+
// A caller-supplied encoding means the body is already encoded and the header describes it, so leave both alone.
|
|
84
|
+
if (getHeader(config, 'content-encoding'))
|
|
85
|
+
return config;
|
|
86
|
+
if (!isCompressibleContentType(getHeader(config, 'content-type')))
|
|
69
87
|
return config;
|
|
70
88
|
const maybeCompressed = await maybeCompressValue(config.data);
|
|
71
89
|
if (maybeCompressed) {
|
|
@@ -82,8 +100,8 @@ function parseResponseData(response) {
|
|
|
82
100
|
) {
|
|
83
101
|
return response;
|
|
84
102
|
}
|
|
85
|
-
|
|
86
|
-
if (
|
|
103
|
+
// A `Buffer` from the Node.js adapter and an `ArrayBuffer` from the browser ones both carry `byteLength`.
|
|
104
|
+
if (!response.data.byteLength) {
|
|
87
105
|
// undefined is better than an empty buffer
|
|
88
106
|
response.data = undefined;
|
|
89
107
|
return response;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Wraps the construction of a zod schema so it runs on the first call and only once.
|
|
3
|
+
*
|
|
4
|
+
* The generated response schemas are exported this way: a couple of hundred `z.looseObject()` calls at import time
|
|
5
|
+
* would cost more than importing zod itself, so each schema is built by the first response validated against it and
|
|
6
|
+
* kept for the rest of the process.
|
|
7
|
+
*/
|
|
8
|
+
export declare function lazySchema<T>(build: () => T): () => T;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Wraps the construction of a zod schema so it runs on the first call and only once.
|
|
3
|
+
*
|
|
4
|
+
* The generated response schemas are exported this way: a couple of hundred `z.looseObject()` calls at import time
|
|
5
|
+
* would cost more than importing zod itself, so each schema is built by the first response validated against it and
|
|
6
|
+
* kept for the rest of the process.
|
|
7
|
+
*/
|
|
8
|
+
export function lazySchema(build) {
|
|
9
|
+
let schema;
|
|
10
|
+
return () => (schema ??= build());
|
|
11
|
+
}
|