apify-client 3.0.0-beta.2 → 3.0.0-beta.20

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.
Files changed (91) hide show
  1. package/README.md +3 -1
  2. package/dist/apify_api_error.d.ts +60 -19
  3. package/dist/apify_api_error.js +88 -21
  4. package/dist/apify_client.d.ts +22 -3
  5. package/dist/apify_client.js +35 -29
  6. package/dist/base/api_client.d.ts +6 -6
  7. package/dist/base/api_client.js +24 -16
  8. package/dist/base/resource_client.d.ts +23 -9
  9. package/dist/base/resource_client.js +49 -25
  10. package/dist/base/resource_collection_client.d.ts +7 -4
  11. package/dist/base/resource_collection_client.js +21 -15
  12. package/dist/bundle.js +38 -48
  13. package/dist/bundle.js.map +1 -1
  14. package/dist/generated/api.d.ts +287 -214
  15. package/dist/generated/schemas.d.ts +6699 -0
  16. package/dist/generated/schemas.js +1521 -0
  17. package/dist/http_client.d.ts +18 -51
  18. package/dist/http_client.js +79 -42
  19. package/dist/index.d.ts +3 -1
  20. package/dist/index.js +2 -1
  21. package/dist/interceptors.js +22 -4
  22. package/dist/lazy_schema.d.ts +8 -0
  23. package/dist/lazy_schema.js +11 -0
  24. package/dist/models.d.ts +32 -52
  25. package/dist/resource_clients/actor.d.ts +46 -31
  26. package/dist/resource_clients/actor.js +71 -53
  27. package/dist/resource_clients/actor_collection.d.ts +9 -5
  28. package/dist/resource_clients/actor_collection.js +12 -3
  29. package/dist/resource_clients/actor_env_var.d.ts +10 -3
  30. package/dist/resource_clients/actor_env_var.js +17 -6
  31. package/dist/resource_clients/actor_env_var_collection.d.ts +14 -18
  32. package/dist/resource_clients/actor_env_var_collection.js +18 -11
  33. package/dist/resource_clients/actor_version.d.ts +18 -3
  34. package/dist/resource_clients/actor_version.js +20 -9
  35. package/dist/resource_clients/actor_version_collection.d.ts +16 -17
  36. package/dist/resource_clients/actor_version_collection.js +20 -11
  37. package/dist/resource_clients/build.d.ts +18 -6
  38. package/dist/resource_clients/build.js +35 -17
  39. package/dist/resource_clients/build_collection.d.ts +3 -1
  40. package/dist/resource_clients/build_collection.js +5 -1
  41. package/dist/resource_clients/dataset.d.ts +23 -9
  42. package/dist/resource_clients/dataset.js +70 -48
  43. package/dist/resource_clients/dataset_collection.d.ts +6 -2
  44. package/dist/resource_clients/dataset_collection.js +13 -2
  45. package/dist/resource_clients/key_value_store.d.ts +48 -15
  46. package/dist/resource_clients/key_value_store.js +69 -39
  47. package/dist/resource_clients/key_value_store_collection.d.ts +6 -2
  48. package/dist/resource_clients/key_value_store_collection.js +13 -2
  49. package/dist/resource_clients/log.d.ts +9 -19
  50. package/dist/resource_clients/log.js +55 -44
  51. package/dist/resource_clients/request_queue.d.ts +56 -26
  52. package/dist/resource_clients/request_queue.js +178 -113
  53. package/dist/resource_clients/request_queue_collection.d.ts +6 -2
  54. package/dist/resource_clients/request_queue_collection.js +10 -3
  55. package/dist/resource_clients/run.d.ts +51 -17
  56. package/dist/resource_clients/run.js +83 -43
  57. package/dist/resource_clients/run_collection.d.ts +3 -1
  58. package/dist/resource_clients/run_collection.js +5 -1
  59. package/dist/resource_clients/schedule.d.ts +16 -7
  60. package/dist/resource_clients/schedule.js +31 -21
  61. package/dist/resource_clients/schedule_collection.d.ts +6 -2
  62. package/dist/resource_clients/schedule_collection.js +10 -3
  63. package/dist/resource_clients/store_collection.d.ts +3 -1
  64. package/dist/resource_clients/store_collection.js +5 -1
  65. package/dist/resource_clients/task.d.ts +28 -10
  66. package/dist/resource_clients/task.js +67 -44
  67. package/dist/resource_clients/task_collection.d.ts +6 -2
  68. package/dist/resource_clients/task_collection.js +10 -3
  69. package/dist/resource_clients/user.d.ts +17 -8
  70. package/dist/resource_clients/user.js +40 -39
  71. package/dist/resource_clients/webhook.d.ts +14 -5
  72. package/dist/resource_clients/webhook.js +30 -22
  73. package/dist/resource_clients/webhook_collection.d.ts +6 -2
  74. package/dist/resource_clients/webhook_collection.js +10 -3
  75. package/dist/resource_clients/webhook_dispatch.d.ts +4 -1
  76. package/dist/resource_clients/webhook_dispatch.js +8 -2
  77. package/dist/resource_clients/webhook_dispatch_collection.d.ts +3 -1
  78. package/dist/resource_clients/webhook_dispatch_collection.js +5 -1
  79. package/dist/response_validation_error.d.ts +26 -0
  80. package/dist/response_validation_error.js +37 -0
  81. package/dist/schemas.d.ts +15 -0
  82. package/dist/schemas.js +15 -0
  83. package/dist/statistics.d.ts +1 -6
  84. package/dist/statistics.js +2 -2
  85. package/dist/timeouts.d.ts +72 -0
  86. package/dist/timeouts.js +31 -0
  87. package/dist/utils.d.ts +57 -41
  88. package/dist/utils.js +155 -121
  89. package/package.json +36 -35
  90. package/dist/argument_validation_error.d.ts +0 -17
  91. package/dist/argument_validation_error.js +0 -153
@@ -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
- timeoutMillis: number;
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
- timeoutSecs: number;
46
+ timeoutShortSecs: number;
47
+ timeoutMediumSecs: number;
48
+ timeoutLongSecs: number;
49
+ timeoutMaxSecs: number;
83
50
  logger: Log;
84
51
  token?: string;
85
52
  workflowKey?: string;
@@ -1,6 +1,7 @@
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
7
  import { asArray, cast, getVersionData, isNode, isStream } from './utils.js';
@@ -12,24 +13,31 @@ 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
- nodeInitPromise;
21
- userAgentSuffix;
24
+ #nodeInitPromise;
25
+ #userAgentSuffix;
22
26
  constructor(options) {
23
27
  const { token } = options;
24
28
  this.stats = options.apifyClientStats;
25
29
  this.maxRetries = options.maxRetries;
26
30
  this.minDelayBetweenRetriesMillis = options.minDelayBetweenRetriesMillis;
27
31
  this.userProvidedRequestInterceptors = options.requestInterceptors;
28
- this.timeoutMillis = options.timeoutSecs * 1000;
32
+ this.timeoutMillis = {
33
+ short: options.timeoutShortSecs * 1000,
34
+ medium: options.timeoutMediumSecs * 1000,
35
+ long: options.timeoutLongSecs * 1000,
36
+ };
37
+ this.timeoutMaxMillis = options.timeoutMaxSecs * 1000;
29
38
  this.logger = options.logger;
30
39
  this.workflowKey = options.workflowKey || process.env[APIFY_ENV_VARS.WORKFLOW_KEY];
31
- this.userAgentSuffix = options.userAgentSuffix;
32
- this._onRequestRetry = this._onRequestRetry.bind(this);
40
+ this.#userAgentSuffix = options.userAgentSuffix;
33
41
  this.axios = axios.create({
34
42
  // Disable axios's built-in proxy handling since we're using custom agents
35
43
  proxy: false,
@@ -46,11 +54,14 @@ export class HttpClient {
46
54
  return new URLSearchParams(formattedParams).toString();
47
55
  },
48
56
  validateStatus: null,
49
- // Using interceptors for this functionality.
50
- transformRequest: undefined,
51
- transformResponse: undefined,
57
+ // Interceptors serialize requests and parse responses instead. Empty arrays rather than `undefined`,
58
+ // which axios fills in with its default transforms.
59
+ transformRequest: [],
60
+ transformResponse: [],
52
61
  responseType: 'arraybuffer',
53
- timeout: this.timeoutMillis,
62
+ // Every request sets its own timeout in `createRequestHandler`, so the default only backs a raw
63
+ // `axios.request()` call.
64
+ timeout: this.timeoutMaxMillis,
54
65
  // maxBodyLength needs to be Infinity, because -1 falls back to a 10 MB default
55
66
  // from an axios subdependency - 'follow-redirects'
56
67
  maxBodyLength: Infinity,
@@ -72,13 +83,13 @@ export class HttpClient {
72
83
  this.userProvidedRequestInterceptors.forEach((i) => this.axios.interceptors.request.use(i));
73
84
  responseInterceptors.forEach((i) => this.axios.interceptors.response.use(i));
74
85
  }
75
- async ensureNodeInit() {
86
+ async #ensureNodeInit() {
76
87
  if (!isNode())
77
88
  return;
78
- this.nodeInitPromise ??= this.initNode();
79
- return this.nodeInitPromise;
89
+ this.#nodeInitPromise ??= this.#initNode();
90
+ return this.#nodeInitPromise;
80
91
  }
81
- async initNode() {
92
+ async #initNode() {
82
93
  if (!isNode())
83
94
  return;
84
95
  const [{ ProxyAgent }, os] = await Promise.all([import('proxy-agent'), import('node:os')]);
@@ -92,7 +103,7 @@ export class HttpClient {
92
103
  keepAlive: true,
93
104
  // Timeout for inactive sockets
94
105
  // Prevents socket leaks from idle connections
95
- timeout: this.timeoutMillis,
106
+ timeout: this.timeoutMaxMillis,
96
107
  // Keep alive timeout for free sockets (15 seconds)
97
108
  // Node.js will close unused sockets after this period
98
109
  keepAliveMsecs: 15_000,
@@ -119,22 +130,22 @@ export class HttpClient {
119
130
  // Works only in Node. Cannot be set in browser
120
131
  const isAtHome = !!process.env[APIFY_ENV_VARS.IS_AT_HOME];
121
132
  let userAgent = `ApifyClient/${version} (${os.platform()}; Node/${process.version}); isAtHome/${isAtHome}`;
122
- if (this.userAgentSuffix) {
123
- userAgent += `; ${asArray(this.userAgentSuffix).join('; ')}`;
133
+ if (this.#userAgentSuffix) {
134
+ userAgent += `; ${asArray(this.#userAgentSuffix).join('; ')}`;
124
135
  }
125
136
  this.axios.defaults.headers['User-Agent'] = userAgent;
126
137
  }
127
138
  async call(config) {
128
- await this.ensureNodeInit();
139
+ await this.#ensureNodeInit();
129
140
  this.stats.calls++;
130
- const makeRequest = this._createRequestHandler(config);
141
+ const makeRequest = this.#createRequestHandler(config);
131
142
  return retry(makeRequest, {
132
143
  retries: this.maxRetries,
133
144
  minTimeout: this.minDelayBetweenRetriesMillis,
134
- onRetry: this._onRequestRetry,
145
+ onRetry: (error, attempt) => this.#onRequestRetry(error, attempt),
135
146
  });
136
147
  }
137
- _informAboutStreamNoRetry() {
148
+ #informAboutStreamNoRetry() {
138
149
  this.logger.warningOnce('Request body was a stream - retrying will not work, as part of it was already consumed.');
139
150
  this.logger.warningOnce('If you want Apify client to handle retries for you, collect the stream into a buffer before sending it.');
140
151
  }
@@ -143,7 +154,8 @@ export class HttpClient {
143
154
  * status codes are retried. See the following functions for the
144
155
  * retrying logic.
145
156
  */
146
- _createRequestHandler(config) {
157
+ #createRequestHandler(config) {
158
+ const { timeoutSecs = 'medium', ...axiosConfig } = config;
147
159
  const makeRequest = async (stopTrying, attempt) => {
148
160
  this.stats.requests++;
149
161
  let response;
@@ -153,24 +165,31 @@ export class HttpClient {
153
165
  // Handling redirects is not possible without buffering - part of the stream has already been sent and can't be recovered
154
166
  // when server sends the redirect. Therefore we need to override this in Axios config to prevent it from buffering the body.
155
167
  // see also axios/axios#1045
156
- config = { ...config, maxRedirects: 0 };
168
+ axiosConfig.maxRedirects = 0;
157
169
  }
158
- // Increase timeout with each attempt. Max timeout is bounded by the client timeout.
159
- config.timeout = Math.min(this.timeoutMillis, (config.timeout ?? this.timeoutMillis) * 2 ** (attempt - 1));
160
- response = await this.axios.request(config);
161
- if (this._isStatusOk(response.status))
170
+ response = await this.axios.request({
171
+ ...axiosConfig,
172
+ timeout: this.#computeTimeoutMillis(timeoutSecs, attempt),
173
+ });
174
+ if (this.#isStatusOk(response.status))
162
175
  return response;
176
+ // A failed request with `responseType: 'stream'` carries the API error body in the stream. Read
177
+ // it so that `ApifyApiError` can parse it like any other error body. A body that cannot be read
178
+ // leaves the error without a message, which beats losing the status code to a stream error.
179
+ if (isStream(response.data)) {
180
+ response.data = await concatStreamToBuffer(response.data).catch(() => undefined);
181
+ }
163
182
  }
164
183
  catch (err) {
165
- return cast(this._handleRequestError(err, config, stopTrying));
184
+ return cast(this.#handleRequestError(err, config, stopTrying));
166
185
  }
167
186
  if (response.status === RATE_LIMIT_EXCEEDED_STATUS_CODE) {
168
187
  this.stats.addRateLimitError(attempt);
169
188
  }
170
- const apiError = new ApifyApiError(response, attempt);
171
- if (this._isStatusCodeRetryable(response.status)) {
189
+ const apiError = ApifyApiError.fromResponse(response, attempt);
190
+ if (this.#isStatusCodeRetryable(response.status)) {
172
191
  if (requestIsStream) {
173
- this._informAboutStreamNoRetry();
192
+ this.#informAboutStreamNoRetry();
174
193
  }
175
194
  else {
176
195
  // allow a retry
@@ -182,20 +201,38 @@ export class HttpClient {
182
201
  };
183
202
  return makeRequest;
184
203
  }
185
- _isStatusOk(statusCode) {
204
+ #isStatusOk(statusCode) {
186
205
  return statusCode < 300;
187
206
  }
207
+ /**
208
+ * Resolves `timeoutSecs` to the number of milliseconds the given attempt gets. A tier name resolves to
209
+ * its configured duration, a number is taken as seconds, and `'noTimeout'` becomes `0`, which axios reads
210
+ * as no timeout. The result doubles with each attempt and is capped at `timeoutMaxMillis`. A requested
211
+ * value above the cap is capped too, which warns once, since it does not take effect in full.
212
+ */
213
+ #computeTimeoutMillis(timeoutSecs, attempt) {
214
+ if (timeoutSecs === 'noTimeout')
215
+ return 0;
216
+ const requestedMillis = typeof timeoutSecs === 'number' ? timeoutSecs * 1000 : this.timeoutMillis[timeoutSecs];
217
+ if (requestedMillis > this.timeoutMaxMillis) {
218
+ // `warningOnce` keys by message, so each requested value warns once.
219
+ this.logger.warningOnce(`The requested timeout of ${requestedMillis / 1000}s exceeds timeoutMaxSecs ` +
220
+ `(${this.timeoutMaxMillis / 1000}s) and is capped at it. ` +
221
+ 'Raise timeoutMaxSecs on the client to allow longer request timeouts.');
222
+ }
223
+ return Math.min(requestedMillis * 2 ** (attempt - 1), this.timeoutMaxMillis);
224
+ }
188
225
  /**
189
226
  * Handles all unexpected errors that can happen, but are not
190
227
  * Apify API typed errors. E.g. network errors, timeouts and so on.
191
228
  */
192
- _handleRequestError(err, config, stopTrying) {
193
- if (this._isTimeoutError(err) && config.doNotRetryTimeouts) {
229
+ #handleRequestError(err, config, stopTrying) {
230
+ if (this.#isTimeoutError(err) && config.doNotRetryTimeouts) {
194
231
  return stopTrying(err);
195
232
  }
196
- if (this._isRetryableError(err)) {
233
+ if (this.#isRetryableError(err)) {
197
234
  if (isStream(config.data)) {
198
- this._informAboutStreamNoRetry();
235
+ this.#informAboutStreamNoRetry();
199
236
  }
200
237
  else {
201
238
  throw err;
@@ -207,7 +244,7 @@ export class HttpClient {
207
244
  * Axios calls req.abort() on timeouts so timeout errors will
208
245
  * have a code ECONNABORTED.
209
246
  */
210
- _isTimeoutError(err) {
247
+ #isTimeoutError(err) {
211
248
  return err.code === 'ECONNABORTED';
212
249
  }
213
250
  /**
@@ -216,15 +253,15 @@ export class HttpClient {
216
253
  * @param {Error} err
217
254
  * @private
218
255
  */
219
- _isRetryableError(err) {
220
- return this._isNetworkError(err) || this._isResponseBodyInvalid(err);
256
+ #isRetryableError(err) {
257
+ return this.#isNetworkError(err) || this.#isResponseBodyInvalid(err);
221
258
  }
222
259
  /**
223
260
  * When a network connection to our API is interrupted in the middle of streaming
224
261
  * a response, the request often does not fail, but simply contains
225
262
  * an incomplete response. This can often be fixed by retrying.
226
263
  */
227
- _isResponseBodyInvalid(err) {
264
+ #isResponseBodyInvalid(err) {
228
265
  return err instanceof InvalidResponseBodyError;
229
266
  }
230
267
  /**
@@ -232,7 +269,7 @@ export class HttpClient {
232
269
  * it throws an AxiosError, which will have the request
233
270
  * and config (and other) properties.
234
271
  */
235
- _isNetworkError(err) {
272
+ #isNetworkError(err) {
236
273
  const hasRequest = err.request && typeof err.request === 'object';
237
274
  const hasConfig = err.config && typeof err.config === 'object';
238
275
  return hasRequest && hasConfig;
@@ -242,12 +279,12 @@ export class HttpClient {
242
279
  * For status codes 300-499 (except 429) we do not retry the request,
243
280
  * because it's probably caused by invalid url (redirect 3xx) or invalid user input (4xx).
244
281
  */
245
- _isStatusCodeRetryable(statusCode) {
282
+ #isStatusCodeRetryable(statusCode) {
246
283
  const isRateLimitError = statusCode === RATE_LIMIT_EXCEEDED_STATUS_CODE;
247
284
  const isInternalError = statusCode >= 500;
248
285
  return isRateLimitError || isInternalError;
249
286
  }
250
- _onRequestRetry(error, attempt) {
287
+ #onRequestRetry(error, attempt) {
251
288
  if (attempt === Math.round(this.maxRetries / 2)) {
252
289
  this.logger.warning(`API request failed ${attempt} times. Max attempts: ${this.maxRetries + 1}.\nCause:${error.stack}`);
253
290
  }
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 * from './argument_validation_error.js';
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 * from './argument_validation_error.js';
30
+ export { ArgumentValidationError } from '@apify/validations';
31
+ export * from './response_validation_error.js';
31
32
  export { InvalidResponseBodyError } from './interceptors.js';
@@ -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 { isNode, maybeCompressValue } from './utils.js';
4
+ import { isCompressibleContentType, isNode, 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.headers?.['Content-Type'] || config.headers?.['content-type'];
48
+ const contentTypeHeader = getHeader(config, 'content-type');
34
49
  try {
35
- const { type } = contentTypeParser.parse(contentTypeHeader);
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
- if (config.headers?.['content-encoding'])
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) {
@@ -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
+ }