From 6aff08e529223d728b6fc49459aadf2bb9be7274 Mon Sep 17 00:00:00 2001 From: Pooya Parsa Date: Wed, 28 Aug 2024 12:30:16 +0200 Subject: [PATCH] types: add `agent` and `dispatcher` options (node-specific) (#308) --- README.md | 104 +++++++++++++++++++++++++++++++-------------- build.config.ts | 1 + examples/proxy.mjs | 9 ++++ package.json | 1 + pnpm-lock.yaml | 17 ++++++++ src/types.ts | 14 +++++- 6 files changed, 112 insertions(+), 34 deletions(-) create mode 100644 examples/proxy.mjs diff --git a/README.md b/README.md index aac7861a..720a8145 100644 --- a/README.md +++ b/README.md @@ -41,12 +41,6 @@ const { ofetch } = require("ofetch"); We use [conditional exports](https://nodejs.org/api/packages.html#packages_conditional_exports) to detect Node.js and automatically use [unjs/node-fetch-native](https://github.com/unjs/node-fetch-native). If `globalThis.fetch` is available, will be used instead. To leverage Node.js 17.5.0 experimental native fetch API use [`--experimental-fetch` flag](https://nodejs.org/dist/latest-v17.x/docs/api/cli.html#--experimental-fetch). -### `keepAlive` support - -By setting the `FETCH_KEEP_ALIVE` environment variable to `true`, an HTTP/HTTPS agent will be registered that keeps sockets around even when there are no outstanding requests, so they can be used for future requests without having to re-establish a TCP connection. - -**Note:** This option can potentially introduce memory leaks. Please check [node-fetch/node-fetch#1325](https://github.com/node-fetch/node-fetch/pull/1325). - ## ✔️ Parsing Response `ofetch` will smartly parse JSON and native values using [destr](https://github.com/unjs/destr), falling back to the text if it fails to parse. @@ -285,55 +279,99 @@ await ofetch("/movies", { }); ``` -## 💡 Adding a HTTP(S) / Proxy Agent +## 🍣 Access to Raw Response + +If you need to access raw response (for headers, etc), can use `ofetch.raw`: + +```js +const response = await ofetch.raw("/sushi"); -If you need use a HTTP(S) / Proxy Agent, you can (for Node.js only): +// response._data +// response.headers +// ... +``` -### Node >= v18 +## 🌿 Using Native Fetch -Add `ProxyAgent` to `dispatcher` option with `undici` +As a shortcut, you can use `ofetch.native` that provides native `fetch` API ```js -import { ofetch } from 'ofetch' -import { ProxyAgent } from 'undici' +const json = await ofetch.native("/sushi").then((r) => r.json()); +``` -await ofetch("/api", { - dispatcher: new ProxyAgent("http://example.com"), -}); +## 🕵️ Adding HTTP(S) Agent + +In Node.js (>= 18) environments, you can provide a custom dispatcher to intercept requests and support features such as Proxy and self-signed certificates. This feature is enabled by [undici](https://undici.nodejs.org/) built-in Node.js. [read more](https://undici.nodejs.org/#/docs/api/Dispatcher) about the Dispatcher API. + +Some available agents: + +- `ProxyAgent`: A Proxy Agent class that implements the Agent API. It allows the connection through a proxy in a simple way. ([docs](https://undici.nodejs.org/#/docs/api/ProxyAgent)) +- `MockAgent`: A mocked Agent class that implements the Agent API. It allows one to intercept HTTP requests made through undici and return mocked responses instead. ([docs](https://undici.nodejs.org/#/docs/api/MockAgent)) +- `Agent`: Agent allows dispatching requests against multiple different origins. ([docs](https://undici.nodejs.org/#/docs/api/Agent)) + +**Example:** Set a proxy agent for one request: + +```ts +import { ProxyAgent } from "undici"; +import { ofetch } from "ofetch"; + +const proxyAgent = new ProxyAgent("http://localhost:3128"); +const data = await ofetch("https://icanhazip.com", { dispatcher: proxyAgent }); ``` -### Node < v18 +**Example:** Create a custom fetch instance that has proxy enabled: -Add `HttpsProxyAgent` to `agent` option with `https-proxy-agent` +```ts +import { ProxyAgent, setGlobalDispatcher } from "undici"; +import { ofetch } from "ofetch"; -```js -import { HttpsProxyAgent } from "https-proxy-agent"; +const proxyAgent = new ProxyAgent("http://localhost:3128"); +const fetchWithProxy = ofetch.create({ dispatcher: proxyAgent }); -await ofetch("/api", { - agent: new HttpsProxyAgent("http://example.com"), -}); +const data = await fetchWithProxy("https://icanhazip.com"); ``` -## 🍣 Access to Raw Response +**Example:** Set a proxy agent for all requests: -If you need to access raw response (for headers, etc), can use `ofetch.raw`: +```ts +import { ProxyAgent, setGlobalDispatcher } from "undici"; +import { ofetch } from "ofetch"; -```js -const response = await ofetch.raw("/sushi"); +const proxyAgent = new ProxyAgent("http://localhost:3128"); +setGlobalDispatcher(proxyAgent); -// response._data -// response.headers -// ... +const data = await ofetch("https://icanhazip.com"); ``` -## Native fetch +**Example:** Allow self-signed certificates (USE AT YOUR OWN RISK!) -As a shortcut, you can use `ofetch.native` that provides native `fetch` API +```ts +import { Agent } from "undici"; +import { ofetch } from "ofetch"; -```js -const json = await ofetch.native("/sushi").then((r) => r.json()); +// Note: This makes fetch unsecure against MITM attacks. USE AT YOUW OWN RISK! +const unsecureAgent = new Agent({ connect: { rejectUnauthorized: false } }); +const unsecureFetch = ofetch.create({ dispatcher: unsecureAgent }); + +const data = await unsecureFetch("https://www.squid-cache.org/"); +``` + +On older Node.js version (<18), you might also use use `agent`: + +```ts +import { HttpsProxyAgent } from "https-proxy-agent"; + +await ofetch("/api", { + agent: new HttpsProxyAgent("http://example.com"), +}); ``` +### `keepAlive` support (only works for Node < 18) + +By setting the `FETCH_KEEP_ALIVE` environment variable to `true`, an HTTP/HTTPS agent will be registered that keeps sockets around even when there are no outstanding requests, so they can be used for future requests without having to re-establish a TCP connection. + +**Note:** This option can potentially introduce memory leaks. Please check [node-fetch/node-fetch#1325](https://github.com/node-fetch/node-fetch/pull/1325). + ## 📦 Bundler Notes - All targets are exported with Module and CommonJS format and named exports diff --git a/build.config.ts b/build.config.ts index 1c66ad05..dd6ce990 100644 --- a/build.config.ts +++ b/build.config.ts @@ -6,4 +6,5 @@ export default defineBuildConfig({ emitCJS: true, }, entries: ["src/index", "src/node"], + externals: ["undici"], }); diff --git a/examples/proxy.mjs b/examples/proxy.mjs new file mode 100644 index 00000000..8f0e33aa --- /dev/null +++ b/examples/proxy.mjs @@ -0,0 +1,9 @@ +import { Agent } from "undici"; +import { ofetch } from "ofetch"; + +// Note: This make fetch unsecure to MITM attacks. USE AT YOUW OWN RISK! +const unsecureAgent = new Agent({ connect: { rejectUnauthorized: false } }); +const unsecureFetch = ofetch.create({ dispatcher: unsecureAgent }); +const data = await unsecureFetch("https://www.squid-cache.org/"); + +console.log(data); diff --git a/package.json b/package.json index b09297b8..f956c488 100644 --- a/package.json +++ b/package.json @@ -89,6 +89,7 @@ "std-env": "^3.7.0", "typescript": "^5.5.4", "unbuild": "2.0.0", + "undici": "^5.27.0", "vitest": "^2.0.5" }, "packageManager": "pnpm@9.9.0" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 8d91ac42..a9b0d41f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -60,6 +60,9 @@ importers: unbuild: specifier: 2.0.0 version: 2.0.0(typescript@5.5.4) + undici: + specifier: ^5.27.0 + version: 5.28.4 vitest: specifier: ^2.0.5 version: 2.0.5(@types/node@22.5.0) @@ -594,6 +597,10 @@ packages: resolution: {integrity: sha512-BsWiH1yFGjXXS2yvrf5LyuoSIIbPrGUWob917o+BTKuZ7qJdxX8aJLRxs1fS9n6r7vESrq1OUqb68dANcFXuQQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@fastify/busboy@2.1.1': + resolution: {integrity: sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA==} + engines: {node: '>=14'} + '@humanwhocodes/module-importer@1.0.1': resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==} engines: {node: '>=12.22'} @@ -2571,6 +2578,10 @@ packages: undici-types@6.19.8: resolution: {integrity: sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==} + undici@5.28.4: + resolution: {integrity: sha512-72RFADWFqKmUb2hmmvNODKL3p9hcB6Gt2DOQMis1SEBaV6a4MH8soBvzg+95CYhCKPFedut2JY9bMfrDl9D23g==} + engines: {node: '>=14.0'} + unenv@1.10.0: resolution: {integrity: sha512-wY5bskBQFL9n3Eca5XnhH6KbUo/tfvkwm9OpcdCvLaeA7piBNbavbOKJySEwQ1V0RH6HvNlSAFRTpvTqgKRQXQ==} @@ -3078,6 +3089,8 @@ snapshots: '@eslint/object-schema@2.1.4': {} + '@fastify/busboy@2.1.1': {} + '@humanwhocodes/module-importer@1.0.1': {} '@humanwhocodes/retry@0.3.0': {} @@ -5155,6 +5168,10 @@ snapshots: undici-types@6.19.8: {} + undici@5.28.4: + dependencies: + '@fastify/busboy': 2.1.1 + unenv@1.10.0: dependencies: consola: 3.2.3 diff --git a/src/types.ts b/src/types.ts index e16566b9..e9c9cbdf 100644 --- a/src/types.ts +++ b/src/types.ts @@ -33,10 +33,22 @@ export interface FetchOptions /** * @experimental Set to "half" to enable duplex streaming. * Will be automatically set to "half" when using a ReadableStream as body. - * https://fetch.spec.whatwg.org/#enumdef-requestduplex + * @see https://fetch.spec.whatwg.org/#enumdef-requestduplex */ duplex?: "half" | undefined; + /** + * Only supported in Node.js >= 18 using undici + * + * @see https://undici.nodejs.org/#/docs/api/Dispatcher + */ + dispatcher?: InstanceType; + + /** + * Only supported older Node.js versions using node-fetch-native polyfill. + */ + agent?: unknown; + /** timeout in milliseconds */ timeout?: number;