Skip to content

Commit 39b340d

Browse files
authored
Workaround premature-close issue in 24.17.0 (#191)
Fix for nodejs/node#63989.
1 parent 5804781 commit 39b340d

18 files changed

Lines changed: 156 additions & 25 deletions

README.md

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -222,11 +222,11 @@ validate or strip extra properties from the response from the API.
222222

223223
### Customizing the fetch client
224224

225-
By default, this library uses `node-fetch` in Node, and expects a global `fetch` function in other environments.
225+
By default, this library uses `globalThis.fetch` in modern Node.js, falls back to `node-fetch`
226+
in older Node.js environments, and expects a global `fetch` function in other runtimes.
226227

227-
If you would prefer to use a global, web-standards-compliant `fetch` function even in a Node environment,
228-
(for example, if you are running Node with `--experimental-fetch` or using NextJS which polyfills with `undici`),
229-
add the following import before your first import `from "Browserbase"`:
228+
If you would like to force the web-standard fetch shim, add the following import before your first
229+
import `from "Browserbase"`:
230230

231231
```ts
232232
// Tell TypeScript and the package to use the global web fetch instead of node-fetch.
@@ -235,7 +235,7 @@ import '@browserbasehq/sdk/shims/web';
235235
import Browserbase from '@browserbasehq/sdk';
236236
```
237237

238-
To do the inverse, add `import "@browserbasehq/sdk/shims/node"` (which does import polyfills).
238+
To force the legacy Node.js shim, add `import "@browserbasehq/sdk/shims/node"` (which does import polyfills).
239239
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)).
240240

241241
### Logging and middleware
@@ -262,9 +262,13 @@ This is intended for debugging purposes only and may change in the future withou
262262

263263
### Configuring an HTTP(S) Agent (e.g., for proxies)
264264

265-
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.
265+
By default, modern Node.js uses `globalThis.fetch` and no node:http Agent is attached. In older
266+
Node.js environments, or when you force the legacy Node.js shim, this library uses a stable agent
267+
for all http/https requests to reuse TCP connections, eliminating many TCP & TLS handshakes.
266268

267-
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:
269+
If you would like to customize agent behavior, for example to use the API behind a proxy, you can
270+
pass an `httpAgent`. In modern Node.js this uses the legacy `node-fetch` transport for that request
271+
so the node:http Agent can be honored:
268272

269273
<!-- prettier-ignore -->
270274
```ts

src/_shims/README.md

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,9 @@
33
`@browserbasehq/sdk` supports a wide variety of runtime environments like Node.js, Deno, Bun, browsers, and various
44
edge runtimes, as well as both CommonJS (CJS) and EcmaScript Modules (ESM).
55

6-
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.
6+
To do this, `@browserbasehq/sdk` provides shims for using `globalThis.fetch` in modern Node.js,
7+
falling back to `node-fetch` in older Node.js environments, or using the global `fetch` API built
8+
into non-Node runtimes.
79

810
It uses [conditional exports](https://nodejs.org/api/packages.html#conditional-exports) to
911
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:
3335
- re-exports the installed shims from `@browserbasehq/sdk/_shims/registry`.
3436

3537
`@browserbasehq/sdk/_shims/auto/runtime` exports web runtime shims.
36-
If the `node` export condition is set, the export map replaces it with `@browserbasehq/sdk/_shims/auto/runtime-node`.
38+
If the `node` export condition is set, the export map replaces it with `@browserbasehq/sdk/_shims/auto/runtime-node`,
39+
which prefers native `globalThis.fetch` when available and falls back to the Node runtime shim when needed.
3740

3841
### How it works - Type time
3942

src/_shims/auto/runtime-node.ts

Lines changed: 60 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,63 @@
11
/**
22
* Disclaimer: modules in _shims aren't intended to be imported by SDK users.
33
*/
4-
export * from '../node-runtime';
4+
import { Readable } from 'node:stream';
5+
import { getRuntime as getNodeRuntime } from '../node-runtime';
6+
import { type Shims } from '../registry';
7+
8+
type FetchInitWithAgent = {
9+
agent?: unknown;
10+
body?: unknown;
11+
[key: string]: unknown;
12+
};
13+
14+
function usesNodeFetchOnlyFeatures(init: FetchInitWithAgent | undefined): boolean {
15+
if (!init) return false;
16+
17+
// Node's native fetch does not use node:http Agent instances. Preserve
18+
// historical behavior for callers who explicitly configure httpAgent.
19+
return Boolean(init.agent);
20+
}
21+
22+
function buildNativeFetchOptions(init: FetchInitWithAgent | undefined): FetchInitWithAgent | undefined {
23+
if (!init) return init;
24+
25+
const { agent: _agent, ...fetchInit } = init;
26+
27+
// Node's native fetch requires `duplex: 'half'` when the body is a stream.
28+
const body = fetchInit.body;
29+
if (body instanceof Readable || typeof (body as any)?.pipe === 'function') {
30+
return { duplex: 'half', ...fetchInit };
31+
}
32+
33+
return fetchInit;
34+
}
35+
36+
export function getRuntime(): Shims {
37+
const nodeRuntime = getNodeRuntime();
38+
const nativeFetch = (globalThis as any).fetch;
39+
40+
if (typeof nativeFetch !== 'function') {
41+
return nodeRuntime;
42+
}
43+
44+
return {
45+
...nodeRuntime,
46+
fetch: (url: unknown, init?: FetchInitWithAgent) => {
47+
if (usesNodeFetchOnlyFeatures(init)) {
48+
return nodeRuntime.fetch(url, init);
49+
}
50+
51+
return nativeFetch.call(undefined, url, buildNativeFetchOptions(init));
52+
},
53+
Request:
54+
typeof (globalThis as any).Request !== 'undefined' ? (globalThis as any).Request : nodeRuntime.Request,
55+
Response:
56+
typeof (globalThis as any).Response !== 'undefined' ?
57+
(globalThis as any).Response
58+
: nodeRuntime.Response,
59+
Headers:
60+
typeof (globalThis as any).Headers !== 'undefined' ? (globalThis as any).Headers : nodeRuntime.Headers,
61+
getDefaultAgent: () => undefined,
62+
};
63+
}

src/_shims/index.d.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,13 +18,17 @@ export const fetch: SelectType<typeof manual.fetch, typeof auto.fetch>;
1818
// @ts-ignore
1919
export type Request = SelectType<manual.Request, auto.Request>;
2020
// @ts-ignore
21+
export const Request: SelectType<typeof manual.Request, typeof auto.Request>;
22+
// @ts-ignore
2123
export type RequestInfo = SelectType<manual.RequestInfo, auto.RequestInfo>;
2224
// @ts-ignore
2325
export type RequestInit = SelectType<manual.RequestInit, auto.RequestInit>;
2426

2527
// @ts-ignore
2628
export type Response = SelectType<manual.Response, auto.Response>;
2729
// @ts-ignore
30+
export const Response: SelectType<typeof manual.Response, typeof auto.Response>;
31+
// @ts-ignore
2832
export type ResponseInit = SelectType<manual.ResponseInit, auto.ResponseInit>;
2933
// @ts-ignore
3034
export type ResponseType = SelectType<manual.ResponseType, auto.ResponseType>;

src/_shims/node-types.d.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,14 +12,17 @@ export { ReadableStream } from 'node:stream/web';
1212
export const fetch: typeof nf.default;
1313

1414
export type Request = nf.Request;
15+
export const Request: typeof nf.Request;
1516
export type RequestInfo = nf.RequestInfo;
1617
export type RequestInit = nf.RequestInit;
1718

1819
export type Response = nf.Response;
20+
export const Response: typeof nf.Response;
1921
export type ResponseInit = nf.ResponseInit;
2022
export type ResponseType = nf.ResponseType;
2123
export type BodyInit = nf.BodyInit;
2224
export type Headers = nf.Headers;
25+
export const Headers: typeof nf.Headers;
2326
export type HeadersInit = nf.HeadersInit;
2427

2528
type EndingType = 'native' | 'transparent';

src/index.ts

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -61,16 +61,18 @@ export interface ClientOptions {
6161
/**
6262
* An HTTP agent used to manage HTTP(S) connections.
6363
*
64-
* If not provided, an agent will be constructed by default in the Node.js environment,
65-
* otherwise no agent is used.
64+
* If provided in Node.js, requests use the legacy `node-fetch` transport so the agent can be
65+
* honored. The default modern Node.js transport uses `globalThis.fetch` and does not attach
66+
* a node:http Agent.
6667
*/
6768
httpAgent?: Agent | undefined;
6869

6970
/**
7071
* Specify a custom `fetch` function implementation.
7172
*
72-
* If not provided, we use `node-fetch` on Node.js and otherwise expect that `fetch` is
73-
* defined globally.
73+
* If not provided, we use `globalThis.fetch` in Node.js when available, fall back to
74+
* `node-fetch` in older Node.js environments, and otherwise expect that `fetch` is defined
75+
* globally.
7476
*/
7577
fetch?: Core.Fetch | undefined;
7678

tests/api-resources/certificates.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
22

33
import Browserbase, { toFile } from '@browserbasehq/sdk';
4-
import { Response } from 'node-fetch';
4+
import { Response } from '@browserbasehq/sdk/_shims/index';
55

66
const client = new Browserbase({
77
apiKey: 'My API Key',

tests/api-resources/contexts.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
22

33
import Browserbase from '@browserbasehq/sdk';
4-
import { Response } from 'node-fetch';
4+
import { Response } from '@browserbasehq/sdk/_shims/index';
55

66
const client = new Browserbase({
77
apiKey: 'My API Key',

tests/api-resources/extensions.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
22

33
import Browserbase, { toFile } from '@browserbasehq/sdk';
4-
import { Response } from 'node-fetch';
4+
import { Response } from '@browserbasehq/sdk/_shims/index';
55

66
const client = new Browserbase({
77
apiKey: 'My API Key',

tests/api-resources/fetch-api.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
22

33
import Browserbase from '@browserbasehq/sdk';
4-
import { Response } from 'node-fetch';
4+
import { Response } from '@browserbasehq/sdk/_shims/index';
55

66
const client = new Browserbase({
77
apiKey: 'My API Key',

0 commit comments

Comments
 (0)