@crawlee/http 4.0.0-beta.105 → 4.0.0-beta.107
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 +3 -9
- package/internals/http-crawler.js +48 -45
- package/package.json +7 -7
|
@@ -242,14 +242,8 @@ JSONData extends JsonValue = any> = RequestHandler<HttpCrawlingContext<UserData,
|
|
|
242
242
|
* @category Crawlers
|
|
243
243
|
*/
|
|
244
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']>>> extends BasicCrawler<Context, ContextExtension, ExtendedContext, Routes> {
|
|
245
|
-
private
|
|
246
|
-
private postNavigationHooks;
|
|
247
|
-
private saveResponseCookies;
|
|
248
|
-
private navigationTimeoutMillis;
|
|
245
|
+
#private;
|
|
249
246
|
private ignoreSslErrors;
|
|
250
|
-
private suggestResponseEncoding?;
|
|
251
|
-
private forceResponseEncoding?;
|
|
252
|
-
private readonly supportedMimeTypes;
|
|
253
247
|
protected static optionsShape: {
|
|
254
248
|
// @ts-ignore optional peer dependency or compatibility with es2022
|
|
255
249
|
navigationTimeoutSecs: import("ow").NumberPredicate & import("ow").BasePredicate<number | undefined>;
|
|
@@ -381,11 +375,11 @@ export declare class HttpCrawler<Context extends InternalHttpCrawlingContext<any
|
|
|
381
375
|
* Handles timeout request
|
|
382
376
|
*/
|
|
383
377
|
private handleRequestTimeout;
|
|
384
|
-
private
|
|
378
|
+
private abortDownloadOfBody;
|
|
385
379
|
/**
|
|
386
380
|
* @internal wraps public utility for mocking purposes
|
|
387
381
|
*/
|
|
388
|
-
private
|
|
382
|
+
private requestAsBrowser;
|
|
389
383
|
}
|
|
390
384
|
/**
|
|
391
385
|
* Creates new {@link Router} instance that works based on request labels.
|
|
@@ -109,14 +109,15 @@ 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;
|
|
112
|
+
#preNavigationHooks;
|
|
113
|
+
#postNavigationHooks;
|
|
114
|
+
#saveResponseCookies;
|
|
115
|
+
#navigationTimeoutMillis;
|
|
116
|
+
// kept as TS-private: tests read it at runtime
|
|
116
117
|
ignoreSslErrors;
|
|
117
|
-
suggestResponseEncoding;
|
|
118
|
-
forceResponseEncoding;
|
|
119
|
-
supportedMimeTypes;
|
|
118
|
+
#suggestResponseEncoding;
|
|
119
|
+
#forceResponseEncoding;
|
|
120
|
+
#supportedMimeTypes;
|
|
120
121
|
static optionsShape = {
|
|
121
122
|
...BasicCrawler.optionsShape,
|
|
122
123
|
navigationTimeoutSecs: ow.optional.number,
|
|
@@ -141,28 +142,28 @@ export class HttpCrawler extends BasicCrawler {
|
|
|
141
142
|
contextPipelineBuilder: contextPipelineBuilder ??
|
|
142
143
|
(() => this.buildContextPipeline()),
|
|
143
144
|
});
|
|
144
|
-
this
|
|
145
|
+
this.#supportedMimeTypes = new Set([...HTML_AND_XML_MIME_TYPES, APPLICATION_JSON_MIME_TYPE]);
|
|
145
146
|
if (additionalMimeTypes.length)
|
|
146
147
|
this.extendSupportedMimeTypes(additionalMimeTypes);
|
|
147
148
|
if (suggestResponseEncoding && forceResponseEncoding) {
|
|
148
149
|
this.log.warning('Both forceResponseEncoding and suggestResponseEncoding options are set. Using forceResponseEncoding.');
|
|
149
150
|
}
|
|
150
|
-
this
|
|
151
|
+
this.#navigationTimeoutMillis = navigationTimeoutSecs * 1000;
|
|
151
152
|
this.ignoreSslErrors = ignoreSslErrors;
|
|
152
|
-
this
|
|
153
|
-
this
|
|
153
|
+
this.#suggestResponseEncoding = suggestResponseEncoding;
|
|
154
|
+
this.#forceResponseEncoding = forceResponseEncoding;
|
|
154
155
|
// Cast away the extension-aware option types to the base internal storage types (see the field
|
|
155
156
|
// declarations above). This is sound - the hooks only ever receive the base context plus the
|
|
156
157
|
// members `extendContext` added at runtime.
|
|
157
|
-
this
|
|
158
|
-
this
|
|
159
|
-
({ request, response }) => this.
|
|
158
|
+
this.#preNavigationHooks = preNavigationHooks;
|
|
159
|
+
this.#postNavigationHooks = [
|
|
160
|
+
({ request, response }) => this.abortDownloadOfBody(request, response),
|
|
160
161
|
...postNavigationHooks,
|
|
161
162
|
];
|
|
162
|
-
this
|
|
163
|
+
this.#saveResponseCookies = saveResponseCookies;
|
|
163
164
|
}
|
|
164
165
|
getNavigationTimeoutMillis() {
|
|
165
|
-
return this
|
|
166
|
+
return this.#navigationTimeoutMillis;
|
|
166
167
|
}
|
|
167
168
|
/**
|
|
168
169
|
* Folds {@link HTTP_OPTIMIZED_CONCURRENCY_SYSTEM_OPTIONS} into the default system, keeping the user's
|
|
@@ -185,9 +186,9 @@ export class HttpCrawler extends BasicCrawler {
|
|
|
185
186
|
// A single navigation window covers the pre-navigation hooks, the navigation, and the post-navigation
|
|
186
187
|
// hooks: the whole phase shares one `navigationTimeoutSecs` budget, so a slow hook eats into the same
|
|
187
188
|
// window the navigation uses instead of each step being timed on its own.
|
|
188
|
-
const navigationTimedOut = `Navigation timed out after ${this
|
|
189
|
+
const navigationTimedOut = `Navigation timed out after ${this.#navigationTimeoutMillis / 1000} seconds.`;
|
|
189
190
|
const windowGuard = (step) => skipGuard(async (ctx) => {
|
|
190
|
-
const remaining = remainingNavigationWindowMillis(ctx, this
|
|
191
|
+
const remaining = remainingNavigationWindowMillis(ctx, this.#navigationTimeoutMillis);
|
|
191
192
|
if (remaining <= 0) {
|
|
192
193
|
throw new TimeoutError(navigationTimedOut);
|
|
193
194
|
}
|
|
@@ -196,11 +197,11 @@ export class HttpCrawler extends BasicCrawler {
|
|
|
196
197
|
let pipeline = ContextPipeline.create().compose({
|
|
197
198
|
action: this.prepareHttpRequest.bind(this),
|
|
198
199
|
});
|
|
199
|
-
for (const hook of this
|
|
200
|
+
for (const hook of this.#preNavigationHooks) {
|
|
200
201
|
pipeline = pipeline.compose(windowGuard(hook));
|
|
201
202
|
}
|
|
202
203
|
let pipelineWithNavigation = pipeline.compose(skipGuard(this.makeHttpRequest.bind(this)));
|
|
203
|
-
for (const hook of this
|
|
204
|
+
for (const hook of this.#postNavigationHooks) {
|
|
204
205
|
pipelineWithNavigation = pipelineWithNavigation.compose(windowGuard(hook));
|
|
205
206
|
}
|
|
206
207
|
return pipelineWithNavigation
|
|
@@ -234,7 +235,7 @@ export class HttpCrawler extends BasicCrawler {
|
|
|
234
235
|
// Bound the request by whatever is left of the shared navigation window (the pre-navigation hooks may
|
|
235
236
|
// have already spent part of it), so it produces a clean navigation-timeout error rather than the raw
|
|
236
237
|
// client abort.
|
|
237
|
-
const httpResponse = await addTimeoutToPromise(async () => this.requestFunction({ request, session, proxyUrl }), Math.max(1, remainingNavigationWindowMillis(crawlingContext, this
|
|
238
|
+
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
239
|
tryCancel();
|
|
239
240
|
request.loadedUrl = httpResponse?.url;
|
|
240
241
|
request.state = RequestState.AFTER_NAV;
|
|
@@ -264,11 +265,11 @@ export class HttpCrawler extends BasicCrawler {
|
|
|
264
265
|
// Reading the body is still part of the navigation, so it draws from the same shared window: on a server
|
|
265
266
|
// that streams the body slowly the request completes (headers arrive) but the body read would otherwise
|
|
266
267
|
// run unbounded. `extendTimeout` from a post-navigation hook has already pushed this deadline out if asked.
|
|
267
|
-
const remaining = remainingNavigationWindowMillis(crawlingContext, this
|
|
268
|
+
const remaining = remainingNavigationWindowMillis(crawlingContext, this.#navigationTimeoutMillis);
|
|
268
269
|
if (remaining <= 0) {
|
|
269
|
-
throw new TimeoutError(`Navigation timed out after ${this
|
|
270
|
+
throw new TimeoutError(`Navigation timed out after ${this.#navigationTimeoutMillis / 1000} seconds.`);
|
|
270
271
|
}
|
|
271
|
-
const parsed = await addTimeoutToPromise(async () => this.parseResponse(crawlingContext.request, crawlingContext.response), remaining, `Navigation timed out after ${this
|
|
272
|
+
const parsed = await addTimeoutToPromise(async () => this.parseResponse(crawlingContext.request, crawlingContext.response), remaining, `Navigation timed out after ${this.#navigationTimeoutMillis / 1000} seconds.`);
|
|
272
273
|
tryCancel();
|
|
273
274
|
const response = parsed.response;
|
|
274
275
|
const contentType = parsed.contentType;
|
|
@@ -287,14 +288,16 @@ export class HttpCrawler extends BasicCrawler {
|
|
|
287
288
|
}
|
|
288
289
|
return $;
|
|
289
290
|
};
|
|
290
|
-
this.
|
|
291
|
-
if (this
|
|
291
|
+
this.throwOnBlockedRequest(response.status);
|
|
292
|
+
if (this.#saveResponseCookies) {
|
|
292
293
|
try {
|
|
293
294
|
for (const cookie of getCookiesFromResponse(response)) {
|
|
294
295
|
if (!cookie)
|
|
295
296
|
continue;
|
|
296
297
|
try {
|
|
297
|
-
crawlingContext.session.cookieJar.
|
|
298
|
+
await crawlingContext.session.cookieJar.setCookie(cookie, response.url, {
|
|
299
|
+
ignoreError: false,
|
|
300
|
+
});
|
|
298
301
|
}
|
|
299
302
|
catch (e) {
|
|
300
303
|
this.log.debug(`Could not set cookie: ${e.message}`);
|
|
@@ -347,7 +350,7 @@ export class HttpCrawler extends BasicCrawler {
|
|
|
347
350
|
async requestFunction({ request, session, proxyUrl }) {
|
|
348
351
|
const opts = this.getRequestOptions(request, session, proxyUrl);
|
|
349
352
|
try {
|
|
350
|
-
return await this.
|
|
353
|
+
return await this.requestAsBrowser(opts, session);
|
|
351
354
|
}
|
|
352
355
|
catch (e) {
|
|
353
356
|
if (e instanceof Error && e.constructor.name === 'TimeoutError') {
|
|
@@ -355,7 +358,7 @@ export class HttpCrawler extends BasicCrawler {
|
|
|
355
358
|
return new Response(); // this will never happen, as handleRequestTimeout always throws
|
|
356
359
|
}
|
|
357
360
|
if (this.isProxyError(e)) {
|
|
358
|
-
throw new SessionError(this.
|
|
361
|
+
throw new SessionError(this.getMessageFromError(e));
|
|
359
362
|
}
|
|
360
363
|
else {
|
|
361
364
|
throw e;
|
|
@@ -391,10 +394,10 @@ export class HttpCrawler extends BasicCrawler {
|
|
|
391
394
|
throw new Error(`${status} - Internal Server Error: ${body.slice(0, 100)}`);
|
|
392
395
|
}
|
|
393
396
|
else if (HTML_AND_XML_MIME_TYPES.includes(type)) {
|
|
394
|
-
if (!charset && !this
|
|
397
|
+
if (!charset && !this.#forceResponseEncoding) {
|
|
395
398
|
const rawBytes = Buffer.from(await response.arrayBuffer());
|
|
396
399
|
const metaCharset = extractCharsetFromHtmlBytes(rawBytes);
|
|
397
|
-
const charsetToUse = metaCharset ?? this
|
|
400
|
+
const charsetToUse = metaCharset ?? this.#suggestResponseEncoding ?? 'utf-8';
|
|
398
401
|
const body = iconv.encodingExists(charsetToUse)
|
|
399
402
|
? iconv.decode(rawBytes, charsetToUse)
|
|
400
403
|
: rawBytes.toString('utf8');
|
|
@@ -419,7 +422,7 @@ export class HttpCrawler extends BasicCrawler {
|
|
|
419
422
|
url: request.url,
|
|
420
423
|
method: request.method,
|
|
421
424
|
proxyUrl,
|
|
422
|
-
timeout: this
|
|
425
|
+
timeout: this.#navigationTimeoutMillis,
|
|
423
426
|
sessionToken: session,
|
|
424
427
|
headers: request.headers,
|
|
425
428
|
https: {
|
|
@@ -428,7 +431,7 @@ export class HttpCrawler extends BasicCrawler {
|
|
|
428
431
|
body: undefined,
|
|
429
432
|
};
|
|
430
433
|
if (requestOptions.headers?.cookie || requestOptions.headers?.Cookie) {
|
|
431
|
-
requestOptions.headers.Cookie = this.
|
|
434
|
+
requestOptions.headers.Cookie = this.getCookieHeaderFromRequest(request);
|
|
432
435
|
delete requestOptions.headers.cookie;
|
|
433
436
|
}
|
|
434
437
|
// Disable SSL verification for MITM proxies
|
|
@@ -443,11 +446,11 @@ export class HttpCrawler extends BasicCrawler {
|
|
|
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
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@crawlee/http",
|
|
3
|
-
"version": "4.0.0-beta.
|
|
3
|
+
"version": "4.0.0-beta.107",
|
|
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,11 +49,11 @@
|
|
|
49
49
|
"dependencies": {
|
|
50
50
|
"@apify/timeout": "^0.4.4",
|
|
51
51
|
"@apify/utilities": "^2.15.5",
|
|
52
|
-
"@crawlee/basic": "4.0.0-beta.
|
|
53
|
-
"@crawlee/core": "4.0.0-beta.
|
|
54
|
-
"@crawlee/http-client": "4.0.0-beta.
|
|
55
|
-
"@crawlee/types": "4.0.0-beta.
|
|
56
|
-
"@crawlee/utils": "4.0.0-beta.
|
|
52
|
+
"@crawlee/basic": "4.0.0-beta.107",
|
|
53
|
+
"@crawlee/core": "4.0.0-beta.107",
|
|
54
|
+
"@crawlee/http-client": "4.0.0-beta.107",
|
|
55
|
+
"@crawlee/types": "4.0.0-beta.107",
|
|
56
|
+
"@crawlee/utils": "4.0.0-beta.107",
|
|
57
57
|
"@types/content-type": "^1.1.8",
|
|
58
58
|
"cheerio": "^1.0.0",
|
|
59
59
|
"content-type": "^1.0.5",
|
|
@@ -70,5 +70,5 @@
|
|
|
70
70
|
}
|
|
71
71
|
}
|
|
72
72
|
},
|
|
73
|
-
"gitHead": "
|
|
73
|
+
"gitHead": "5a3cb6242d0ae261df93c5e03cfebbb9b736e6b7"
|
|
74
74
|
}
|