@crawlee/http-client 4.0.0-beta.99 → 4.0.0-rc.1

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/README.md CHANGED
@@ -34,7 +34,7 @@ Crawlee is available as the [`crawlee`](https://www.npmjs.com/package/crawlee) N
34
34
 
35
35
  We recommend visiting the [Introduction tutorial](https://crawlee.dev/js/docs/introduction) in Crawlee documentation for more information.
36
36
 
37
- > Crawlee requires **Node.js 16 or higher**.
37
+ > Crawlee requires **Node.js 22.13 or higher**.
38
38
 
39
39
  ### With Crawlee CLI
40
40
 
@@ -1,5 +1,4 @@
1
- import type { BaseHttpClient as BaseHttpClientInterface, CrawleeLogger, SendRequestOptions, SessionFingerprint } from '@crawlee/types';
2
- import { CookieJar } from 'tough-cookie';
1
+ import type { BaseHttpClient as BaseHttpClientInterface, CookieJar, CrawleeLogger, SendRequestOptions, SessionFingerprint } from '@crawlee/types';
3
2
  /**
4
3
  * Per-request options handed to a concrete client's `fetch` implementation.
5
4
  */
@@ -25,6 +24,14 @@ export interface CustomFetchOptions {
25
24
  * rest on a best-effort basis. Sourced from `SendRequestOptions.session.fingerprint`.
26
25
  */
27
26
  fingerprint?: SessionFingerprint;
27
+ /**
28
+ * When `true`, TLS certificate errors should be ignored for this request.
29
+ * Set when `SendRequestOptions.ignoreTlsErrors` is passed (e.g. from the
30
+ * `ignoreTlsErrors` crawler option) or when the session's proxy is a MITM
31
+ * proxy (`session.proxyInfo.ignoreTlsErrors`). Best-effort: clients that
32
+ * cannot disable TLS verification ignore it.
33
+ */
34
+ ignoreTlsErrors?: boolean;
28
35
  }
29
36
  /**
30
37
  * Base HTTP client that provides fetch-like `sendRequest` with Crawlee-managed
@@ -32,7 +39,7 @@ export interface CustomFetchOptions {
32
39
  * implement only the low-level network call in `fetch`.
33
40
  */
34
41
  export declare abstract class BaseHttpClient implements BaseHttpClientInterface {
35
- private log?;
42
+ #private;
36
43
  constructor(options?: {
37
44
  logger?: CrawleeLogger;
38
45
  });
@@ -41,12 +48,6 @@ export declare abstract class BaseHttpClient implements BaseHttpClientInterface
41
48
  * automatic redirect following or special error handling.
42
49
  */
43
50
  protected abstract fetch(input: Request, init?: RequestInit & CustomFetchOptions): Promise<Response>;
44
- private applyCookies;
45
- private setCookies;
46
- private resolveRequestContext;
47
- private createAbortSignal;
48
- private isRedirect;
49
- private buildRedirectRequest;
50
51
  /**
51
52
  * Public fetch-like method that handles redirects and uses provided proxy and cookie jar.
52
53
  */
@@ -1,15 +1,14 @@
1
- import { CookieJar } from 'tough-cookie';
2
1
  /**
3
2
  * Base HTTP client that provides fetch-like `sendRequest` with Crawlee-managed
4
3
  * behaviors (redirect handling, proxy and cookie handling). Concrete clients
5
4
  * implement only the low-level network call in `fetch`.
6
5
  */
7
6
  export class BaseHttpClient {
8
- log;
7
+ #log;
9
8
  constructor(options) {
10
- this.log = options?.logger;
9
+ this.#log = options?.logger;
11
10
  }
12
- async applyCookies(request, cookieJar) {
11
+ async #applyCookies(request, cookieJar) {
13
12
  try {
14
13
  const requestCookies = request.headers.get('cookie') ?? '';
15
14
  if (!requestCookies) {
@@ -27,39 +26,44 @@ export class BaseHttpClient {
27
26
  .split(/; */)
28
27
  .filter(Boolean)
29
28
  .map((pair) => merged.setCookie(pair, request.url)));
30
- const cookieString = merged.getCookieStringSync(request.url);
29
+ const cookieString = await merged.getCookieString(request.url);
31
30
  if (cookieString) {
32
31
  request.headers.set('cookie', cookieString);
33
32
  }
34
33
  }
35
34
  catch (e) {
36
- this.log?.warning(`Failed to get cookies for URL "${request.url}": ${e.message}`);
35
+ this.#log?.warning(`Failed to get cookies for URL "${request.url}": ${e.message}`);
37
36
  }
38
37
  return request;
39
38
  }
40
- async setCookies(response, cookieJar) {
39
+ async #setCookies(response, cookieJar) {
41
40
  const setCookieHeaders = response.headers.getSetCookie();
42
41
  for (const header of setCookieHeaders) {
43
42
  try {
44
43
  await cookieJar.setCookie(header, response.url);
45
44
  }
46
45
  catch (e) {
47
- this.log?.warning(`Failed to set cookie for URL "${response.url}": ${e.message}`);
46
+ this.#log?.warning(`Failed to set cookie for URL "${response.url}": ${e.message}`);
48
47
  }
49
48
  }
50
49
  }
51
- resolveRequestContext(options) {
50
+ async #resolveRequestContext(options) {
52
51
  const proxyUrl = options?.proxyUrl ?? options?.session?.proxyInfo?.url;
53
- const cookieJar = options?.cookieJar ?? options?.session?.cookieJar ?? new CookieJar();
54
- const signal = this.createAbortSignal(options?.signal, options?.timeoutMillis);
52
+ const cookieJar = options?.cookieJar ?? options?.session?.cookieJar ?? (await this.#createDefaultCookieJar());
53
+ const signal = this.#createAbortSignal(options?.signal, options?.timeoutMillis);
55
54
  return {
56
55
  proxyUrl,
57
- cookieJar: cookieJar,
56
+ cookieJar,
58
57
  signal,
59
58
  fingerprint: options?.session?.fingerprint,
59
+ ignoreTlsErrors: options?.ignoreTlsErrors || options?.session?.proxyInfo?.ignoreTlsErrors,
60
60
  };
61
61
  }
62
- createAbortSignal(signal, timeoutMillis) {
62
+ async #createDefaultCookieJar() {
63
+ const { CookieJar: ToughCookieJar } = await import('tough-cookie');
64
+ return new ToughCookieJar();
65
+ }
66
+ #createAbortSignal(signal, timeoutMillis) {
63
67
  if (signal && timeoutMillis) {
64
68
  return AbortSignal.any([signal, AbortSignal.timeout(timeoutMillis)]);
65
69
  }
@@ -68,11 +72,11 @@ export class BaseHttpClient {
68
72
  }
69
73
  return timeoutMillis ? AbortSignal.timeout(timeoutMillis) : undefined;
70
74
  }
71
- isRedirect(response) {
75
+ #isRedirect(response) {
72
76
  const status = response.status;
73
77
  return status >= 300 && status < 400 && !!response.headers.get('location');
74
78
  }
75
- buildRedirectRequest(currentRequest, response, initialRequest) {
79
+ #buildRedirectRequest(currentRequest, response, initialRequest) {
76
80
  const location = response.headers.get('location');
77
81
  const nextUrl = new URL(location, response.url ?? currentRequest.url);
78
82
  const prevMethod = (currentRequest.method ?? 'GET').toUpperCase();
@@ -89,12 +93,20 @@ export class BaseHttpClient {
89
93
  }
90
94
  const nextHeaders = new Headers();
91
95
  currentRequest.headers.forEach((value, key) => nextHeaders.set(key, value));
96
+ // Like native `fetch`, do not forward credentials to a different origin
97
+ if (nextUrl.origin !== new URL(currentRequest.url).origin) {
98
+ for (const header of ['authorization', 'proxy-authorization', 'cookie']) {
99
+ nextHeaders.delete(header);
100
+ }
101
+ }
92
102
  return new Request(nextUrl.toString(), {
93
103
  method: nextMethod,
94
104
  headers: nextHeaders,
95
105
  body: nextBody,
96
106
  credentials: currentRequest.credentials,
97
107
  redirect: 'manual',
108
+ // Node-specific option required for a stream body, which the replayed body always is
109
+ duplex: 'half',
98
110
  });
99
111
  }
100
112
  /**
@@ -104,23 +116,24 @@ export class BaseHttpClient {
104
116
  const maxRedirects = 10;
105
117
  let currentRequest = initialRequest;
106
118
  let redirectCount = 0;
107
- const { proxyUrl, cookieJar, signal, fingerprint } = this.resolveRequestContext(options);
119
+ const { proxyUrl, cookieJar, signal, fingerprint, ignoreTlsErrors } = await this.#resolveRequestContext(options);
108
120
  currentRequest = initialRequest.clone();
109
121
  while (true) {
110
- await this.applyCookies(currentRequest, cookieJar);
111
- const response = await this.fetch(currentRequest, {
122
+ // Like `fetch`, set the jar cookies on a copy, so that the next redirect hop does not inherit them
123
+ const response = await this.fetch(await this.#applyCookies(new Request(currentRequest), cookieJar), {
112
124
  signal,
113
125
  proxyUrl,
114
126
  cookieJar,
115
127
  fingerprint,
128
+ ignoreTlsErrors,
116
129
  redirect: 'manual',
117
130
  });
118
- await this.setCookies(response, cookieJar);
119
- if (this.isRedirect(response)) {
131
+ await this.#setCookies(response, cookieJar);
132
+ if (this.#isRedirect(response)) {
120
133
  if (redirectCount++ >= maxRedirects) {
121
134
  throw new Error(`Too many redirects (${maxRedirects}) while requesting ${currentRequest.url}`);
122
135
  }
123
- currentRequest = this.buildRedirectRequest(currentRequest, response, initialRequest);
136
+ currentRequest = this.#buildRedirectRequest(currentRequest, response, initialRequest);
124
137
  continue;
125
138
  }
126
139
  return response;
@@ -1,3 +1,4 @@
1
+ import type { CrawleeLogger } from '@crawlee/types';
1
2
  import { BaseHttpClient, type CustomFetchOptions } from './base-http-client.js';
2
3
  /**
3
4
  * A HTTP client implementation using the native `fetch` API.
@@ -5,5 +6,9 @@ import { BaseHttpClient, type CustomFetchOptions } from './base-http-client.js';
5
6
  * This implementation does not support proxying.
6
7
  */
7
8
  export declare class FetchHttpClient extends BaseHttpClient {
8
- fetch(request: Request, options?: RequestInit & CustomFetchOptions): Promise<Response>;
9
+ #private;
10
+ constructor(options?: {
11
+ logger?: CrawleeLogger;
12
+ });
13
+ protected fetch(request: Request, options?: RequestInit & CustomFetchOptions): Promise<Response>;
9
14
  }
@@ -5,7 +5,16 @@ import { BaseHttpClient } from './base-http-client.js';
5
5
  * This implementation does not support proxying.
6
6
  */
7
7
  export class FetchHttpClient extends BaseHttpClient {
8
+ #logger;
9
+ constructor(options) {
10
+ super(options);
11
+ this.#logger = options?.logger;
12
+ }
8
13
  async fetch(request, options) {
14
+ if (options?.ignoreTlsErrors) {
15
+ this.#logger?.warningOnce('FetchHttpClient cannot disable TLS certificate verification, the `ignoreTlsErrors` option is ignored. ' +
16
+ 'Install the optional @crawlee/impit-client dependency to make it work.');
17
+ }
9
18
  return fetch(request, options);
10
19
  }
11
20
  }
package/index.d.ts CHANGED
@@ -1,3 +1,3 @@
1
1
  export { BaseHttpClient, type CustomFetchOptions } from './base-http-client.js';
2
- export { ResponseWithUrl, type IResponseWithUrl } from './response.js';
2
+ export { ResponseWithUrl } from './response.js';
3
3
  export { FetchHttpClient } from './fetch-http-client.js';
package/package.json CHANGED
@@ -1,9 +1,9 @@
1
1
  {
2
2
  "name": "@crawlee/http-client",
3
- "version": "4.0.0-beta.99",
3
+ "version": "4.0.0-rc.1",
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
- "node": ">=22.0.0"
6
+ "node": ">=22.13.0"
7
7
  },
8
8
  "type": "module",
9
9
  "exports": {
@@ -47,7 +47,7 @@
47
47
  "access": "public"
48
48
  },
49
49
  "dependencies": {
50
- "@crawlee/types": "4.0.0-beta.99",
50
+ "@crawlee/types": "4.0.0-rc.1",
51
51
  "tough-cookie": "^6.0.0"
52
52
  },
53
53
  "lerna": {
@@ -57,5 +57,5 @@
57
57
  }
58
58
  }
59
59
  },
60
- "gitHead": "ad2748380941842bb10cff100f4b4caad92049e3"
60
+ "gitHead": "f354ca5e943bed1a5c657a1fce1974c053f9fff0"
61
61
  }
package/response.d.ts CHANGED
@@ -1,13 +1,10 @@
1
- export interface IResponseWithUrl extends Response {
2
- url: string;
3
- }
4
1
  /**
5
2
  * A Response class that includes the original request URL.
6
3
  *
7
4
  * This class extends `Response` from `fetch` API and is fully compatible with this.
8
5
  */
9
- export declare class ResponseWithUrl extends Response implements IResponseWithUrl {
10
- url: string;
6
+ export declare class ResponseWithUrl extends Response {
7
+ readonly url: string;
11
8
  constructor(body: BodyInit | null, init: ResponseInit & {
12
9
  url?: string;
13
10
  });