apify-client 3.0.0-beta.71 → 3.0.0-beta.73

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.
@@ -27,9 +27,10 @@ export declare const httpClientOptionsShape: {
27
27
  export type HttpResponseHeaders = Record<string, string | string[] | undefined>;
28
28
  /**
29
29
  * A request body as the transport receives it: already serialized, and compressed when that paid off. A `Readable`
30
- * is passed through untouched and is only available in Node.js.
30
+ * is passed through untouched and is only available in Node.js. A `Blob` body reaches a Node.js transport as a
31
+ * fresh `Readable` over its bytes for every attempt, and a transport in any other runtime as the `Blob` itself.
31
32
  */
32
- export type HttpRequestBody = string | Buffer | ArrayBuffer | ArrayBufferView | Readable;
33
+ export type HttpRequestBody = string | Buffer | ArrayBuffer | ArrayBufferView | Readable | Blob;
33
34
  /**
34
35
  * A response body as the transport hands it back: the raw bytes, a `Readable` when the request asked for a streamed
35
36
  * response, or `undefined` when the response carried no body.
@@ -63,8 +64,9 @@ export interface ApifyRequestConfig {
63
64
  * Request body. A plain object or array is serialized to JSON and sent as `application/json`, or form-encoded
64
65
  * when a `Content-Type: application/x-www-form-urlencoded` header asks for it. A string, binary value
65
66
  * (`Buffer`, `ArrayBuffer`, typed array) or `Readable` is sent as it is, and `URLSearchParams` is sent
66
- * form-encoded. A `Blob`, `File`, `FormData` or web `ReadableStream` is rejected with a `TypeError`, since
67
- * serializing one would send an empty body.
67
+ * form-encoded. A `Blob` or `File`, such as one from `fs.openAsBlob()`, is streamed without being held in
68
+ * memory, under its own `type` when no `Content-Type` header is set. A `FormData` or web `ReadableStream` is
69
+ * rejected with a `TypeError`, since serializing one would send an empty body.
68
70
  */
69
71
  data?: unknown;
70
72
  /**
@@ -278,7 +280,8 @@ export declare abstract class HttpClient {
278
280
  * The inherited {@link call} needs it, so every transport has to implement it. Let the library's errors
279
281
  * propagate unwrapped: {@link call} classifies them through {@link isRetryableTransportError} and
280
282
  * {@link isTimeoutError}. Return error responses as they are too, the pipeline turns them into
281
- * {@link ApifyApiError} and decides whether to retry.
283
+ * {@link ApifyApiError} and decides whether to retry. Reject when the connection drops before the body is
284
+ * complete: the pipeline takes a returned body as whole and does not retry one that fails to parse.
282
285
  *
283
286
  * @param request - The request to send, with the headers merged, the body serialized and the query encoded.
284
287
  * @returns The response, with the body unread when `request.stream` is set and as raw bytes otherwise.
@@ -290,8 +293,10 @@ export declare abstract class HttpClient {
290
293
  * Network errors the transport classifies as retryable, rate limits (HTTP 429) and server errors (HTTP 5xx)
291
294
  * are retried up to {@link maxRetries} times. Any other error status is thrown as {@link ApifyApiError} right
292
295
  * away. A request whose body is a `Readable` is never retried, since part of the stream has already been
293
- * consumed by the time the failure shows. Aborting `config.signal` ends the attempt in flight, skips the
294
- * remaining retries and rejects the call with the signal's `reason`.
296
+ * consumed by the time the failure shows. A `Blob` body is read again for every attempt, so it is retried like
297
+ * any other, unless reading the blob itself fails: that error is thrown right away, with the transport error
298
+ * as its `cause` when that error does not already wrap it. Aborting `config.signal` ends the attempt in flight,
299
+ * skips the remaining retries and rejects the call with the signal's `reason`.
295
300
  *
296
301
  * @template T - Type of the parsed response body.
297
302
  * @param config - The request to make.
@@ -9,7 +9,7 @@ import { InvalidResponseBodyError } from '../invalid_response_body_error.js';
9
9
  import { Statistics } from '../statistics.js';
10
10
  import { DEFAULT_TIMEOUT_LONG_SECS, DEFAULT_TIMEOUT_MAX_SECS, DEFAULT_TIMEOUT_MEDIUM_SECS, DEFAULT_TIMEOUT_SHORT_SECS, } from '../timeouts.js';
11
11
  import { runtime } from '#runtime';
12
- import { asArray, getEnv, isBuffer, isCompressibleContentType, isStream, MIN_COMPRESS_BYTES, parseArgument, sleep, toBytes, version, } from '../utils.js';
12
+ import { asArray, getEnv, isBlob, isBuffer, isCompressibleContentType, isStream, MIN_COMPRESS_BYTES, parseArgument, sleep, toBytes, version, } from '../utils.js';
13
13
  export const DEFAULT_MAX_RETRIES = 8;
14
14
  export const DEFAULT_MIN_DELAY_BETWEEN_RETRIES_MILLIS = 500;
15
15
  const RATE_LIMIT_EXCEEDED_STATUS_CODE = 429;
@@ -29,7 +29,7 @@ export const httpClientOptionsShape = {
29
29
  };
30
30
  const httpClientOptionsSchema = z.looseObject(httpClientOptionsShape).partial();
31
31
  /** Objects whose payload does not live in their own enumerable keys, so JSON serialization loses it. */
32
- const UNSERIALIZABLE_OBJECT_TAGS = new Set(['Blob', 'File', 'FormData', 'ReadableStream']);
32
+ const UNSERIALIZABLE_OBJECT_TAGS = new Set(['FormData', 'ReadableStream']);
33
33
  /**
34
34
  * Base class for the HTTP clients used by {@link ApifyClient}.
35
35
  *
@@ -169,7 +169,8 @@ export class HttpClient {
169
169
  * The inherited {@link call} needs it, so every transport has to implement it. Let the library's errors
170
170
  * propagate unwrapped: {@link call} classifies them through {@link isRetryableTransportError} and
171
171
  * {@link isTimeoutError}. Return error responses as they are too, the pipeline turns them into
172
- * {@link ApifyApiError} and decides whether to retry.
172
+ * {@link ApifyApiError} and decides whether to retry. Reject when the connection drops before the body is
173
+ * complete: the pipeline takes a returned body as whole and does not retry one that fails to parse.
173
174
  *
174
175
  * @param request - The request to send, with the headers merged, the body serialized and the query encoded.
175
176
  * @returns The response, with the body unread when `request.stream` is set and as raw bytes otherwise.
@@ -183,8 +184,10 @@ export class HttpClient {
183
184
  * Network errors the transport classifies as retryable, rate limits (HTTP 429) and server errors (HTTP 5xx)
184
185
  * are retried up to {@link maxRetries} times. Any other error status is thrown as {@link ApifyApiError} right
185
186
  * away. A request whose body is a `Readable` is never retried, since part of the stream has already been
186
- * consumed by the time the failure shows. Aborting `config.signal` ends the attempt in flight, skips the
187
- * remaining retries and rejects the call with the signal's `reason`.
187
+ * consumed by the time the failure shows. A `Blob` body is read again for every attempt, so it is retried like
188
+ * any other, unless reading the blob itself fails: that error is thrown right away, with the transport error
189
+ * as its `cause` when that error does not already wrap it. Aborting `config.signal` ends the attempt in flight,
190
+ * skips the remaining retries and rejects the call with the signal's `reason`.
188
191
  *
189
192
  * @template T - Type of the parsed response body.
190
193
  * @param config - The request to make.
@@ -219,6 +222,11 @@ export class HttpClient {
219
222
  headers = mergeHeaders(headers, { 'Content-Encoding': this.httpCompressor.contentEncoding });
220
223
  deleteHeader(headers, 'content-length');
221
224
  }
225
+ // A blob reaches a Node.js transport as a stream, which would go out chunked without a known length.
226
+ // Browsers set the header themselves and refuse to let a page set it.
227
+ if (isBlob(body) && runtime.isNode) {
228
+ headers = mergeHeaders(headers, { 'Content-Length': String(body.size) });
229
+ }
222
230
  return { headers, body };
223
231
  }
224
232
  /**
@@ -303,13 +311,20 @@ export class HttpClient {
303
311
  const { config, url, headers, body, attempt, stopRetrying } = options;
304
312
  this.stats.requests++;
305
313
  const requestIsStream = isStream(config.data);
314
+ // The transport reports a failure of the blob as an error of its own, which may pass as transient.
315
+ let sourceFailure;
316
+ const attemptBody = isBlob(body)
317
+ ? runtime.openBlobBody(body, (error) => {
318
+ sourceFailure = { error };
319
+ })
320
+ : body;
306
321
  let response;
307
322
  try {
308
323
  response = await this.sendRequest({
309
324
  method: config.method,
310
325
  url,
311
326
  headers,
312
- body,
327
+ body: attemptBody,
313
328
  timeoutMillis: this.computeTimeoutMillis(attempt, config.timeoutSecs),
314
329
  stream: config.responseType === 'stream',
315
330
  signal: config.signal,
@@ -318,9 +333,20 @@ export class HttpClient {
318
333
  catch (err) {
319
334
  // Every HTTP library reports an abort with an error of its own, so the caller gets the reason instead.
320
335
  config.signal?.throwIfAborted();
336
+ if (sourceFailure) {
337
+ // Sending the body again cannot fix its source.
338
+ stopRetrying();
339
+ throw withCause(sourceFailure.error, err);
340
+ }
321
341
  this.#handleRequestError(err, config, stopRetrying);
322
342
  throw err;
323
343
  }
344
+ finally {
345
+ // A transport may stop reading the body early, for example on an error response sent before the whole
346
+ // body arrived, which would leave the blob's file open.
347
+ if (attemptBody !== body && isStream(attemptBody))
348
+ attemptBody.destroy();
349
+ }
324
350
  // A failed streaming request carries the API error body in the stream, so read it and let
325
351
  // `ApifyApiError` parse it like any other error body. A body that cannot be read leaves the error
326
352
  // without a message, which beats losing the status code to a stream error.
@@ -332,12 +358,14 @@ export class HttpClient {
332
358
  data = this.#parseResponseBody(response, config);
333
359
  }
334
360
  catch (err) {
335
- // A body that does not parse is usually a connection dropped mid-response, which a retry fixes.
336
- if (requestIsStream) {
337
- this.#informAboutStreamNoRetry();
361
+ // The body of a success arrived in full, since a transfer cut short fails in `sendRequest()`, so a
362
+ // retry would get the same body back.
363
+ if (response.status < 300) {
338
364
  stopRetrying();
365
+ throw err;
339
366
  }
340
- throw err;
367
+ // An error status is reported by its code, and `ApifyApiError` makes what it can of the raw body.
368
+ data = response.body;
341
369
  }
342
370
  const apifyResponse = {
343
371
  status: response.status,
@@ -451,7 +479,7 @@ function deleteHeader(headers, name) {
451
479
  /**
452
480
  * Views a serialized body as the bytes to compress, or `undefined` when compressing it would not pay off: the
453
481
  * caller already labeled the body with a `Content-Encoding`, its content type carries its own compression, it is
454
- * a stream or smaller than {@link MIN_COMPRESS_BYTES}, or the runtime has no compression to offer.
482
+ * a stream or a blob, it is smaller than {@link MIN_COMPRESS_BYTES}, or the runtime has no compression to offer.
455
483
  */
456
484
  function compressibleBytes(body, headers) {
457
485
  if (body === undefined || !runtime.isNode)
@@ -463,6 +491,22 @@ function compressibleBytes(body, headers) {
463
491
  const bytes = toBytes(body);
464
492
  return bytes && bytes.byteLength >= MIN_COMPRESS_BYTES ? bytes : undefined;
465
493
  }
494
+ /**
495
+ * Sets `cause` on an error that has none, and returns the error. A `cause` that already wraps the error, as an axios
496
+ * error wraps the error of its request body, is left out, since linking it would make the chain circular.
497
+ */
498
+ function withCause(error, cause) {
499
+ if (!(error instanceof Error) || error.cause !== undefined)
500
+ return error;
501
+ const seen = new Set();
502
+ for (let link = cause; link instanceof Error && !seen.has(link); link = link.cause) {
503
+ if (link === error)
504
+ return error;
505
+ seen.add(link);
506
+ }
507
+ error.cause = cause;
508
+ return error;
509
+ }
466
510
  function headerValue(value) {
467
511
  return Array.isArray(value) ? value[0] : value;
468
512
  }
@@ -481,6 +525,11 @@ function serializeBody(data, headers, stringifyFunctions) {
481
525
  return undefined;
482
526
  if (isStream(data))
483
527
  return data;
528
+ if (isBlob(data)) {
529
+ if (getHeader(headers, 'content-type') === undefined && data.type)
530
+ headers['Content-Type'] = data.type;
531
+ return data;
532
+ }
484
533
  if (isBuffer(data)) {
485
534
  // The axios Node.js adapter refuses a body that is neither a `Buffer`, an `ArrayBuffer` nor a string.
486
535
  if (!runtime.isNode || Buffer.isBuffer(data))
@@ -1,9 +1,9 @@
1
1
  import { ApifyClientError } from './apify_client_error.js';
2
2
  import type { HttpResponse } from './http_clients/base.js';
3
3
  /**
4
- * Thrown when a response body does not parse as its content type claims, typically a JSON document cut short by a
5
- * connection dropped mid-response. The client retries such requests, so the error surfaces once the retries are
6
- * exhausted.
4
+ * Thrown when the body of a successful response does not parse as its content type claims, for example a non-JSON
5
+ * value stored under `application/json`. The response arrived in full, so the request is not retried. An error
6
+ * status with an unparsable body throws {@link ApifyApiError} for its status code.
7
7
  */
8
8
  export declare class InvalidResponseBodyError extends ApifyClientError {
9
9
  code: string;
@@ -1,8 +1,8 @@
1
1
  import { ApifyClientError } from './apify_client_error.js';
2
2
  /**
3
- * Thrown when a response body does not parse as its content type claims, typically a JSON document cut short by a
4
- * connection dropped mid-response. The client retries such requests, so the error surfaces once the retries are
5
- * exhausted.
3
+ * Thrown when the body of a successful response does not parse as its content type claims, for example a non-JSON
4
+ * value stored under `application/json`. The response arrived in full, so the request is not retried. An error
5
+ * status with an unparsable body throws {@link ApifyApiError} for its status code.
6
6
  */
7
7
  export class InvalidResponseBodyError extends ApifyClientError {
8
8
  code;
@@ -210,15 +210,18 @@ export declare class KeyValueStoreClient extends ResourceClient {
210
210
  * to be valid JSON already, or {@link getRecord} fails to parse it back.
211
211
  *
212
212
  * **Note about streams:** If the value is a stream object (has `.pipe` and `.on` methods),
213
- * the upload cannot be retried on failure or follow redirects. For reliable uploads,
214
- * buffer the entire stream into memory first.
213
+ * the upload cannot be retried on failure or follow redirects. For reliable uploads of a file,
214
+ * pass a `Blob` from `fs.openAsBlob()` instead: it is read again for every attempt and never held
215
+ * in memory whole.
215
216
  *
216
217
  * @param record - The record to store
217
218
  * @param record.key - Record key (unique identifier)
218
- * @param record.value - Record value (a JSON-serializable value, Buffer, ArrayBuffer, typed array, or Readable)
219
+ * @param record.value - Record value (a JSON-serializable value, Buffer, ArrayBuffer, typed array, Blob, or
220
+ * Readable)
219
221
  * @param record.contentType - Optional MIME type. Auto-detected if not provided:
220
222
  * - Objects: `'application/json; charset=utf-8'`
221
223
  * - Strings: `'text/plain; charset=utf-8'`
224
+ * - Blobs: their `type`, or `'application/octet-stream'` when it is empty
222
225
  * - Binary values and streams: `'application/octet-stream'`
223
226
  *
224
227
  * Worth setting for media and archives: the client skips compressing a body
@@ -349,10 +352,10 @@ export interface KeyValueClientGetRecordOptions extends TimeoutOptions {
349
352
  * A value that `setRecord` accepts.
350
353
  *
351
354
  * Anything JSON-serializable, or binary content the client uploads as bytes instead of JSON-encoding
352
- * it: a `Buffer`, an `ArrayBuffer`, a typed array, or a readable stream. `Buffer` is a `Uint8Array`, so
353
- * `TypedArray` covers it.
355
+ * it: a `Buffer`, an `ArrayBuffer`, a typed array, a `Blob`, or a readable stream. `Buffer` is a
356
+ * `Uint8Array`, so `TypedArray` covers it.
354
357
  */
355
- export type KeyValueStoreRecordValue = JsonValue | ArrayBuffer | TypedArray | Readable;
358
+ export type KeyValueStoreRecordValue = JsonValue | ArrayBuffer | TypedArray | Blob | Readable;
356
359
  /**
357
360
  * Represents a record (key-value pair) in a Key-Value Store.
358
361
  *
@@ -5,7 +5,7 @@ import { ResourceClient } from '../base/resource_client.js';
5
5
  import * as schemas from '../schemas.js';
6
6
  import { timeoutOptionsSchema, timeoutOptionsShape } from '../timeouts.js';
7
7
  import { runtime } from '#runtime';
8
- import { anyObjectSchema, applyQueryParamsToUrl, catchNotFoundOrThrow, isBuffer, isStream, minForLimitParam, parseArgument, parseResponse, } from '../utils.js';
8
+ import { anyObjectSchema, applyQueryParamsToUrl, catchNotFoundOrThrow, isBlob, isBuffer, isStream, minForLimitParam, parseArgument, parseResponse, } from '../utils.js';
9
9
  const listKeysOptionsSchema = z.strictObject({
10
10
  limit: z.number().min(0).optional(),
11
11
  chunkSize: z.number().positive().optional(),
@@ -364,15 +364,18 @@ export class KeyValueStoreClient extends ResourceClient {
364
364
  * to be valid JSON already, or {@link getRecord} fails to parse it back.
365
365
  *
366
366
  * **Note about streams:** If the value is a stream object (has `.pipe` and `.on` methods),
367
- * the upload cannot be retried on failure or follow redirects. For reliable uploads,
368
- * buffer the entire stream into memory first.
367
+ * the upload cannot be retried on failure or follow redirects. For reliable uploads of a file,
368
+ * pass a `Blob` from `fs.openAsBlob()` instead: it is read again for every attempt and never held
369
+ * in memory whole.
369
370
  *
370
371
  * @param record - The record to store
371
372
  * @param record.key - Record key (unique identifier)
372
- * @param record.value - Record value (a JSON-serializable value, Buffer, ArrayBuffer, typed array, or Readable)
373
+ * @param record.value - Record value (a JSON-serializable value, Buffer, ArrayBuffer, typed array, Blob, or
374
+ * Readable)
373
375
  * @param record.contentType - Optional MIME type. Auto-detected if not provided:
374
376
  * - Objects: `'application/json; charset=utf-8'`
375
377
  * - Strings: `'text/plain; charset=utf-8'`
378
+ * - Blobs: their `type`, or `'application/octet-stream'` when it is empty
376
379
  * - Binary values and streams: `'application/octet-stream'`
377
380
  *
378
381
  * Worth setting for media and archives: the client skips compressing a body
@@ -435,18 +438,20 @@ export class KeyValueStoreClient extends ResourceClient {
435
438
  if (contentEncoding !== undefined && !runtime.isNode) {
436
439
  throw new Error('The contentEncoding option can only be used in Node.js environment.');
437
440
  }
438
- const isValueStreamOrBuffer = isStream(value) || isBuffer(value);
441
+ const isBinaryValue = isStream(value) || isBuffer(value) || isBlob(value);
439
442
  // The client forwards the header without inspecting the body, so a string or a value serialized to JSON, which
440
443
  // cannot hold compressed bytes, would be stored under an encoding that misdescribes it.
441
444
  const declaresCompression = contentEncoding !== undefined && contentEncoding.trim().toLowerCase() !== 'identity';
442
- if (declaresCompression && !isValueStreamOrBuffer) {
445
+ if (declaresCompression && !isBinaryValue) {
443
446
  throw new TypeError(`Cannot upload a value of type ${typeof value} with Content-Encoding: ${contentEncoding}. ` +
444
447
  'An encoding other than identity declares the value is already compressed, so pass the ' +
445
- 'compressed bytes as a Buffer, an ArrayBuffer, a typed array, or a readable stream.');
448
+ 'compressed bytes as a Buffer, an ArrayBuffer, a typed array, a Blob, or a readable stream.');
446
449
  }
447
450
  // To allow saving Objects to JSON without providing content type
448
451
  if (!contentType) {
449
- if (isValueStreamOrBuffer)
452
+ if (isBlob(value))
453
+ contentType = value.type || 'application/octet-stream';
454
+ else if (isBinaryValue)
450
455
  contentType = 'application/octet-stream';
451
456
  else if (typeof value === 'string')
452
457
  contentType = 'text/plain; charset=utf-8';
@@ -454,7 +459,7 @@ export class KeyValueStoreClient extends ResourceClient {
454
459
  contentType = 'application/json; charset=utf-8';
455
460
  }
456
461
  const isContentTypeJson = contentType.startsWith('application/json');
457
- if (isContentTypeJson && !isValueStreamOrBuffer && typeof value !== 'string') {
462
+ if (isContentTypeJson && !isBinaryValue && typeof value !== 'string') {
458
463
  try {
459
464
  value = JSON.stringify(value, null, 2);
460
465
  }
@@ -1,4 +1,5 @@
1
1
  import os from 'node:os';
2
+ import { Readable } from 'node:stream';
2
3
  import { brotliCompress, constants, gzip } from 'node:zlib';
3
4
  /**
4
5
  * The Node.js implementation of {@link Runtime}. Deno and Bun resolve the `node` condition too and
@@ -46,4 +47,34 @@ export const runtime = {
46
47
  });
47
48
  return { httpAgent: agent, httpsAgent: agent };
48
49
  },
50
+ openBlobBody(blob, onSourceError) {
51
+ return Readable.from(readBlob(blob, onSourceError), { objectMode: false });
52
+ },
49
53
  };
54
+ /**
55
+ * Yields the bytes of a blob. Ending the iteration early, which destroying the `Readable` around it does, cancels
56
+ * the read and releases a file the blob reads from. Only a failing read is reported: an error the `Readable` is
57
+ * destroyed with is thrown in at the `yield` and belongs to the transport. A `for await` loop would surface both at
58
+ * the same place, so the loop calls `next()` itself to catch only the read.
59
+ */
60
+ async function* readBlob(blob, onSourceError) {
61
+ const chunks = blob.stream()[Symbol.asyncIterator]();
62
+ try {
63
+ while (true) {
64
+ let chunk;
65
+ try {
66
+ chunk = await chunks.next();
67
+ }
68
+ catch (error) {
69
+ onSourceError(error);
70
+ throw error;
71
+ }
72
+ if (chunk.done)
73
+ return;
74
+ yield chunk.value;
75
+ }
76
+ }
77
+ finally {
78
+ await chunks.return?.();
79
+ }
80
+ }
@@ -1,5 +1,6 @@
1
1
  import type http from 'node:http';
2
2
  import type https from 'node:https';
3
+ import type { Readable } from 'node:stream';
3
4
  /**
4
5
  * What to compress a request body with, as the built-in `HttpCompressor` implementations ask for it.
5
6
  * @internal
@@ -49,4 +50,11 @@ export interface Runtime {
49
50
  createHttpAgents(options: {
50
51
  timeoutMillis: number;
51
52
  }): Promise<HttpAgents | undefined>;
53
+ /**
54
+ * Opens a `Blob` request body for one attempt. The Node.js implementation reads the blob into a `Readable` and
55
+ * reports an error the blob raises while it is read to `onSourceError` before the stream fails with it, so the
56
+ * pipeline can tell a failing source apart from a failing connection. The Web API one returns the blob itself,
57
+ * which `fetch` and `XMLHttpRequest` read on their own, and never calls `onSourceError`.
58
+ */
59
+ openBlobBody(blob: Blob, onSourceError: (error: unknown) => void): Readable | Blob;
52
60
  }
@@ -15,4 +15,7 @@ export const runtime = {
15
15
  async createHttpAgents() {
16
16
  return undefined;
17
17
  },
18
+ openBlobBody(blob) {
19
+ return blob;
20
+ },
18
21
  };
package/dist/utils.d.ts CHANGED
@@ -101,6 +101,11 @@ export declare function splitIntoJsonArrayBatches<T extends {
101
101
  maxByteLength: number;
102
102
  }): T[][];
103
103
  export declare function isBuffer(value: unknown): value is Buffer | ArrayBuffer | TypedArray;
104
+ /**
105
+ * Whether a value is a `Blob`, a `File` included. Unlike a stream, a blob can be read again for every attempt of a
106
+ * request, so a request with a blob body can be retried.
107
+ */
108
+ export declare function isBlob(value: unknown): value is Blob;
104
109
  export declare function isStream(value: unknown): value is Readable;
105
110
  /**
106
111
  * Helper class to create async iterators from paginated list endpoints.
package/dist/utils.js CHANGED
@@ -236,6 +236,13 @@ export function isBuffer(value) {
236
236
  return !isTagged(value, 'DataView');
237
237
  return isTagged(value, 'ArrayBuffer');
238
238
  }
239
+ /**
240
+ * Whether a value is a `Blob`, a `File` included. Unlike a stream, a blob can be read again for every attempt of a
241
+ * request, so a request with a blob body can be retried.
242
+ */
243
+ export function isBlob(value) {
244
+ return isTagged(value, 'Blob') || isTagged(value, 'File');
245
+ }
239
246
  function isTagged(value, tag) {
240
247
  return Object.prototype.toString.call(value) === `[object ${tag}]`;
241
248
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "apify-client",
3
- "version": "3.0.0-beta.71",
3
+ "version": "3.0.0-beta.73",
4
4
  "description": "Apify API client for JavaScript",
5
5
  "engines": {
6
6
  "node": ">=22.13.0"