@crawlee/http 4.0.0-beta.99 → 4.0.0-rc.0
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/internals/http-crawler.d.ts +124 -103
- package/internals/http-crawler.js +76 -72
- package/internals/utils.js +4 -6
- package/package.json +9 -9
|
@@ -2,8 +2,9 @@ import type { BasicCrawlerOptions, ConcurrencySystem, ConcurrencySystemOptions,
|
|
|
2
2
|
import { BasicCrawler, ContextPipeline } from '@crawlee/basic';
|
|
3
3
|
import { type LoadedRequest } from '@crawlee/core';
|
|
4
4
|
import type { Awaitable, Dictionary } from '@crawlee/types';
|
|
5
|
-
import { type CheerioRoot } from '@crawlee/utils';
|
|
5
|
+
import { type CheerioRoot } from '@crawlee/utils/internal';
|
|
6
6
|
import type { JsonValue } from 'type-fest';
|
|
7
|
+
import { z } from 'zod';
|
|
7
8
|
/**
|
|
8
9
|
* A higher starting concurrency and a relaxed event loop signal, since HTTP-only crawling barely touches the event
|
|
9
10
|
* loop. {@link HttpCrawler} folds these into the {@link ConcurrencySystem} it builds by default.
|
|
@@ -19,7 +20,7 @@ export declare const HTTP_OPTIMIZED_CONCURRENCY_SYSTEM_OPTIONS: ConcurrencySyste
|
|
|
19
20
|
export type HttpErrorHandler<UserData extends Dictionary = any, // with default to Dictionary we cant use a typed router in untyped crawler
|
|
20
21
|
JSONData extends JsonValue = any, // with default to Dictionary we cant use a typed router in untyped crawler
|
|
21
22
|
ContextExtension = Dictionary<never>> = ErrorHandler<CrawlingContext, HttpCrawlingContext<UserData, JSONData> & ContextExtension>;
|
|
22
|
-
export interface HttpCrawlerOptions<Context extends InternalHttpCrawlingContext = InternalHttpCrawlingContext, ContextExtension = Dictionary<never>, ExtendedContext extends Context = Context & ContextExtension, Routes extends Record<keyof Routes, Dictionary> = Record<string, GetUserDataFromRequest<Context['request']
|
|
23
|
+
export interface HttpCrawlerOptions<Context extends InternalHttpCrawlingContext = InternalHttpCrawlingContext, ContextExtension = Dictionary<never>, ExtendedContext extends Context = Context & ContextExtension, Routes extends Record<keyof Routes, Dictionary> = Record<string, GetUserDataFromRequest<Context['request']>>, StatisticStateExtension extends object = {}> extends BasicCrawlerOptions<Context, ContextExtension, ExtendedContext, Routes, StatisticStateExtension> {
|
|
23
24
|
/**
|
|
24
25
|
* Timeout for the whole navigation phase, given in seconds. A single window shared by the
|
|
25
26
|
* `preNavigationHooks`, the navigation (the HTTP request to the resource), and the `postNavigationHooks` -
|
|
@@ -29,9 +30,14 @@ export interface HttpCrawlerOptions<Context extends InternalHttpCrawlingContext
|
|
|
29
30
|
*/
|
|
30
31
|
navigationTimeoutSecs?: number;
|
|
31
32
|
/**
|
|
32
|
-
* If set to true
|
|
33
|
+
* If set to `true`, TLS/SSL certificate errors are ignored. Forwarded to the HTTP client as
|
|
34
|
+
* {@link SendRequestOptions.ignoreTlsErrors|`ignoreTlsErrors`} on every navigation request, so custom
|
|
35
|
+
* {@link BaseHttpClient} implementations should honor that flag (the built-in impit and got-scraping
|
|
36
|
+
* clients do; the native fetch fallback cannot disable TLS verification and warns instead).
|
|
37
|
+
*
|
|
38
|
+
* @default true
|
|
33
39
|
*/
|
|
34
|
-
|
|
40
|
+
ignoreTlsErrors?: boolean;
|
|
35
41
|
/**
|
|
36
42
|
* Async functions that are sequentially evaluated before the navigation. Good for setting additional cookies
|
|
37
43
|
* or browser properties before navigation. The function accepts one parameter `crawlingContext`,
|
|
@@ -53,7 +59,7 @@ export interface HttpCrawlerOptions<Context extends InternalHttpCrawlingContext
|
|
|
53
59
|
* ]
|
|
54
60
|
* ```
|
|
55
61
|
*/
|
|
56
|
-
preNavigationHooks?: InternalHttpHook<CrawlingContext
|
|
62
|
+
preNavigationHooks?: InternalHttpHook<CrawlingContext<any>, ContextExtension>[];
|
|
57
63
|
/**
|
|
58
64
|
* Async functions that are sequentially evaluated after the navigation. Good for checking if the navigation was successful.
|
|
59
65
|
* The function accepts `crawlingContext` as the only parameter.
|
|
@@ -241,107 +247,122 @@ JSONData extends JsonValue = any> = RequestHandler<HttpCrawlingContext<UserData,
|
|
|
241
247
|
* ```
|
|
242
248
|
* @category Crawlers
|
|
243
249
|
*/
|
|
244
|
-
export declare class HttpCrawler<Context extends InternalHttpCrawlingContext<any, any> = InternalHttpCrawlingContext, ContextExtension = Dictionary<never>, ExtendedContext extends Context = Context & ContextExtension, Routes extends Record<keyof Routes, Dictionary> = Record<string, GetUserDataFromRequest<Context['request']
|
|
245
|
-
private
|
|
246
|
-
private postNavigationHooks;
|
|
247
|
-
private saveResponseCookies;
|
|
248
|
-
private navigationTimeoutMillis;
|
|
249
|
-
private ignoreSslErrors;
|
|
250
|
-
private suggestResponseEncoding?;
|
|
251
|
-
private forceResponseEncoding?;
|
|
252
|
-
private readonly supportedMimeTypes;
|
|
250
|
+
export declare class HttpCrawler<Context extends InternalHttpCrawlingContext<any, any> = InternalHttpCrawlingContext, ContextExtension = Dictionary<never>, ExtendedContext extends Context = Context & ContextExtension, Routes extends Record<keyof Routes, Dictionary> = Record<string, GetUserDataFromRequest<Context['request']>>, StatisticStateExtension extends object = {}> extends BasicCrawler<Context, ContextExtension, ExtendedContext, Routes, StatisticStateExtension> {
|
|
251
|
+
#private;
|
|
253
252
|
protected static optionsShape: {
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
// @ts-ignore optional peer dependency or compatibility with es2022
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
additionalHttpErrorStatusCodes: import("ow").ArrayPredicate<number>;
|
|
308
|
-
// @ts-ignore optional peer dependency or compatibility with es2022
|
|
309
|
-
ignoreHttpErrorStatusCodes: import("ow").ArrayPredicate<number>;
|
|
310
|
-
// @ts-ignore optional peer dependency or compatibility with es2022
|
|
311
|
-
blockedStatusCodes: import("ow").ArrayPredicate<number>;
|
|
312
|
-
// @ts-ignore optional peer dependency or compatibility with es2022
|
|
313
|
-
retryOnBlocked: import("ow").BooleanPredicate & import("ow").BasePredicate<boolean | undefined>;
|
|
314
|
-
// @ts-ignore optional peer dependency or compatibility with es2022
|
|
315
|
-
respectRobotsTxtFile: import("ow").AnyPredicate<boolean | object>;
|
|
316
|
-
// @ts-ignore optional peer dependency or compatibility with es2022
|
|
317
|
-
onSkippedRequest: import("ow").Predicate<Function> & import("ow").BasePredicate<Function | undefined>;
|
|
318
|
-
// @ts-ignore optional peer dependency or compatibility with es2022
|
|
319
|
-
httpClient: import("ow").ObjectPredicate<object> & import("ow").BasePredicate<object | undefined>;
|
|
320
|
-
// @ts-ignore optional peer dependency or compatibility with es2022
|
|
321
|
-
configuration: import("ow").ObjectPredicate<object> & import("ow").BasePredicate<object | undefined>;
|
|
322
|
-
// @ts-ignore optional peer dependency or compatibility with es2022
|
|
323
|
-
storageBackend: import("ow").ObjectPredicate<object> & import("ow").BasePredicate<object | undefined>;
|
|
324
|
-
// @ts-ignore optional peer dependency or compatibility with es2022
|
|
325
|
-
eventManager: import("ow").ObjectPredicate<object> & import("ow").BasePredicate<object | undefined>;
|
|
326
|
-
// @ts-ignore optional peer dependency or compatibility with es2022
|
|
327
|
-
logger: import("ow").ObjectPredicate<object> & import("ow").BasePredicate<object | undefined>;
|
|
328
|
-
// @ts-ignore optional peer dependency or compatibility with es2022
|
|
329
|
-
minConcurrency: import("ow").NumberPredicate & import("ow").BasePredicate<number | undefined>;
|
|
330
|
-
// @ts-ignore optional peer dependency or compatibility with es2022
|
|
331
|
-
maxConcurrency: import("ow").NumberPredicate & import("ow").BasePredicate<number | undefined>;
|
|
332
|
-
// @ts-ignore optional peer dependency or compatibility with es2022
|
|
333
|
-
maxRequestsPerMinute: import("ow").NumberPredicate & import("ow").BasePredicate<number | undefined>;
|
|
334
|
-
// @ts-ignore optional peer dependency or compatibility with es2022
|
|
335
|
-
keepAlive: import("ow").BooleanPredicate & import("ow").BasePredicate<boolean | undefined>;
|
|
336
|
-
// @ts-ignore optional peer dependency or compatibility with es2022
|
|
337
|
-
statisticsOptions: import("ow").ObjectPredicate<object> & import("ow").BasePredicate<object | undefined>;
|
|
338
|
-
// @ts-ignore optional peer dependency or compatibility with es2022
|
|
339
|
-
id: import("ow").StringPredicate & import("ow").BasePredicate<string | undefined>;
|
|
253
|
+
navigationTimeoutSecs: z.ZodDefault<z.ZodCustom<number, number>>;
|
|
254
|
+
ignoreTlsErrors: z.ZodDefault<z.ZodBoolean>;
|
|
255
|
+
additionalMimeTypes: z.ZodDefault<z.ZodArray<z.ZodString>>;
|
|
256
|
+
suggestResponseEncoding: z.ZodOptional<z.ZodString>;
|
|
257
|
+
forceResponseEncoding: z.ZodOptional<z.ZodString>;
|
|
258
|
+
saveResponseCookies: z.ZodDefault<z.ZodBoolean>;
|
|
259
|
+
preNavigationHooks: z.ZodDefault<z.ZodCustom<unknown[], unknown[]>>;
|
|
260
|
+
postNavigationHooks: z.ZodDefault<z.ZodCustom<unknown[], unknown[]>>;
|
|
261
|
+
contextPipelineBuilder: z.ZodOptional<z.ZodCustom<Dictionary, Dictionary>>;
|
|
262
|
+
extendContext: z.ZodOptional<z.ZodCustom<(...args: any[]) => unknown, (...args: any[]) => unknown>>;
|
|
263
|
+
requestList: z.ZodOptional<z.ZodType<Dictionary<any>, unknown, z.core.$ZodTypeInternals<Dictionary<any>, unknown>>>;
|
|
264
|
+
requestQueue: z.ZodOptional<z.ZodType<Dictionary<any>, unknown, z.core.$ZodTypeInternals<Dictionary<any>, unknown>>>;
|
|
265
|
+
requestManager: z.ZodOptional<z.ZodType<Dictionary<any>, unknown, z.core.$ZodTypeInternals<Dictionary<any>, unknown>>>;
|
|
266
|
+
requestHandler: z.ZodOptional<z.ZodCustom<(...args: any[]) => unknown, (...args: any[]) => unknown>>;
|
|
267
|
+
requestHandlerTimeoutSecs: z.ZodOptional<z.ZodCustom<number, number>>;
|
|
268
|
+
errorHandler: z.ZodOptional<z.ZodCustom<(...args: any[]) => unknown, (...args: any[]) => unknown>>;
|
|
269
|
+
failedRequestHandler: z.ZodOptional<z.ZodCustom<(...args: any[]) => unknown, (...args: any[]) => unknown>>;
|
|
270
|
+
maxRequestRetries: z.ZodDefault<z.ZodCustom<number, number>>;
|
|
271
|
+
sameDomainDelaySecs: z.ZodDefault<z.ZodCustom<number, number>>;
|
|
272
|
+
maxRequestsPerCrawl: z.ZodOptional<z.ZodCustom<number, number>>;
|
|
273
|
+
maxCrawlDepth: z.ZodOptional<z.ZodCustom<number, number>>;
|
|
274
|
+
taskLoopOptions: z.ZodOptional<z.ZodCustom<Dictionary, Dictionary>>;
|
|
275
|
+
concurrencySystem: z.ZodOptional<z.ZodCustom<Dictionary, Dictionary>>;
|
|
276
|
+
sessionPool: z.ZodOptional<z.ZodType<Dictionary<any>, unknown, z.core.$ZodTypeInternals<Dictionary<any>, unknown>>>;
|
|
277
|
+
proxyConfiguration: z.ZodOptional<z.ZodType<Dictionary<any>, unknown, z.core.$ZodTypeInternals<Dictionary<any>, unknown>>>;
|
|
278
|
+
statusMessageLoggingInterval: z.ZodDefault<z.ZodCustom<number, number>>;
|
|
279
|
+
statusMessageCallback: z.ZodOptional<z.ZodCustom<(...args: any[]) => unknown, (...args: any[]) => unknown>>;
|
|
280
|
+
additionalHttpErrorStatusCodes: z.ZodDefault<z.ZodArray<z.ZodCustom<number, number>>>;
|
|
281
|
+
ignoreHttpErrorStatusCodes: z.ZodDefault<z.ZodArray<z.ZodCustom<number, number>>>;
|
|
282
|
+
blockedStatusCodes: z.ZodOptional<z.ZodArray<z.ZodCustom<number, number>>>;
|
|
283
|
+
retryOnBlocked: z.ZodDefault<z.ZodBoolean>;
|
|
284
|
+
respectRobotsTxtFile: z.ZodDefault<z.ZodUnion<readonly [z.ZodBoolean, z.ZodCustom<Dictionary, Dictionary>]>>;
|
|
285
|
+
transactionalStorage: z.ZodOptional<z.ZodUnion<readonly [z.ZodBoolean, z.ZodObject<{
|
|
286
|
+
requestQueue: z.ZodOptional<z.ZodEnum<{
|
|
287
|
+
deferred: "deferred";
|
|
288
|
+
writeThrough: "writeThrough";
|
|
289
|
+
}>>;
|
|
290
|
+
}, z.core.$strict>]>>;
|
|
291
|
+
onSkippedRequest: z.ZodOptional<z.ZodCustom<(...args: any[]) => unknown, (...args: any[]) => unknown>>;
|
|
292
|
+
// @ts-ignore optional peer dependency or compatibility with es2022
|
|
293
|
+
httpClient: z.ZodOptional<z.ZodCustom<import("@crawlee/http-client").BaseHttpClient, import("@crawlee/http-client").BaseHttpClient>>;
|
|
294
|
+
// @ts-ignore optional peer dependency or compatibility with es2022
|
|
295
|
+
configuration: z.ZodOptional<z.ZodCustom<import("@crawlee/basic").Configuration, import("@crawlee/basic").Configuration>>;
|
|
296
|
+
storageBackend: z.ZodOptional<z.ZodType<Dictionary<any>, unknown, z.core.$ZodTypeInternals<Dictionary<any>, unknown>>>;
|
|
297
|
+
// @ts-ignore optional peer dependency or compatibility with es2022
|
|
298
|
+
eventManager: z.ZodOptional<z.ZodCustom<import("@crawlee/basic").EventManager, import("@crawlee/basic").EventManager>>;
|
|
299
|
+
logger: z.ZodOptional<z.ZodType<Dictionary<any>, unknown, z.core.$ZodTypeInternals<Dictionary<any>, unknown>>>;
|
|
300
|
+
minConcurrency: z.ZodOptional<z.ZodCustom<number, number>>;
|
|
301
|
+
maxConcurrency: z.ZodOptional<z.ZodCustom<number, number>>;
|
|
302
|
+
maxRequestsPerMinute: z.ZodOptional<z.ZodCustom<number, number>>;
|
|
303
|
+
keepAlive: z.ZodOptional<z.ZodBoolean>;
|
|
304
|
+
statistics: z.ZodOptional<z.ZodCustom<Dictionary, Dictionary>>;
|
|
305
|
+
id: z.ZodOptional<z.ZodString>;
|
|
340
306
|
};
|
|
307
|
+
protected static optionsSchema: z.ZodObject<{
|
|
308
|
+
navigationTimeoutSecs: z.ZodDefault<z.ZodCustom<number, number>>;
|
|
309
|
+
ignoreTlsErrors: z.ZodDefault<z.ZodBoolean>;
|
|
310
|
+
additionalMimeTypes: z.ZodDefault<z.ZodArray<z.ZodString>>;
|
|
311
|
+
suggestResponseEncoding: z.ZodOptional<z.ZodString>;
|
|
312
|
+
forceResponseEncoding: z.ZodOptional<z.ZodString>;
|
|
313
|
+
saveResponseCookies: z.ZodDefault<z.ZodBoolean>;
|
|
314
|
+
preNavigationHooks: z.ZodDefault<z.ZodCustom<unknown[], unknown[]>>;
|
|
315
|
+
postNavigationHooks: z.ZodDefault<z.ZodCustom<unknown[], unknown[]>>;
|
|
316
|
+
contextPipelineBuilder: z.ZodOptional<z.ZodCustom<Dictionary, Dictionary>>;
|
|
317
|
+
extendContext: z.ZodOptional<z.ZodCustom<(...args: any[]) => unknown, (...args: any[]) => unknown>>;
|
|
318
|
+
requestList: z.ZodOptional<z.ZodType<Dictionary<any>, unknown, z.core.$ZodTypeInternals<Dictionary<any>, unknown>>>;
|
|
319
|
+
requestQueue: z.ZodOptional<z.ZodType<Dictionary<any>, unknown, z.core.$ZodTypeInternals<Dictionary<any>, unknown>>>;
|
|
320
|
+
requestManager: z.ZodOptional<z.ZodType<Dictionary<any>, unknown, z.core.$ZodTypeInternals<Dictionary<any>, unknown>>>;
|
|
321
|
+
requestHandler: z.ZodOptional<z.ZodCustom<(...args: any[]) => unknown, (...args: any[]) => unknown>>;
|
|
322
|
+
requestHandlerTimeoutSecs: z.ZodOptional<z.ZodCustom<number, number>>;
|
|
323
|
+
errorHandler: z.ZodOptional<z.ZodCustom<(...args: any[]) => unknown, (...args: any[]) => unknown>>;
|
|
324
|
+
failedRequestHandler: z.ZodOptional<z.ZodCustom<(...args: any[]) => unknown, (...args: any[]) => unknown>>;
|
|
325
|
+
maxRequestRetries: z.ZodDefault<z.ZodCustom<number, number>>;
|
|
326
|
+
sameDomainDelaySecs: z.ZodDefault<z.ZodCustom<number, number>>;
|
|
327
|
+
maxRequestsPerCrawl: z.ZodOptional<z.ZodCustom<number, number>>;
|
|
328
|
+
maxCrawlDepth: z.ZodOptional<z.ZodCustom<number, number>>;
|
|
329
|
+
taskLoopOptions: z.ZodOptional<z.ZodCustom<Dictionary, Dictionary>>;
|
|
330
|
+
concurrencySystem: z.ZodOptional<z.ZodCustom<Dictionary, Dictionary>>;
|
|
331
|
+
sessionPool: z.ZodOptional<z.ZodType<Dictionary<any>, unknown, z.core.$ZodTypeInternals<Dictionary<any>, unknown>>>;
|
|
332
|
+
proxyConfiguration: z.ZodOptional<z.ZodType<Dictionary<any>, unknown, z.core.$ZodTypeInternals<Dictionary<any>, unknown>>>;
|
|
333
|
+
statusMessageLoggingInterval: z.ZodDefault<z.ZodCustom<number, number>>;
|
|
334
|
+
statusMessageCallback: z.ZodOptional<z.ZodCustom<(...args: any[]) => unknown, (...args: any[]) => unknown>>;
|
|
335
|
+
additionalHttpErrorStatusCodes: z.ZodDefault<z.ZodArray<z.ZodCustom<number, number>>>;
|
|
336
|
+
ignoreHttpErrorStatusCodes: z.ZodDefault<z.ZodArray<z.ZodCustom<number, number>>>;
|
|
337
|
+
blockedStatusCodes: z.ZodOptional<z.ZodArray<z.ZodCustom<number, number>>>;
|
|
338
|
+
retryOnBlocked: z.ZodDefault<z.ZodBoolean>;
|
|
339
|
+
respectRobotsTxtFile: z.ZodDefault<z.ZodUnion<readonly [z.ZodBoolean, z.ZodCustom<Dictionary, Dictionary>]>>;
|
|
340
|
+
transactionalStorage: z.ZodOptional<z.ZodUnion<readonly [z.ZodBoolean, z.ZodObject<{
|
|
341
|
+
requestQueue: z.ZodOptional<z.ZodEnum<{
|
|
342
|
+
deferred: "deferred";
|
|
343
|
+
writeThrough: "writeThrough";
|
|
344
|
+
}>>;
|
|
345
|
+
}, z.core.$strict>]>>;
|
|
346
|
+
onSkippedRequest: z.ZodOptional<z.ZodCustom<(...args: any[]) => unknown, (...args: any[]) => unknown>>;
|
|
347
|
+
// @ts-ignore optional peer dependency or compatibility with es2022
|
|
348
|
+
httpClient: z.ZodOptional<z.ZodCustom<import("@crawlee/http-client").BaseHttpClient, import("@crawlee/http-client").BaseHttpClient>>;
|
|
349
|
+
// @ts-ignore optional peer dependency or compatibility with es2022
|
|
350
|
+
configuration: z.ZodOptional<z.ZodCustom<import("@crawlee/basic").Configuration, import("@crawlee/basic").Configuration>>;
|
|
351
|
+
storageBackend: z.ZodOptional<z.ZodType<Dictionary<any>, unknown, z.core.$ZodTypeInternals<Dictionary<any>, unknown>>>;
|
|
352
|
+
// @ts-ignore optional peer dependency or compatibility with es2022
|
|
353
|
+
eventManager: z.ZodOptional<z.ZodCustom<import("@crawlee/basic").EventManager, import("@crawlee/basic").EventManager>>;
|
|
354
|
+
logger: z.ZodOptional<z.ZodType<Dictionary<any>, unknown, z.core.$ZodTypeInternals<Dictionary<any>, unknown>>>;
|
|
355
|
+
minConcurrency: z.ZodOptional<z.ZodCustom<number, number>>;
|
|
356
|
+
maxConcurrency: z.ZodOptional<z.ZodCustom<number, number>>;
|
|
357
|
+
maxRequestsPerMinute: z.ZodOptional<z.ZodCustom<number, number>>;
|
|
358
|
+
keepAlive: z.ZodOptional<z.ZodBoolean>;
|
|
359
|
+
statistics: z.ZodOptional<z.ZodCustom<Dictionary, Dictionary>>;
|
|
360
|
+
id: z.ZodOptional<z.ZodString>;
|
|
361
|
+
}, z.core.$strict>;
|
|
341
362
|
/**
|
|
342
363
|
* All `HttpCrawlerOptions` parameters are passed via an options object.
|
|
343
364
|
*/
|
|
344
|
-
constructor(options?: HttpCrawlerOptions<Context, ContextExtension, ExtendedContext> & RequireContextPipeline<InternalHttpCrawlingContext, Context>);
|
|
365
|
+
constructor(options?: HttpCrawlerOptions<Context, ContextExtension, ExtendedContext, Routes, StatisticStateExtension> & RequireContextPipeline<InternalHttpCrawlingContext, Context>);
|
|
345
366
|
protected getNavigationTimeoutMillis(): number;
|
|
346
367
|
/**
|
|
347
368
|
* Folds {@link HTTP_OPTIMIZED_CONCURRENCY_SYSTEM_OPTIONS} into the default system, keeping the user's
|
|
@@ -379,11 +400,11 @@ export declare class HttpCrawler<Context extends InternalHttpCrawlingContext<any
|
|
|
379
400
|
* Handles timeout request
|
|
380
401
|
*/
|
|
381
402
|
private handleRequestTimeout;
|
|
382
|
-
private
|
|
403
|
+
private abortDownloadOfBody;
|
|
383
404
|
/**
|
|
384
405
|
* @internal wraps public utility for mocking purposes
|
|
385
406
|
*/
|
|
386
|
-
private
|
|
407
|
+
private requestAsBrowser;
|
|
387
408
|
}
|
|
388
409
|
/**
|
|
389
410
|
* Creates new {@link Router} instance that works based on request labels.
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import { Readable } from 'node:stream';
|
|
2
2
|
import util from 'node:util';
|
|
3
3
|
import { BasicCrawler, ContextPipeline, NavigationSkippedError, remainingNavigationWindowMillis, RequestState, Router, SessionError, } from '@crawlee/basic';
|
|
4
|
-
import { getCookiesFromResponse } from '@crawlee/core';
|
|
4
|
+
import { RequestThrottledError, getCookiesFromResponse, parseArgument, schemas, } from '@crawlee/core';
|
|
5
5
|
import { ResponseWithUrl } from '@crawlee/http-client';
|
|
6
|
-
import { RETRY_CSS_SELECTORS } from '@crawlee/utils';
|
|
6
|
+
import { RETRY_CSS_SELECTORS } from '@crawlee/utils/internal';
|
|
7
7
|
import contentTypeParser from 'content-type';
|
|
8
8
|
import iconv from 'iconv-lite';
|
|
9
|
-
import
|
|
9
|
+
import { z } from 'zod';
|
|
10
10
|
import { addTimeoutToPromise, storage, TimeoutError, tryCancel } from '@apify/timeout';
|
|
11
11
|
import { extractCharsetFromHtmlBytes, parseContentTypeFromResponse, processHttpRequestOptions } from './utils.js';
|
|
12
12
|
/**
|
|
@@ -109,60 +109,60 @@ export class HttpCrawler extends BasicCrawler {
|
|
|
109
109
|
// extension-aware for consumer DX, but internally the pipeline composes hooks against the
|
|
110
110
|
// concrete crawling context, which does not statically carry `ContextExtension`. The members
|
|
111
111
|
// added by `extendContext` are present at runtime regardless.
|
|
112
|
-
preNavigationHooks;
|
|
113
|
-
postNavigationHooks;
|
|
114
|
-
saveResponseCookies;
|
|
115
|
-
navigationTimeoutMillis;
|
|
116
|
-
|
|
117
|
-
suggestResponseEncoding;
|
|
118
|
-
forceResponseEncoding;
|
|
119
|
-
supportedMimeTypes;
|
|
112
|
+
#preNavigationHooks;
|
|
113
|
+
#postNavigationHooks;
|
|
114
|
+
#saveResponseCookies;
|
|
115
|
+
#navigationTimeoutMillis;
|
|
116
|
+
#ignoreTlsErrors;
|
|
117
|
+
#suggestResponseEncoding;
|
|
118
|
+
#forceResponseEncoding;
|
|
119
|
+
#supportedMimeTypes;
|
|
120
120
|
static optionsShape = {
|
|
121
121
|
...BasicCrawler.optionsShape,
|
|
122
|
-
navigationTimeoutSecs:
|
|
123
|
-
|
|
124
|
-
additionalMimeTypes:
|
|
125
|
-
suggestResponseEncoding:
|
|
126
|
-
forceResponseEncoding:
|
|
127
|
-
saveResponseCookies:
|
|
128
|
-
preNavigationHooks:
|
|
129
|
-
postNavigationHooks:
|
|
122
|
+
navigationTimeoutSecs: schemas.anyNumber.default(30),
|
|
123
|
+
ignoreTlsErrors: z.boolean().default(true),
|
|
124
|
+
additionalMimeTypes: schemas.arrayOf(z.string(), 'strings').default(() => []),
|
|
125
|
+
suggestResponseEncoding: z.string().optional(),
|
|
126
|
+
forceResponseEncoding: z.string().optional(),
|
|
127
|
+
saveResponseCookies: z.boolean().default(true),
|
|
128
|
+
preNavigationHooks: schemas.anyArray.default(() => []),
|
|
129
|
+
postNavigationHooks: schemas.anyArray.default(() => []),
|
|
130
130
|
};
|
|
131
|
+
static optionsSchema = z.strictObject(HttpCrawler.optionsShape);
|
|
131
132
|
/**
|
|
132
133
|
* All `HttpCrawlerOptions` parameters are passed via an options object.
|
|
133
134
|
*/
|
|
134
135
|
constructor(options = {}) {
|
|
135
|
-
|
|
136
|
-
const { navigationTimeoutSecs = 30, ignoreSslErrors = true, additionalMimeTypes = [], suggestResponseEncoding, forceResponseEncoding, saveResponseCookies = true, preNavigationHooks = [], postNavigationHooks = [],
|
|
136
|
+
const { navigationTimeoutSecs, ignoreTlsErrors, additionalMimeTypes, suggestResponseEncoding, forceResponseEncoding, saveResponseCookies, preNavigationHooks, postNavigationHooks,
|
|
137
137
|
// BasicCrawler
|
|
138
|
-
contextPipelineBuilder, ...basicCrawlerOptions } = options;
|
|
138
|
+
contextPipelineBuilder, ...basicCrawlerOptions } = parseArgument(options, HttpCrawler.optionsSchema, 'HttpCrawlerOptions');
|
|
139
139
|
super({
|
|
140
140
|
...basicCrawlerOptions,
|
|
141
141
|
contextPipelineBuilder: contextPipelineBuilder ??
|
|
142
142
|
(() => this.buildContextPipeline()),
|
|
143
143
|
});
|
|
144
|
-
this
|
|
144
|
+
this.#supportedMimeTypes = new Set([...HTML_AND_XML_MIME_TYPES, APPLICATION_JSON_MIME_TYPE]);
|
|
145
145
|
if (additionalMimeTypes.length)
|
|
146
146
|
this.extendSupportedMimeTypes(additionalMimeTypes);
|
|
147
147
|
if (suggestResponseEncoding && forceResponseEncoding) {
|
|
148
148
|
this.log.warning('Both forceResponseEncoding and suggestResponseEncoding options are set. Using forceResponseEncoding.');
|
|
149
149
|
}
|
|
150
|
-
this
|
|
151
|
-
this
|
|
152
|
-
this
|
|
153
|
-
this
|
|
150
|
+
this.#navigationTimeoutMillis = navigationTimeoutSecs * 1000;
|
|
151
|
+
this.#ignoreTlsErrors = ignoreTlsErrors;
|
|
152
|
+
this.#suggestResponseEncoding = suggestResponseEncoding;
|
|
153
|
+
this.#forceResponseEncoding = forceResponseEncoding;
|
|
154
154
|
// Cast away the extension-aware option types to the base internal storage types (see the field
|
|
155
155
|
// declarations above). This is sound - the hooks only ever receive the base context plus the
|
|
156
156
|
// members `extendContext` added at runtime.
|
|
157
|
-
this
|
|
158
|
-
this
|
|
159
|
-
({ request, response }) => this.
|
|
157
|
+
this.#preNavigationHooks = preNavigationHooks;
|
|
158
|
+
this.#postNavigationHooks = [
|
|
159
|
+
({ request, response }) => this.abortDownloadOfBody(request, response),
|
|
160
160
|
...postNavigationHooks,
|
|
161
161
|
];
|
|
162
|
-
this
|
|
162
|
+
this.#saveResponseCookies = saveResponseCookies;
|
|
163
163
|
}
|
|
164
164
|
getNavigationTimeoutMillis() {
|
|
165
|
-
return this
|
|
165
|
+
return this.#navigationTimeoutMillis;
|
|
166
166
|
}
|
|
167
167
|
/**
|
|
168
168
|
* Folds {@link HTTP_OPTIMIZED_CONCURRENCY_SYSTEM_OPTIONS} into the default system, keeping the user's
|
|
@@ -185,9 +185,9 @@ export class HttpCrawler extends BasicCrawler {
|
|
|
185
185
|
// A single navigation window covers the pre-navigation hooks, the navigation, and the post-navigation
|
|
186
186
|
// hooks: the whole phase shares one `navigationTimeoutSecs` budget, so a slow hook eats into the same
|
|
187
187
|
// window the navigation uses instead of each step being timed on its own.
|
|
188
|
-
const navigationTimedOut = `Navigation timed out after ${this
|
|
188
|
+
const navigationTimedOut = `Navigation timed out after ${this.#navigationTimeoutMillis / 1000} seconds.`;
|
|
189
189
|
const windowGuard = (step) => skipGuard(async (ctx) => {
|
|
190
|
-
const remaining = remainingNavigationWindowMillis(ctx, this
|
|
190
|
+
const remaining = remainingNavigationWindowMillis(ctx, this.#navigationTimeoutMillis);
|
|
191
191
|
if (remaining <= 0) {
|
|
192
192
|
throw new TimeoutError(navigationTimedOut);
|
|
193
193
|
}
|
|
@@ -196,11 +196,11 @@ export class HttpCrawler extends BasicCrawler {
|
|
|
196
196
|
let pipeline = ContextPipeline.create().compose({
|
|
197
197
|
action: this.prepareHttpRequest.bind(this),
|
|
198
198
|
});
|
|
199
|
-
for (const hook of this
|
|
199
|
+
for (const hook of this.#preNavigationHooks) {
|
|
200
200
|
pipeline = pipeline.compose(windowGuard(hook));
|
|
201
201
|
}
|
|
202
202
|
let pipelineWithNavigation = pipeline.compose(skipGuard(this.makeHttpRequest.bind(this)));
|
|
203
|
-
for (const hook of this
|
|
203
|
+
for (const hook of this.#postNavigationHooks) {
|
|
204
204
|
pipelineWithNavigation = pipelineWithNavigation.compose(windowGuard(hook));
|
|
205
205
|
}
|
|
206
206
|
return pipelineWithNavigation
|
|
@@ -234,7 +234,7 @@ export class HttpCrawler extends BasicCrawler {
|
|
|
234
234
|
// Bound the request by whatever is left of the shared navigation window (the pre-navigation hooks may
|
|
235
235
|
// have already spent part of it), so it produces a clean navigation-timeout error rather than the raw
|
|
236
236
|
// client abort.
|
|
237
|
-
const httpResponse = await addTimeoutToPromise(async () => this.requestFunction({ request, session, proxyUrl }), Math.max(1, remainingNavigationWindowMillis(crawlingContext, this
|
|
237
|
+
const httpResponse = await addTimeoutToPromise(async () => this.requestFunction({ request, session, proxyUrl }), Math.max(1, remainingNavigationWindowMillis(crawlingContext, this.#navigationTimeoutMillis)), `Navigation timed out after ${this.#navigationTimeoutMillis / 1000} seconds.`);
|
|
238
238
|
tryCancel();
|
|
239
239
|
request.loadedUrl = httpResponse?.url;
|
|
240
240
|
request.state = RequestState.AFTER_NAV;
|
|
@@ -261,14 +261,25 @@ export class HttpCrawler extends BasicCrawler {
|
|
|
261
261
|
};
|
|
262
262
|
}
|
|
263
263
|
tryCancel();
|
|
264
|
+
// Before `parseResponse`, which throws for error status codes - a 429 the user opted into treating as an
|
|
265
|
+
// error is still a rate limit the domain should back off from.
|
|
266
|
+
if (crawlingContext.response.status === 429) {
|
|
267
|
+
const retryAfter = crawlingContext.response.headers.get('retry-after');
|
|
268
|
+
if (this.recordDomainRateLimit(crawlingContext.request.url, retryAfter)) {
|
|
269
|
+
// This is the one path that never reads the body, so cancel it to release the connection
|
|
270
|
+
// rather than leaving it to the garbage collector.
|
|
271
|
+
await crawlingContext.response.body?.cancel().catch(() => { });
|
|
272
|
+
throw new RequestThrottledError(`${crawlingContext.request.url} responded with 429.`);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
264
275
|
// Reading the body is still part of the navigation, so it draws from the same shared window: on a server
|
|
265
276
|
// that streams the body slowly the request completes (headers arrive) but the body read would otherwise
|
|
266
277
|
// run unbounded. `extendTimeout` from a post-navigation hook has already pushed this deadline out if asked.
|
|
267
|
-
const remaining = remainingNavigationWindowMillis(crawlingContext, this
|
|
278
|
+
const remaining = remainingNavigationWindowMillis(crawlingContext, this.#navigationTimeoutMillis);
|
|
268
279
|
if (remaining <= 0) {
|
|
269
|
-
throw new TimeoutError(`Navigation timed out after ${this
|
|
280
|
+
throw new TimeoutError(`Navigation timed out after ${this.#navigationTimeoutMillis / 1000} seconds.`);
|
|
270
281
|
}
|
|
271
|
-
const parsed = await addTimeoutToPromise(async () => this.parseResponse(crawlingContext.request, crawlingContext.response), remaining, `Navigation timed out after ${this
|
|
282
|
+
const parsed = await addTimeoutToPromise(async () => this.parseResponse(crawlingContext.request, crawlingContext.response), remaining, `Navigation timed out after ${this.#navigationTimeoutMillis / 1000} seconds.`);
|
|
272
283
|
tryCancel();
|
|
273
284
|
const response = parsed.response;
|
|
274
285
|
const contentType = parsed.contentType;
|
|
@@ -287,14 +298,16 @@ export class HttpCrawler extends BasicCrawler {
|
|
|
287
298
|
}
|
|
288
299
|
return $;
|
|
289
300
|
};
|
|
290
|
-
this.
|
|
291
|
-
if (this
|
|
301
|
+
this.throwOnBlockedRequest(response.status);
|
|
302
|
+
if (this.#saveResponseCookies) {
|
|
292
303
|
try {
|
|
293
304
|
for (const cookie of getCookiesFromResponse(response)) {
|
|
294
305
|
if (!cookie)
|
|
295
306
|
continue;
|
|
296
307
|
try {
|
|
297
|
-
crawlingContext.session.cookieJar.
|
|
308
|
+
await crawlingContext.session.cookieJar.setCookie(cookie, response.url, {
|
|
309
|
+
ignoreError: false,
|
|
310
|
+
});
|
|
298
311
|
}
|
|
299
312
|
catch (e) {
|
|
300
313
|
this.log.debug(`Could not set cookie: ${e.message}`);
|
|
@@ -347,7 +360,7 @@ export class HttpCrawler extends BasicCrawler {
|
|
|
347
360
|
async requestFunction({ request, session, proxyUrl }) {
|
|
348
361
|
const opts = this.getRequestOptions(request, session, proxyUrl);
|
|
349
362
|
try {
|
|
350
|
-
return await this.
|
|
363
|
+
return await this.requestAsBrowser(opts, session);
|
|
351
364
|
}
|
|
352
365
|
catch (e) {
|
|
353
366
|
if (e instanceof Error && e.constructor.name === 'TimeoutError') {
|
|
@@ -355,7 +368,7 @@ export class HttpCrawler extends BasicCrawler {
|
|
|
355
368
|
return new Response(); // this will never happen, as handleRequestTimeout always throws
|
|
356
369
|
}
|
|
357
370
|
if (this.isProxyError(e)) {
|
|
358
|
-
throw new SessionError(this.
|
|
371
|
+
throw new SessionError(this.getMessageFromError(e));
|
|
359
372
|
}
|
|
360
373
|
else {
|
|
361
374
|
throw e;
|
|
@@ -371,7 +384,7 @@ export class HttpCrawler extends BasicCrawler {
|
|
|
371
384
|
const { response: reencodedResponse, encoding } = this.encodeResponse(request, response, charset);
|
|
372
385
|
const contentType = { type, encoding };
|
|
373
386
|
if (status >= 400 && status <= 599) {
|
|
374
|
-
this.
|
|
387
|
+
this.statistics.registerStatusCode(status);
|
|
375
388
|
}
|
|
376
389
|
if (this.isErrorStatusCode(status)) {
|
|
377
390
|
const body = await reencodedResponse.text(); // TODO - this always uses UTF-8 (see https://developer.mozilla.org/en-US/docs/Web/API/Request/text)
|
|
@@ -391,10 +404,10 @@ export class HttpCrawler extends BasicCrawler {
|
|
|
391
404
|
throw new Error(`${status} - Internal Server Error: ${body.slice(0, 100)}`);
|
|
392
405
|
}
|
|
393
406
|
else if (HTML_AND_XML_MIME_TYPES.includes(type)) {
|
|
394
|
-
if (!charset && !this
|
|
407
|
+
if (!charset && !this.#forceResponseEncoding) {
|
|
395
408
|
const rawBytes = Buffer.from(await response.arrayBuffer());
|
|
396
409
|
const metaCharset = extractCharsetFromHtmlBytes(rawBytes);
|
|
397
|
-
const charsetToUse = metaCharset ?? this
|
|
410
|
+
const charsetToUse = metaCharset ?? this.#suggestResponseEncoding ?? 'utf-8';
|
|
398
411
|
const body = iconv.encodingExists(charsetToUse)
|
|
399
412
|
? iconv.decode(rawBytes, charsetToUse)
|
|
400
413
|
: rawBytes.toString('utf8');
|
|
@@ -419,35 +432,25 @@ export class HttpCrawler extends BasicCrawler {
|
|
|
419
432
|
url: request.url,
|
|
420
433
|
method: request.method,
|
|
421
434
|
proxyUrl,
|
|
422
|
-
timeout: this
|
|
435
|
+
timeout: this.#navigationTimeoutMillis,
|
|
423
436
|
sessionToken: session,
|
|
424
437
|
headers: request.headers,
|
|
425
|
-
https: {
|
|
426
|
-
rejectUnauthorized: !this.ignoreSslErrors,
|
|
427
|
-
},
|
|
428
438
|
body: undefined,
|
|
429
439
|
};
|
|
430
440
|
if (requestOptions.headers?.cookie || requestOptions.headers?.Cookie) {
|
|
431
|
-
requestOptions.headers.Cookie = this.
|
|
441
|
+
requestOptions.headers.Cookie = this.getCookieHeaderFromRequest(request);
|
|
432
442
|
delete requestOptions.headers.cookie;
|
|
433
443
|
}
|
|
434
|
-
// Disable SSL verification for MITM proxies
|
|
435
|
-
if (session.proxyInfo?.ignoreTlsErrors) {
|
|
436
|
-
requestOptions.https = {
|
|
437
|
-
...requestOptions.https,
|
|
438
|
-
rejectUnauthorized: false,
|
|
439
|
-
};
|
|
440
|
-
}
|
|
441
444
|
if (/PATCH|POST|PUT/.test(request.method))
|
|
442
445
|
requestOptions.body = request.payload ?? '';
|
|
443
446
|
return requestOptions;
|
|
444
447
|
}
|
|
445
448
|
encodeResponse(request, response, encoding) {
|
|
446
|
-
if (this
|
|
447
|
-
encoding = this
|
|
449
|
+
if (this.#forceResponseEncoding) {
|
|
450
|
+
encoding = this.#forceResponseEncoding;
|
|
448
451
|
}
|
|
449
|
-
else if (!encoding && this
|
|
450
|
-
encoding = this
|
|
452
|
+
else if (!encoding && this.#suggestResponseEncoding) {
|
|
453
|
+
encoding = this.#suggestResponseEncoding;
|
|
451
454
|
}
|
|
452
455
|
// Fall back to utf-8 if we still don't have encoding.
|
|
453
456
|
const utf8 = 'utf8';
|
|
@@ -481,12 +484,12 @@ export class HttpCrawler extends BasicCrawler {
|
|
|
481
484
|
extendSupportedMimeTypes(additionalMimeTypes) {
|
|
482
485
|
for (const mimeType of additionalMimeTypes) {
|
|
483
486
|
if (mimeType === '*/*') {
|
|
484
|
-
this
|
|
487
|
+
this.#supportedMimeTypes.add(mimeType);
|
|
485
488
|
continue;
|
|
486
489
|
}
|
|
487
490
|
try {
|
|
488
491
|
const parsedType = contentTypeParser.parse(mimeType);
|
|
489
|
-
this
|
|
492
|
+
this.#supportedMimeTypes.add(parsedType.type);
|
|
490
493
|
}
|
|
491
494
|
catch (err) {
|
|
492
495
|
throw new Error(`Can not parse mime type ${mimeType} from "options.additionalMimeTypes".`);
|
|
@@ -498,22 +501,22 @@ export class HttpCrawler extends BasicCrawler {
|
|
|
498
501
|
*/
|
|
499
502
|
handleRequestTimeout(session) {
|
|
500
503
|
session.markBad();
|
|
501
|
-
throw new Error(`Request timed out after ${this
|
|
504
|
+
throw new Error(`Request timed out after ${this.#navigationTimeoutMillis / 1000} seconds.`);
|
|
502
505
|
}
|
|
503
|
-
|
|
506
|
+
abortDownloadOfBody(request, response) {
|
|
504
507
|
const { status } = response;
|
|
505
508
|
const { type } = parseContentTypeFromResponse(response);
|
|
506
509
|
const isTransientContentType = status >= 500 || this.blockedStatusCodes.has(status);
|
|
507
|
-
if (!this
|
|
510
|
+
if (!this.#supportedMimeTypes.has(type) && !this.#supportedMimeTypes.has('*/*') && !isTransientContentType) {
|
|
508
511
|
request.noRetry = true;
|
|
509
512
|
throw new Error(`Resource ${request.url} served Content-Type ${type}, ` +
|
|
510
|
-
`but only ${Array.from(this
|
|
513
|
+
`but only ${Array.from(this.#supportedMimeTypes).join(', ')} are allowed. Skipping resource.`);
|
|
511
514
|
}
|
|
512
515
|
}
|
|
513
516
|
/**
|
|
514
517
|
* @internal wraps public utility for mocking purposes
|
|
515
518
|
*/
|
|
516
|
-
|
|
519
|
+
requestAsBrowser = async (options, session) => {
|
|
517
520
|
const opts = processHttpRequestOptions({
|
|
518
521
|
...options,
|
|
519
522
|
responseType: 'text',
|
|
@@ -521,7 +524,7 @@ export class HttpCrawler extends BasicCrawler {
|
|
|
521
524
|
// When saveResponseCookies is false, the response cookies must not mutate the
|
|
522
525
|
// session jar. Reads still go through the session (so session.setCookie() in pre-nav
|
|
523
526
|
// hooks keeps working) but a per-request clone is passed in so writes are discarded.
|
|
524
|
-
const cookieJar = this
|
|
527
|
+
const cookieJar = this.#saveResponseCookies ? session.cookieJar : await session.cookieJar.clone();
|
|
525
528
|
// Bind the request to the shared navigation window instead of a fixed per-request timeout, so
|
|
526
529
|
// `extendTimeout()` can push the deadline and a fixed `AbortSignal.timeout` won't fire on its own and
|
|
527
530
|
// kill a lazily-read body mid-extension. This aborts the socket only during the header phase; the body
|
|
@@ -539,6 +542,7 @@ export class HttpCrawler extends BasicCrawler {
|
|
|
539
542
|
cookieJar,
|
|
540
543
|
signal: cancelSignal,
|
|
541
544
|
timeoutMillis: cancelSignal ? undefined : opts.timeout,
|
|
545
|
+
ignoreTlsErrors: this.#ignoreTlsErrors,
|
|
542
546
|
});
|
|
543
547
|
return response;
|
|
544
548
|
};
|
package/internals/utils.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { extname } from 'node:path';
|
|
2
2
|
import { Readable } from 'node:stream';
|
|
3
|
-
import { applySearchParams } from '@crawlee/utils';
|
|
3
|
+
import { applySearchParams, parseArgument } from '@crawlee/utils/internal';
|
|
4
4
|
import contentTypeParser from 'content-type';
|
|
5
5
|
import mime from 'mime-types';
|
|
6
|
-
import
|
|
6
|
+
import { z } from 'zod';
|
|
7
|
+
const responseWithUrlSchema = z.looseObject({ url: z.url(), headers: z.looseObject({}) });
|
|
7
8
|
/**
|
|
8
9
|
* Converts {@link HttpRequestOptions} to a {@link HttpRequest}.
|
|
9
10
|
*/
|
|
@@ -54,10 +55,7 @@ export function extractCharsetFromHtmlBytes(bytes) {
|
|
|
54
55
|
* @param response HTTP response object
|
|
55
56
|
*/
|
|
56
57
|
export function parseContentTypeFromResponse(response) {
|
|
57
|
-
|
|
58
|
-
url: ow.string.url,
|
|
59
|
-
headers: new ObjectPredicate(),
|
|
60
|
-
}));
|
|
58
|
+
parseArgument(response, responseWithUrlSchema);
|
|
61
59
|
const { url, headers } = response;
|
|
62
60
|
let parsedContentType;
|
|
63
61
|
if (headers.get('content-type')) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@crawlee/http",
|
|
3
|
-
"version": "4.0.0-
|
|
3
|
+
"version": "4.0.0-rc.0",
|
|
4
4
|
"description": "The scalable web crawling and scraping library for JavaScript/Node.js. Enables development of data extraction and web automation jobs (not only) with headless Chrome and Puppeteer.",
|
|
5
5
|
"engines": {
|
|
6
6
|
"node": ">=22.0.0"
|
|
@@ -49,19 +49,19 @@
|
|
|
49
49
|
"dependencies": {
|
|
50
50
|
"@apify/timeout": "^0.4.4",
|
|
51
51
|
"@apify/utilities": "^2.15.5",
|
|
52
|
-
"@crawlee/basic": "4.0.0-
|
|
53
|
-
"@crawlee/core": "4.0.0-
|
|
54
|
-
"@crawlee/http-client": "4.0.0-
|
|
55
|
-
"@crawlee/types": "4.0.0-
|
|
56
|
-
"@crawlee/utils": "4.0.0-
|
|
52
|
+
"@crawlee/basic": "4.0.0-rc.0",
|
|
53
|
+
"@crawlee/core": "4.0.0-rc.0",
|
|
54
|
+
"@crawlee/http-client": "4.0.0-rc.0",
|
|
55
|
+
"@crawlee/types": "4.0.0-rc.0",
|
|
56
|
+
"@crawlee/utils": "4.0.0-rc.0",
|
|
57
57
|
"@types/content-type": "^1.1.8",
|
|
58
58
|
"cheerio": "^1.0.0",
|
|
59
59
|
"content-type": "^1.0.5",
|
|
60
60
|
"iconv-lite": "^0.7.2",
|
|
61
61
|
"mime-types": "^3.0.1",
|
|
62
|
-
"ow": "^2.0.0",
|
|
63
62
|
"tslib": "^2.8.1",
|
|
64
|
-
"type-fest": "^4.41.0"
|
|
63
|
+
"type-fest": "^4.41.0",
|
|
64
|
+
"zod": "^4.4.3"
|
|
65
65
|
},
|
|
66
66
|
"lerna": {
|
|
67
67
|
"command": {
|
|
@@ -70,5 +70,5 @@
|
|
|
70
70
|
}
|
|
71
71
|
}
|
|
72
72
|
},
|
|
73
|
-
"gitHead": "
|
|
73
|
+
"gitHead": "79ab33dacdacb83e0197e6516d145f3aceef80c7"
|
|
74
74
|
}
|