@crawlee/http 4.0.0-beta.105 → 4.0.0-beta.106
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 +45 -44
- 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,8 +288,8 @@ 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)
|
|
@@ -347,7 +348,7 @@ export class HttpCrawler extends BasicCrawler {
|
|
|
347
348
|
async requestFunction({ request, session, proxyUrl }) {
|
|
348
349
|
const opts = this.getRequestOptions(request, session, proxyUrl);
|
|
349
350
|
try {
|
|
350
|
-
return await this.
|
|
351
|
+
return await this.requestAsBrowser(opts, session);
|
|
351
352
|
}
|
|
352
353
|
catch (e) {
|
|
353
354
|
if (e instanceof Error && e.constructor.name === 'TimeoutError') {
|
|
@@ -355,7 +356,7 @@ export class HttpCrawler extends BasicCrawler {
|
|
|
355
356
|
return new Response(); // this will never happen, as handleRequestTimeout always throws
|
|
356
357
|
}
|
|
357
358
|
if (this.isProxyError(e)) {
|
|
358
|
-
throw new SessionError(this.
|
|
359
|
+
throw new SessionError(this.getMessageFromError(e));
|
|
359
360
|
}
|
|
360
361
|
else {
|
|
361
362
|
throw e;
|
|
@@ -391,10 +392,10 @@ export class HttpCrawler extends BasicCrawler {
|
|
|
391
392
|
throw new Error(`${status} - Internal Server Error: ${body.slice(0, 100)}`);
|
|
392
393
|
}
|
|
393
394
|
else if (HTML_AND_XML_MIME_TYPES.includes(type)) {
|
|
394
|
-
if (!charset && !this
|
|
395
|
+
if (!charset && !this.#forceResponseEncoding) {
|
|
395
396
|
const rawBytes = Buffer.from(await response.arrayBuffer());
|
|
396
397
|
const metaCharset = extractCharsetFromHtmlBytes(rawBytes);
|
|
397
|
-
const charsetToUse = metaCharset ?? this
|
|
398
|
+
const charsetToUse = metaCharset ?? this.#suggestResponseEncoding ?? 'utf-8';
|
|
398
399
|
const body = iconv.encodingExists(charsetToUse)
|
|
399
400
|
? iconv.decode(rawBytes, charsetToUse)
|
|
400
401
|
: rawBytes.toString('utf8');
|
|
@@ -419,7 +420,7 @@ export class HttpCrawler extends BasicCrawler {
|
|
|
419
420
|
url: request.url,
|
|
420
421
|
method: request.method,
|
|
421
422
|
proxyUrl,
|
|
422
|
-
timeout: this
|
|
423
|
+
timeout: this.#navigationTimeoutMillis,
|
|
423
424
|
sessionToken: session,
|
|
424
425
|
headers: request.headers,
|
|
425
426
|
https: {
|
|
@@ -428,7 +429,7 @@ export class HttpCrawler extends BasicCrawler {
|
|
|
428
429
|
body: undefined,
|
|
429
430
|
};
|
|
430
431
|
if (requestOptions.headers?.cookie || requestOptions.headers?.Cookie) {
|
|
431
|
-
requestOptions.headers.Cookie = this.
|
|
432
|
+
requestOptions.headers.Cookie = this.getCookieHeaderFromRequest(request);
|
|
432
433
|
delete requestOptions.headers.cookie;
|
|
433
434
|
}
|
|
434
435
|
// Disable SSL verification for MITM proxies
|
|
@@ -443,11 +444,11 @@ export class HttpCrawler extends BasicCrawler {
|
|
|
443
444
|
return requestOptions;
|
|
444
445
|
}
|
|
445
446
|
encodeResponse(request, response, encoding) {
|
|
446
|
-
if (this
|
|
447
|
-
encoding = this
|
|
447
|
+
if (this.#forceResponseEncoding) {
|
|
448
|
+
encoding = this.#forceResponseEncoding;
|
|
448
449
|
}
|
|
449
|
-
else if (!encoding && this
|
|
450
|
-
encoding = this
|
|
450
|
+
else if (!encoding && this.#suggestResponseEncoding) {
|
|
451
|
+
encoding = this.#suggestResponseEncoding;
|
|
451
452
|
}
|
|
452
453
|
// Fall back to utf-8 if we still don't have encoding.
|
|
453
454
|
const utf8 = 'utf8';
|
|
@@ -481,12 +482,12 @@ export class HttpCrawler extends BasicCrawler {
|
|
|
481
482
|
extendSupportedMimeTypes(additionalMimeTypes) {
|
|
482
483
|
for (const mimeType of additionalMimeTypes) {
|
|
483
484
|
if (mimeType === '*/*') {
|
|
484
|
-
this
|
|
485
|
+
this.#supportedMimeTypes.add(mimeType);
|
|
485
486
|
continue;
|
|
486
487
|
}
|
|
487
488
|
try {
|
|
488
489
|
const parsedType = contentTypeParser.parse(mimeType);
|
|
489
|
-
this
|
|
490
|
+
this.#supportedMimeTypes.add(parsedType.type);
|
|
490
491
|
}
|
|
491
492
|
catch (err) {
|
|
492
493
|
throw new Error(`Can not parse mime type ${mimeType} from "options.additionalMimeTypes".`);
|
|
@@ -498,22 +499,22 @@ export class HttpCrawler extends BasicCrawler {
|
|
|
498
499
|
*/
|
|
499
500
|
handleRequestTimeout(session) {
|
|
500
501
|
session.markBad();
|
|
501
|
-
throw new Error(`Request timed out after ${this
|
|
502
|
+
throw new Error(`Request timed out after ${this.#navigationTimeoutMillis / 1000} seconds.`);
|
|
502
503
|
}
|
|
503
|
-
|
|
504
|
+
abortDownloadOfBody(request, response) {
|
|
504
505
|
const { status } = response;
|
|
505
506
|
const { type } = parseContentTypeFromResponse(response);
|
|
506
507
|
const isTransientContentType = status >= 500 || this.blockedStatusCodes.has(status);
|
|
507
|
-
if (!this
|
|
508
|
+
if (!this.#supportedMimeTypes.has(type) && !this.#supportedMimeTypes.has('*/*') && !isTransientContentType) {
|
|
508
509
|
request.noRetry = true;
|
|
509
510
|
throw new Error(`Resource ${request.url} served Content-Type ${type}, ` +
|
|
510
|
-
`but only ${Array.from(this
|
|
511
|
+
`but only ${Array.from(this.#supportedMimeTypes).join(', ')} are allowed. Skipping resource.`);
|
|
511
512
|
}
|
|
512
513
|
}
|
|
513
514
|
/**
|
|
514
515
|
* @internal wraps public utility for mocking purposes
|
|
515
516
|
*/
|
|
516
|
-
|
|
517
|
+
requestAsBrowser = async (options, session) => {
|
|
517
518
|
const opts = processHttpRequestOptions({
|
|
518
519
|
...options,
|
|
519
520
|
responseType: 'text',
|
|
@@ -521,7 +522,7 @@ export class HttpCrawler extends BasicCrawler {
|
|
|
521
522
|
// When saveResponseCookies is false, the response cookies must not mutate the
|
|
522
523
|
// session jar. Reads still go through the session (so session.setCookie() in pre-nav
|
|
523
524
|
// hooks keeps working) but a per-request clone is passed in so writes are discarded.
|
|
524
|
-
const cookieJar = this
|
|
525
|
+
const cookieJar = this.#saveResponseCookies ? session.cookieJar : await session.cookieJar.clone();
|
|
525
526
|
// Bind the request to the shared navigation window instead of a fixed per-request timeout, so
|
|
526
527
|
// `extendTimeout()` can push the deadline and a fixed `AbortSignal.timeout` won't fire on its own and
|
|
527
528
|
// 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.106",
|
|
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.106",
|
|
53
|
+
"@crawlee/core": "4.0.0-beta.106",
|
|
54
|
+
"@crawlee/http-client": "4.0.0-beta.106",
|
|
55
|
+
"@crawlee/types": "4.0.0-beta.106",
|
|
56
|
+
"@crawlee/utils": "4.0.0-beta.106",
|
|
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": "c622f1fc65e65221ea245817c58ecc0ffb4a5cb0"
|
|
74
74
|
}
|