Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 11 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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
Expand All @@ -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:

<!-- prettier-ignore -->
```ts
Expand Down
7 changes: 5 additions & 2 deletions src/_shims/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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

Expand Down
61 changes: 60 additions & 1 deletion src/_shims/auto/runtime-node.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,63 @@
/**
* 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.
return Boolean(init.agent);
}

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;
}

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, buildNativeFetchOptions(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,
};
}
4 changes: 4 additions & 0 deletions src/_shims/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,17 @@ export const fetch: SelectType<typeof manual.fetch, typeof auto.fetch>;
// @ts-ignore
export type Request = SelectType<manual.Request, auto.Request>;
// @ts-ignore
export const Request: SelectType<typeof manual.Request, typeof auto.Request>;
// @ts-ignore
export type RequestInfo = SelectType<manual.RequestInfo, auto.RequestInfo>;
// @ts-ignore
export type RequestInit = SelectType<manual.RequestInit, auto.RequestInit>;

// @ts-ignore
export type Response = SelectType<manual.Response, auto.Response>;
// @ts-ignore
export const Response: SelectType<typeof manual.Response, typeof auto.Response>;
// @ts-ignore
export type ResponseInit = SelectType<manual.ResponseInit, auto.ResponseInit>;
// @ts-ignore
export type ResponseType = SelectType<manual.ResponseType, auto.ResponseType>;
Expand Down
3 changes: 3 additions & 0 deletions src/_shims/node-types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
10 changes: 6 additions & 4 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down
2 changes: 1 addition & 1 deletion tests/api-resources/certificates.test.ts
Original file line number Diff line number Diff line change
@@ -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',
Expand Down
2 changes: 1 addition & 1 deletion tests/api-resources/contexts.test.ts
Original file line number Diff line number Diff line change
@@ -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',
Expand Down
2 changes: 1 addition & 1 deletion tests/api-resources/extensions.test.ts
Original file line number Diff line number Diff line change
@@ -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',
Expand Down
2 changes: 1 addition & 1 deletion tests/api-resources/fetch-api.test.ts
Original file line number Diff line number Diff line change
@@ -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',
Expand Down
2 changes: 1 addition & 1 deletion tests/api-resources/projects.test.ts
Original file line number Diff line number Diff line change
@@ -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',
Expand Down
2 changes: 1 addition & 1 deletion tests/api-resources/search.test.ts
Original file line number Diff line number Diff line change
@@ -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',
Expand Down
2 changes: 1 addition & 1 deletion tests/api-resources/sessions/logs.test.ts
Original file line number Diff line number Diff line change
@@ -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',
Expand Down
2 changes: 1 addition & 1 deletion tests/api-resources/sessions/recording.test.ts
Original file line number Diff line number Diff line change
@@ -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',
Expand Down
2 changes: 1 addition & 1 deletion tests/api-resources/sessions/replays.test.ts
Original file line number Diff line number Diff line change
@@ -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',
Expand Down
2 changes: 1 addition & 1 deletion tests/api-resources/sessions/sessions.test.ts
Original file line number Diff line number Diff line change
@@ -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',
Expand Down
2 changes: 1 addition & 1 deletion tests/api-resources/sessions/uploads.test.ts
Original file line number Diff line number Diff line change
@@ -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',
Expand Down
56 changes: 56 additions & 0 deletions tests/auto-runtime-node.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import { getRuntime } from '@browserbasehq/sdk/_shims/auto/runtime';
import http from 'node:http';
import { Readable } from 'node:stream';

describe('auto node runtime', () => {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

could we add a test that confirms that when init.agent exists, we prefer node fetch to native

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah done in 78c0d18

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();
});

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,
});
});

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();
}
});
});
Loading