Skip to content

Commit a7a6eb7

Browse files
authored
feat: fetch API for custom http clients (#3326)
Removes `HttpClient.stream()`, leaving only `HttpClient.sendRequest()` (they were both returning a streamable `Response` since #3295). Extracts cookie- and redirect-related behaviour to a separate abstract `BaseHttpClient` class. Custom HTTP clients now only have to implement one `fetch` method (matches the native `fetch` API). This makes the custom HTTP client implementation easier and will hopefully drive community contributions. Closes #3071 Closes #3314 Blocked by apify/impit#348
1 parent 72e82cd commit a7a6eb7

51 files changed

Lines changed: 150907 additions & 150068 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

docs/examples/skip-navigation.ts

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,15 @@ const crawler = new PlaywrightCrawler({
88
// The request should have the navigation skipped
99
if (request.skipNavigation) {
1010
// Request the image and get its buffer back
11-
const imageResponse = await sendRequest({ responseType: 'buffer' });
12-
13-
// Save the image in the key-value store
14-
await imageStore.setValue(`${request.userData.key}.png`, imageResponse.body);
11+
const imageResponse = await sendRequest();
12+
13+
// Saves the image in the key-value store.
14+
//
15+
// Note: For large-scale file downloads, consider using FileDownload crawler:
16+
// https://crawlee.dev/js/api/http-crawler/class/FileDownload
17+
await imageStore.setValue(`${request.userData.key}.svg`, await imageResponse.bytes(), {
18+
contentType: 'image/svg+xml',
19+
});
1520

1621
// Prevent executing the rest of the code as we do not need it
1722
return;

docs/guides/custom-http-client/custom-http-client.mdx

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,14 +10,34 @@ import CodeBlock from '@theme/CodeBlock';
1010
import ImplementationSource from '!!raw-loader!./implementation.ts';
1111
import UsageSource from '!!raw-loader!./usage.ts';
1212

13-
The <ApiLink to="basic-crawler/class/BasicCrawler">`BasicCrawler`</ApiLink> class allows you to configure the HTTP client implementation using the `httpClient` constructor option. This might be useful for testing or if you need to swap out the default implementation based on `got-scraping` for something else, such as `curl-impersonate` or `axios`.
13+
The <ApiLink to="basic-crawler/class/BasicCrawler">`BasicCrawler`</ApiLink> class allows you to configure the HTTP client implementation using the `httpClient` constructor option. This might be useful for testing or if you need to swap out the default implementation based on `got-scraping` for something else, such as `curl-impersonate`.
1414

15-
The HTTP client implementation needs to conform to the <ApiLink to="types/interface/BaseHttpClient">`BaseHttpClient`</ApiLink> interface. For a rough idea on how it might look, see a skeleton implementation that uses the standard `fetch` interface:
15+
## Built-in HTTP clients
16+
17+
Crawlee provides several HTTP client implementations out of the box:
18+
19+
- **`GotScrapingHttpClient`** (default) - Uses the `got-scraping` library for browser-like requests with support for custom headers, browser fingerprints, and proxies.
20+
- **`ImpitHttpClient`** - Uses the `impit` library for making requests that closely mimic browser behavior.
21+
- **`FetchHttpClient`** - Simple implementation using the native `fetch` API (does not support proxies).
22+
23+
## Implementing a custom HTTP client
24+
25+
To create a custom HTTP client, extend the <ApiLink to="http-client/class/BaseHttpClient">`BaseHttpClient`</ApiLink> abstract class from `@crawlee/http-client`. The base class handles common functionality like cookie management, redirect following, session integration, proxy support, and timeout handling.
26+
27+
Your custom implementation only needs to override the `fetch` method to perform the actual network request:
1628

1729
<CodeBlock language="ts">{ImplementationSource}</CodeBlock>
1830

31+
By extending `BaseHttpClient`, your implementation automatically gets:
32+
- Cookie jar management (applying cookies before requests, saving cookies from responses)
33+
- Automatic redirect following (up to 10 redirects)
34+
- Session integration (proxy URL and cookies from session)
35+
- Timeout handling via AbortSignal
36+
- Proxy URL support
37+
1938
You may then instantiate it and pass to a crawler constructor:
2039

2140
<CodeBlock language="ts">{UsageSource}</CodeBlock>
2241

23-
Please note that the interface is experimental and it will likely change with Crawlee version 4.
42+
Alternatively, you can implement the <ApiLink to="types/interface/BaseHttpClient">`BaseHttpClient`</ApiLink> interface directly if you need full control over all aspects of the HTTP request handling, including cookies, redirects, and sessions. However, this approach requires implementing significantly more logic yourself.
43+
Lines changed: 11 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,14 @@
1-
import type { BaseHttpClient, SendRequestOptions, StreamOptions } from '@crawlee/types';
1+
import { BaseHttpClient, type CustomFetchOptions } from '@crawlee/http-client';
22

3-
export class FetchHttpClient implements BaseHttpClient {
4-
async sendRequest(request: Request, options?: SendRequestOptions): Promise<Response> {
5-
const signal = options?.timeout ? AbortSignal.timeout(options.timeout ?? 0) : undefined;
6-
return fetch(request, {
7-
signal,
8-
});
9-
}
10-
11-
async stream(request: Request, options: StreamOptions): Promise<Response> {
12-
const signal = options?.timeout ? AbortSignal.timeout(options.timeout ?? 0) : undefined;
13-
return fetch(request, {
14-
signal,
15-
});
3+
/**
4+
* A simple HTTP client implementation using the native `fetch` API.
5+
*
6+
* Custom implementations only need to override the `fetch` method.
7+
*/
8+
export class CustomFetchClient extends BaseHttpClient {
9+
protected override async fetch(request: Request, options?: RequestInit & CustomFetchOptions): Promise<Response> {
10+
// The base class handles cookies, redirects, sessions, and timeouts.
11+
// We only need to perform the actual network request here.
12+
return fetch(request, options);
1613
}
1714
}

docs/guides/custom-http-client/usage.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
import { HttpCrawler } from 'crawlee';
2-
import { FetchHttpClient } from './implementation.js';
2+
import { CustomFetchClient } from './implementation.js';
33

44
const crawler = new HttpCrawler({
5-
httpClient: new FetchHttpClient(),
5+
httpClient: new CustomFetchClient(),
66
async requestHandler() {
77
/* ... */
88
},

packages/basic-crawler/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,11 +43,11 @@
4343
"@apify/timeout": "^0.3.2",
4444
"@apify/utilities": "^2.15.5",
4545
"@crawlee/core": "4.0.0",
46+
"@crawlee/got-scraping-client": "4.0.0",
4647
"@crawlee/types": "4.0.0",
4748
"@crawlee/utils": "4.0.0",
4849
"csv-stringify": "^6.5.2",
4950
"fs-extra": "^11.3.0",
50-
"got-scraping": "^4.1.1",
5151
"ow": "^2.0.0",
5252
"tldts": "^7.0.6",
5353
"tslib": "^2.8.1",

packages/basic-crawler/src/internals/basic-crawler.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,6 @@ import {
3838
enqueueLinks,
3939
EnqueueStrategy,
4040
EventType,
41-
GotScrapingHttpClient,
4241
KeyValueStore,
4342
mergeCookies,
4443
NonRetryableError,
@@ -57,6 +56,7 @@ import {
5756
Statistics,
5857
validators,
5958
} from '@crawlee/core';
59+
import { GotScrapingHttpClient } from '@crawlee/got-scraping-client';
6060
import type {
6161
Awaitable,
6262
BaseHttpClient,
@@ -1631,7 +1631,7 @@ export class BasicCrawler<
16311631
},
16321632
pushData: this.pushData.bind(this),
16331633
useState: this.useState.bind(this),
1634-
sendRequest: createSendRequest(this.httpClient, request!, session) as CrawlingContext['sendRequest'],
1634+
sendRequest: createSendRequest(this.httpClient, request!, session),
16351635
getKeyValueStore: async (idOrName?: string) => KeyValueStore.open(idOrName, { config: this.config }),
16361636
registerDeferredCleanup: (cleanup) => {
16371637
deferredCleanup.push(cleanup);

packages/basic-crawler/src/internals/send-request.ts

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,6 @@ export function createSendRequest(
1818
overrideRequest: Partial<HttpRequestOptions> = {},
1919
overrideOptions: SendRequestOptions = {},
2020
): Promise<Response> => {
21-
const sessionCookieJar = {
22-
getCookieString: async (url: string) => session?.getCookieString(url),
23-
setCookie: async (rawCookie: string, url: string) => session?.setCookie(rawCookie, url),
24-
};
25-
2621
const baseRequest = originRequest.intoFetchAPIRequest();
2722
const mergedUrl = overrideRequest.url ?? baseRequest.url;
2823
const mergedMethod = overrideRequest.method ?? baseRequest.method;
@@ -42,7 +37,7 @@ export function createSendRequest(
4237

4338
return httpClient.sendRequest(request, {
4439
session,
45-
cookieJar: overrideOptions?.cookieJar ?? (sessionCookieJar as any),
40+
cookieJar: overrideOptions?.cookieJar ?? session?.cookieJar,
4641
timeout: overrideOptions.timeout,
4742
});
4843
};

packages/core/package.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,6 @@
6060
"@vladfrangu/async_event_emitter": "^2.4.6",
6161
"csv-stringify": "^6.5.2",
6262
"fs-extra": "^11.3.0",
63-
"got-scraping": "^4.1.1",
6463
"json5": "^2.2.3",
6564
"minimatch": "^10.0.1",
6665
"ow": "^2.0.0",

packages/core/src/crawlers/crawler_commons.ts

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
1-
import type { Dictionary, ProxyInfo } from '@crawlee/types';
2-
import type { OptionsInit } from 'got-scraping';
1+
import type { Dictionary, HttpRequestOptions, ProxyInfo, SendRequestOptions } from '@crawlee/types';
32
import type { ReadonlyDeep, SetRequired } from 'type-fest';
43

54
import type { Configuration } from '../configuration.js';
@@ -139,8 +138,7 @@ export interface CrawlingContext<UserData extends Dictionary = Dictionary> exten
139138
): Promise<unknown>;
140139

141140
/**
142-
* Fires HTTP request via [`got-scraping`](https://crawlee.dev/js/docs/guides/got-scraping), allowing to override the request
143-
* options on the fly.
141+
* Fires HTTP request via the internal HTTP client, allowing to override the request options on the fly.
144142
*
145143
* This is handy when you work with a browser crawler but want to execute some requests outside it (e.g. API requests).
146144
* Check the [Skipping navigations for certain requests](https://crawlee.dev/js/docs/examples/skip-navigation) example for
@@ -155,7 +153,10 @@ export interface CrawlingContext<UserData extends Dictionary = Dictionary> exten
155153
* },
156154
* ```
157155
*/
158-
sendRequest(overrideOptions?: Partial<OptionsInit>): Promise<Response>;
156+
sendRequest: (
157+
requestOverrides?: Partial<HttpRequestOptions>,
158+
optionsOverrides?: SendRequestOptions,
159+
) => Promise<Response>;
159160

160161
/**
161162
* Register a function to be called at the very end of the request handling process. This is useful for resources that should be accessible to error handlers, for instance.

packages/core/src/http_clients/base-http-client.ts

Lines changed: 0 additions & 64 deletions
This file was deleted.

0 commit comments

Comments
 (0)