From 6484e5fdd61015b1fe365ae804faffd8f71990b5 Mon Sep 17 00:00:00 2001 From: dom Date: Thu, 18 Jun 2026 11:46:28 -0700 Subject: [PATCH 1/4] workaround for 24.17.0 --- README.md | 18 ++++++---- src/_shims/README.md | 7 ++-- src/_shims/auto/runtime-node.ts | 60 ++++++++++++++++++++++++++++++++- src/index.ts | 10 +++--- tests/auto-runtime-node.test.ts | 23 +++++++++++++ 5 files changed, 104 insertions(+), 14 deletions(-) create mode 100644 tests/auto-runtime-node.test.ts diff --git a/README.md b/README.md index e7676522..95d727fb 100644 --- a/README.md +++ b/README.md @@ -222,11 +222,11 @@ validate or strip extra properties from the response from the API. ### Customizing the fetch client -By default, this library uses `node-fetch` in Node, and expects a global `fetch` function in other environments. +By default, this library uses `globalThis.fetch` in modern Node.js, falls back to `node-fetch` +in older Node.js environments, and expects a global `fetch` function in other runtimes. -If you would prefer to use a global, web-standards-compliant `fetch` function even in a Node environment, -(for example, if you are running Node with `--experimental-fetch` or using NextJS which polyfills with `undici`), -add the following import before your first import `from "Browserbase"`: +If you would like to force the web-standard fetch shim, add the following import before your first +import `from "Browserbase"`: ```ts // Tell TypeScript and the package to use the global web fetch instead of node-fetch. @@ -235,7 +235,7 @@ import '@browserbasehq/sdk/shims/web'; import Browserbase from '@browserbasehq/sdk'; ``` -To do the inverse, add `import "@browserbasehq/sdk/shims/node"` (which does import polyfills). +To force the legacy Node.js shim, add `import "@browserbasehq/sdk/shims/node"` (which does import polyfills). This can also be useful if you are getting the wrong TypeScript types for `Response` ([more details](https://github.com/browserbase/sdk-node/tree/main/src/_shims#readme)). ### Logging and middleware @@ -262,9 +262,13 @@ This is intended for debugging purposes only and may change in the future withou ### Configuring an HTTP(S) Agent (e.g., for proxies) -By default, this library uses a stable agent for all http/https requests to reuse TCP connections, eliminating many TCP & TLS handshakes and shaving around 100ms off most requests. +By default, modern Node.js uses `globalThis.fetch` and no node:http Agent is attached. In older +Node.js environments, or when you force the legacy Node.js shim, this library uses a stable agent +for all http/https requests to reuse TCP connections, eliminating many TCP & TLS handshakes. -If you would like to disable or customize this behavior, for example to use the API behind a proxy, you can pass an `httpAgent` which is used for all requests (be they http or https), for example: +If you would like to customize agent behavior, for example to use the API behind a proxy, you can +pass an `httpAgent`. In modern Node.js this uses the legacy `node-fetch` transport for that request +so the node:http Agent can be honored: ```ts diff --git a/src/_shims/README.md b/src/_shims/README.md index 3c78493d..8c65b201 100644 --- a/src/_shims/README.md +++ b/src/_shims/README.md @@ -3,7 +3,9 @@ `@browserbasehq/sdk` supports a wide variety of runtime environments like Node.js, Deno, Bun, browsers, and various edge runtimes, as well as both CommonJS (CJS) and EcmaScript Modules (ESM). -To do this, `@browserbasehq/sdk` provides shims for either using `node-fetch` when in Node (because `fetch` is still experimental there) or the global `fetch` API built into the environment when not in Node. +To do this, `@browserbasehq/sdk` provides shims for using `globalThis.fetch` in modern Node.js, +falling back to `node-fetch` in older Node.js environments, or using the global `fetch` API built +into non-Node runtimes. It uses [conditional exports](https://nodejs.org/api/packages.html#conditional-exports) to automatically select the correct shims for each environment. However, conditional exports are a fairly new @@ -33,7 +35,8 @@ All client code imports shims from `@browserbasehq/sdk/_shims/index`, which: - re-exports the installed shims from `@browserbasehq/sdk/_shims/registry`. `@browserbasehq/sdk/_shims/auto/runtime` exports web runtime shims. -If the `node` export condition is set, the export map replaces it with `@browserbasehq/sdk/_shims/auto/runtime-node`. +If the `node` export condition is set, the export map replaces it with `@browserbasehq/sdk/_shims/auto/runtime-node`, +which prefers native `globalThis.fetch` when available and falls back to the Node runtime shim when needed. ### How it works - Type time diff --git a/src/_shims/auto/runtime-node.ts b/src/_shims/auto/runtime-node.ts index 0ae2216f..42f900c2 100644 --- a/src/_shims/auto/runtime-node.ts +++ b/src/_shims/auto/runtime-node.ts @@ -1,4 +1,62 @@ /** * Disclaimer: modules in _shims aren't intended to be imported by SDK users. */ -export * from '../node-runtime'; +import { Readable } from 'node:stream'; +import { getRuntime as getNodeRuntime } from '../node-runtime'; +import { type Shims } from '../registry'; + +type FetchInitWithAgent = { + agent?: unknown; + body?: unknown; + [key: string]: unknown; +}; + +function usesNodeFetchOnlyFeatures(init: FetchInitWithAgent | undefined): boolean { + if (!init) return false; + + // Node's native fetch does not use node:http Agent instances. Preserve + // historical behavior for callers who explicitly configure httpAgent. + if (init.agent) return true; + + // Multipart uploads are encoded as node:stream Readable bodies by the Node + // runtime shim. Keep those on node-fetch to avoid requiring undici's + // stream-specific `duplex` option here. + const body = init.body; + return body instanceof Readable || typeof (body as any)?.pipe === 'function'; +} + +function stripNodeFetchOptions(init: FetchInitWithAgent | undefined): FetchInitWithAgent | undefined { + if (!init || !('agent' in init)) return init; + + const { agent: _agent, ...fetchInit } = init; + return fetchInit; +} + +export function getRuntime(): Shims { + const nodeRuntime = getNodeRuntime(); + const nativeFetch = (globalThis as any).fetch; + + if (typeof nativeFetch !== 'function') { + return nodeRuntime; + } + + return { + ...nodeRuntime, + fetch: (url: unknown, init?: FetchInitWithAgent) => { + if (usesNodeFetchOnlyFeatures(init)) { + return nodeRuntime.fetch(url, init); + } + + return nativeFetch.call(undefined, url, stripNodeFetchOptions(init)); + }, + Request: + typeof (globalThis as any).Request !== 'undefined' ? (globalThis as any).Request : nodeRuntime.Request, + Response: + typeof (globalThis as any).Response !== 'undefined' ? + (globalThis as any).Response + : nodeRuntime.Response, + Headers: + typeof (globalThis as any).Headers !== 'undefined' ? (globalThis as any).Headers : nodeRuntime.Headers, + getDefaultAgent: () => undefined, + }; +} diff --git a/src/index.ts b/src/index.ts index 68d3aeee..af75d1a5 100644 --- a/src/index.ts +++ b/src/index.ts @@ -61,16 +61,18 @@ export interface ClientOptions { /** * An HTTP agent used to manage HTTP(S) connections. * - * If not provided, an agent will be constructed by default in the Node.js environment, - * otherwise no agent is used. + * If provided in Node.js, requests use the legacy `node-fetch` transport so the agent can be + * honored. The default modern Node.js transport uses `globalThis.fetch` and does not attach + * a node:http Agent. */ httpAgent?: Agent | undefined; /** * Specify a custom `fetch` function implementation. * - * If not provided, we use `node-fetch` on Node.js and otherwise expect that `fetch` is - * defined globally. + * If not provided, we use `globalThis.fetch` in Node.js when available, fall back to + * `node-fetch` in older Node.js environments, and otherwise expect that `fetch` is defined + * globally. */ fetch?: Core.Fetch | undefined; diff --git a/tests/auto-runtime-node.test.ts b/tests/auto-runtime-node.test.ts new file mode 100644 index 00000000..fb5ca72b --- /dev/null +++ b/tests/auto-runtime-node.test.ts @@ -0,0 +1,23 @@ +import { getRuntime } from '@browserbasehq/sdk/_shims/auto/runtime'; + +describe('auto node runtime', () => { + const originalFetch = (globalThis as any).fetch; + + afterEach(() => { + (globalThis as any).fetch = originalFetch; + }); + + test('prefers native fetch when available', async () => { + const response = { ok: true }; + const nativeFetch = jest.fn().mockResolvedValue(response); + (globalThis as any).fetch = nativeFetch; + + const runtime = getRuntime(); + + await expect(runtime.fetch('https://example.com', { method: 'post', body: '{}' })).resolves.toBe( + response, + ); + expect(nativeFetch).toHaveBeenCalledWith('https://example.com', { method: 'post', body: '{}' }); + expect(runtime.getDefaultAgent('https://example.com')).toBeUndefined(); + }); +}); From 7951be6cfd789e5651172f9f7c7572f65cd82ec6 Mon Sep 17 00:00:00 2001 From: dom Date: Thu, 18 Jun 2026 12:22:32 -0700 Subject: [PATCH 2/4] fix tests --- src/_shims/auto/runtime-node.ts | 21 ++++++++++--------- tests/api-resources/certificates.test.ts | 2 +- tests/api-resources/contexts.test.ts | 2 +- tests/api-resources/extensions.test.ts | 2 +- tests/api-resources/fetch-api.test.ts | 2 +- tests/api-resources/projects.test.ts | 2 +- tests/api-resources/search.test.ts | 2 +- tests/api-resources/sessions/logs.test.ts | 2 +- .../api-resources/sessions/recording.test.ts | 2 +- tests/api-resources/sessions/replays.test.ts | 2 +- tests/api-resources/sessions/sessions.test.ts | 2 +- tests/api-resources/sessions/uploads.test.ts | 2 +- tests/auto-runtime-node.test.ts | 17 +++++++++++++++ 13 files changed, 39 insertions(+), 21 deletions(-) diff --git a/src/_shims/auto/runtime-node.ts b/src/_shims/auto/runtime-node.ts index 42f900c2..0c06b9a3 100644 --- a/src/_shims/auto/runtime-node.ts +++ b/src/_shims/auto/runtime-node.ts @@ -16,19 +16,20 @@ function usesNodeFetchOnlyFeatures(init: FetchInitWithAgent | undefined): boolea // Node's native fetch does not use node:http Agent instances. Preserve // historical behavior for callers who explicitly configure httpAgent. - if (init.agent) return true; - - // Multipart uploads are encoded as node:stream Readable bodies by the Node - // runtime shim. Keep those on node-fetch to avoid requiring undici's - // stream-specific `duplex` option here. - const body = init.body; - return body instanceof Readable || typeof (body as any)?.pipe === 'function'; + return Boolean(init.agent); } -function stripNodeFetchOptions(init: FetchInitWithAgent | undefined): FetchInitWithAgent | undefined { - if (!init || !('agent' in init)) return init; +function buildNativeFetchOptions(init: FetchInitWithAgent | undefined): FetchInitWithAgent | undefined { + if (!init) return init; const { agent: _agent, ...fetchInit } = init; + + // Node's native fetch requires `duplex: 'half'` when the body is a stream. + const body = fetchInit.body; + if (body instanceof Readable || typeof (body as any)?.pipe === 'function') { + return { duplex: 'half', ...fetchInit }; + } + return fetchInit; } @@ -47,7 +48,7 @@ export function getRuntime(): Shims { return nodeRuntime.fetch(url, init); } - return nativeFetch.call(undefined, url, stripNodeFetchOptions(init)); + return nativeFetch.call(undefined, url, buildNativeFetchOptions(init)); }, Request: typeof (globalThis as any).Request !== 'undefined' ? (globalThis as any).Request : nodeRuntime.Request, diff --git a/tests/api-resources/certificates.test.ts b/tests/api-resources/certificates.test.ts index f795a51f..2c12fc7e 100644 --- a/tests/api-resources/certificates.test.ts +++ b/tests/api-resources/certificates.test.ts @@ -1,7 +1,7 @@ // File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. import Browserbase, { toFile } from '@browserbasehq/sdk'; -import { Response } from 'node-fetch'; +import { Response } from '@browserbasehq/sdk/_shims/index'; const client = new Browserbase({ apiKey: 'My API Key', diff --git a/tests/api-resources/contexts.test.ts b/tests/api-resources/contexts.test.ts index 17e3dcd6..2456cbf0 100644 --- a/tests/api-resources/contexts.test.ts +++ b/tests/api-resources/contexts.test.ts @@ -1,7 +1,7 @@ // File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. import Browserbase from '@browserbasehq/sdk'; -import { Response } from 'node-fetch'; +import { Response } from '@browserbasehq/sdk/_shims/index'; const client = new Browserbase({ apiKey: 'My API Key', diff --git a/tests/api-resources/extensions.test.ts b/tests/api-resources/extensions.test.ts index 3ebdf1a6..effbe36c 100644 --- a/tests/api-resources/extensions.test.ts +++ b/tests/api-resources/extensions.test.ts @@ -1,7 +1,7 @@ // File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. import Browserbase, { toFile } from '@browserbasehq/sdk'; -import { Response } from 'node-fetch'; +import { Response } from '@browserbasehq/sdk/_shims/index'; const client = new Browserbase({ apiKey: 'My API Key', diff --git a/tests/api-resources/fetch-api.test.ts b/tests/api-resources/fetch-api.test.ts index a74882c6..615542ee 100644 --- a/tests/api-resources/fetch-api.test.ts +++ b/tests/api-resources/fetch-api.test.ts @@ -1,7 +1,7 @@ // File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. import Browserbase from '@browserbasehq/sdk'; -import { Response } from 'node-fetch'; +import { Response } from '@browserbasehq/sdk/_shims/index'; const client = new Browserbase({ apiKey: 'My API Key', diff --git a/tests/api-resources/projects.test.ts b/tests/api-resources/projects.test.ts index bd106995..53e1912f 100644 --- a/tests/api-resources/projects.test.ts +++ b/tests/api-resources/projects.test.ts @@ -1,7 +1,7 @@ // File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. import Browserbase from '@browserbasehq/sdk'; -import { Response } from 'node-fetch'; +import { Response } from '@browserbasehq/sdk/_shims/index'; const client = new Browserbase({ apiKey: 'My API Key', diff --git a/tests/api-resources/search.test.ts b/tests/api-resources/search.test.ts index c0384ae3..d262717c 100644 --- a/tests/api-resources/search.test.ts +++ b/tests/api-resources/search.test.ts @@ -1,7 +1,7 @@ // File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. import Browserbase from '@browserbasehq/sdk'; -import { Response } from 'node-fetch'; +import { Response } from '@browserbasehq/sdk/_shims/index'; const client = new Browserbase({ apiKey: 'My API Key', diff --git a/tests/api-resources/sessions/logs.test.ts b/tests/api-resources/sessions/logs.test.ts index bd5d7383..7e578bd4 100644 --- a/tests/api-resources/sessions/logs.test.ts +++ b/tests/api-resources/sessions/logs.test.ts @@ -1,7 +1,7 @@ // File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. import Browserbase from '@browserbasehq/sdk'; -import { Response } from 'node-fetch'; +import { Response } from '@browserbasehq/sdk/_shims/index'; const client = new Browserbase({ apiKey: 'My API Key', diff --git a/tests/api-resources/sessions/recording.test.ts b/tests/api-resources/sessions/recording.test.ts index bdb7eb54..b4ec380f 100644 --- a/tests/api-resources/sessions/recording.test.ts +++ b/tests/api-resources/sessions/recording.test.ts @@ -1,7 +1,7 @@ // File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. import Browserbase from '@browserbasehq/sdk'; -import { Response } from 'node-fetch'; +import { Response } from '@browserbasehq/sdk/_shims/index'; const client = new Browserbase({ apiKey: 'My API Key', diff --git a/tests/api-resources/sessions/replays.test.ts b/tests/api-resources/sessions/replays.test.ts index 4387aaa9..1c5f1005 100644 --- a/tests/api-resources/sessions/replays.test.ts +++ b/tests/api-resources/sessions/replays.test.ts @@ -1,7 +1,7 @@ // File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. import Browserbase from '@browserbasehq/sdk'; -import { Response } from 'node-fetch'; +import { Response } from '@browserbasehq/sdk/_shims/index'; const client = new Browserbase({ apiKey: 'My API Key', diff --git a/tests/api-resources/sessions/sessions.test.ts b/tests/api-resources/sessions/sessions.test.ts index 1933ab12..137f03d0 100644 --- a/tests/api-resources/sessions/sessions.test.ts +++ b/tests/api-resources/sessions/sessions.test.ts @@ -1,7 +1,7 @@ // File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. import Browserbase from '@browserbasehq/sdk'; -import { Response } from 'node-fetch'; +import { Response } from '@browserbasehq/sdk/_shims/index'; const client = new Browserbase({ apiKey: 'My API Key', diff --git a/tests/api-resources/sessions/uploads.test.ts b/tests/api-resources/sessions/uploads.test.ts index 6b3abc91..2c1fe8c7 100644 --- a/tests/api-resources/sessions/uploads.test.ts +++ b/tests/api-resources/sessions/uploads.test.ts @@ -1,7 +1,7 @@ // File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. import Browserbase, { toFile } from '@browserbasehq/sdk'; -import { Response } from 'node-fetch'; +import { Response } from '@browserbasehq/sdk/_shims/index'; const client = new Browserbase({ apiKey: 'My API Key', diff --git a/tests/auto-runtime-node.test.ts b/tests/auto-runtime-node.test.ts index fb5ca72b..43504b67 100644 --- a/tests/auto-runtime-node.test.ts +++ b/tests/auto-runtime-node.test.ts @@ -1,4 +1,5 @@ import { getRuntime } from '@browserbasehq/sdk/_shims/auto/runtime'; +import { Readable } from 'node:stream'; describe('auto node runtime', () => { const originalFetch = (globalThis as any).fetch; @@ -20,4 +21,20 @@ describe('auto node runtime', () => { expect(nativeFetch).toHaveBeenCalledWith('https://example.com', { method: 'post', body: '{}' }); expect(runtime.getDefaultAgent('https://example.com')).toBeUndefined(); }); + + test('uses native fetch for stream bodies with duplex enabled', async () => { + const response = { ok: true }; + const nativeFetch = jest.fn().mockResolvedValue(response); + const body = Readable.from(['Example data']); + (globalThis as any).fetch = nativeFetch; + + const runtime = getRuntime(); + + await expect(runtime.fetch('https://example.com', { method: 'post', body })).resolves.toBe(response); + expect(nativeFetch).toHaveBeenCalledWith('https://example.com', { + duplex: 'half', + method: 'post', + body, + }); + }); }); From ebf7b1e8d64b534f8bcee9a383e9af45c502068b Mon Sep 17 00:00:00 2001 From: dom Date: Thu, 18 Jun 2026 12:32:24 -0700 Subject: [PATCH 3/4] fix lint --- src/_shims/index.d.ts | 4 ++++ src/_shims/node-types.d.ts | 3 +++ 2 files changed, 7 insertions(+) diff --git a/src/_shims/index.d.ts b/src/_shims/index.d.ts index 63cd4332..2383aa19 100644 --- a/src/_shims/index.d.ts +++ b/src/_shims/index.d.ts @@ -18,6 +18,8 @@ export const fetch: SelectType; // @ts-ignore export type Request = SelectType; // @ts-ignore +export const Request: SelectType; +// @ts-ignore export type RequestInfo = SelectType; // @ts-ignore export type RequestInit = SelectType; @@ -25,6 +27,8 @@ export type RequestInit = SelectType; // @ts-ignore export type Response = SelectType; // @ts-ignore +export const Response: SelectType; +// @ts-ignore export type ResponseInit = SelectType; // @ts-ignore export type ResponseType = SelectType; diff --git a/src/_shims/node-types.d.ts b/src/_shims/node-types.d.ts index c159e5fa..8bd5280d 100644 --- a/src/_shims/node-types.d.ts +++ b/src/_shims/node-types.d.ts @@ -12,14 +12,17 @@ export { ReadableStream } from 'node:stream/web'; export const fetch: typeof nf.default; export type Request = nf.Request; +export const Request: typeof nf.Request; export type RequestInfo = nf.RequestInfo; export type RequestInit = nf.RequestInit; export type Response = nf.Response; +export const Response: typeof nf.Response; export type ResponseInit = nf.ResponseInit; export type ResponseType = nf.ResponseType; export type BodyInit = nf.BodyInit; export type Headers = nf.Headers; +export const Headers: typeof nf.Headers; export type HeadersInit = nf.HeadersInit; type EndingType = 'native' | 'transparent'; From 78c0d1816cf4b6012f8794b784bf29f2dbb349c2 Mon Sep 17 00:00:00 2001 From: dom Date: Thu, 18 Jun 2026 13:28:52 -0700 Subject: [PATCH 4/4] add coverage for init.agent --- tests/auto-runtime-node.test.ts | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/tests/auto-runtime-node.test.ts b/tests/auto-runtime-node.test.ts index 43504b67..c2e9fec7 100644 --- a/tests/auto-runtime-node.test.ts +++ b/tests/auto-runtime-node.test.ts @@ -1,4 +1,5 @@ import { getRuntime } from '@browserbasehq/sdk/_shims/auto/runtime'; +import http from 'node:http'; import { Readable } from 'node:stream'; describe('auto node runtime', () => { @@ -37,4 +38,19 @@ describe('auto node runtime', () => { body, }); }); + + test('uses node-fetch when an agent is provided', async () => { + const nativeFetch = jest.fn().mockRejectedValue(new Error('native fetch should not be called')); + (globalThis as any).fetch = nativeFetch; + + const agent = new http.Agent({ keepAlive: false }); + + try { + const runtime = getRuntime(); + await expect(runtime.fetch('not a url', { agent })).rejects.toThrow(); + expect(nativeFetch).not.toHaveBeenCalled(); + } finally { + agent.destroy(); + } + }); });