Visitar URL original
fix(gaxios): remove --bun-plugin-shim and support Bun fetch natively by danieljbruce · Pull Request #9561 · googleapis/google-cloud-node · GitHub
Skip to content

fix(gaxios): remove --bun-plugin-shim and support Bun fetch natively - #9561

Draft
danieljbruce wants to merge 4 commits into
googleapis:mainfrom
danieljbruce:fix/remove-bun-plugin-shim
Draft

danieljbruce wants to merge 4 commits into
googleapis:mainfrom
danieljbruce:fix/remove-bun-plugin-shim

Conversation

@danieljbruce

Copy link
Copy Markdown
Contributor

Description

Removes the --bun-plugin-shim (Bun.plugin onLoad source rewriting) monkey patch from bin/proxyquire-bun-shim.cjs and bin/run-test.cjs, and updates gaxios to route default fetch calls on Bun through Gaxios.#createBunFetch() instead of import('node-fetch') (b/570680331).

Previously, Gaxios.#getFetch() evaluated (await import('node-fetch')).default in non-browser runtimes. In Bun, import('node-fetch') is hijacked by Bun's internal bun:node-fetch polyfill, which calls Bun's internal C++ fetch directly and bypasses globalThis.fetch (as well as nock v14's globalThis.fetch interception) and fails to convert Node stream.Readable request bodies or return Node stream.Readable response bodies for responseType: 'stream'. To work around this, bin/proxyquire-bun-shim.cjs registered a Bun.plugin onLoad hook that rewrote build/esm/src/gaxios.js on disk at load time.

Impact

  • core/packages/gaxios: Gaxios.#getFetch() now uses #createBunFetch() when running under Bun ('Bun' in globalThis && typeof globalThis.fetch === 'function'), delegating to globalThis.fetch (or globalThis.__googleCloudBunFetch when --fetch-shim is active), converting Node Readable request bodies via Readable.toWeb, and lazily wrapping Web ReadableStream response bodies via Readable.fromWeb.
  • bin/proxyquire-bun-shim.cjs & bin/run-test.cjs: Removes --bun-plugin-shim (BUN_PLUGIN_SHIM / BUN_ENABLE_BUN_PLUGIN_SHIM) and the Bun.plugin onLoad source-rewriting hook.

Changes

  • core/packages/gaxios/src/gaxios.ts: Add Gaxios.#createBunFetch() and use it in Gaxios.#getFetch() when running on Bun; forward proxy and tls options to fetchImplementation in _defaultAdapter and handle non-Error AbortController.abort(reason) values.
  • core/packages/gaxios/test/test.getch.ts: Add unit test coverage for proxy/tls forwarding to fetchImplementation and non-Error abort reasons.
  • core/packages/gaxios/package.json, bin/run-test.cjs, bin/proxyquire-bun-shim.cjs: Remove --bun-plugin-shim and its implementation.

Testing

  • Executed pnpm --dir core/packages/gaxios test under both Node.js and Bun (JS_RUNTIME=bun).
  • Verified repeated unit test runs pass with 0 failures across both runtimes.

Alternatives

  • Keeping the Bun.plugin onLoad string-replacement hook in bin/proxyquire-bun-shim.cjs was rejected because rewriting compiled JS files at module load time is fragile and only worked inside the test runner rather than fixing gaxios on Bun directly (b/570091189).

@gemini-code-assist gemini-code-assist Bot left a comment

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.

Code Review

This pull request removes the Bun plugin shim and introduces a native Bun fetch wrapper in gaxios to handle stream conversions between Node.js and Web streams. The review feedback highlights two important issues: first, error events should be explicitly forwarded when piping streams to a PassThrough to avoid silent failures; second, res.arrayBuffer() should be overridden alongside res.text() and res.json() to prevent stream locking errors when standard response methods are invoked.

Comment thread core/packages/gaxios/src/gaxios.ts Outdated
Comment on lines +691 to +694
const stream =
fetchInit.body instanceof Readable
? fetchInit.body
: (fetchInit.body as Readable).pipe(new PassThrough());

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.

high

When piping a non-Readable stream (which has a .pipe method) to a PassThrough stream, any errors emitted by the source stream will not be automatically propagated to the PassThrough stream. This can lead to silent failures or unhandled stream errors during the fetch request. We should explicitly forward the 'error' event from the source stream to the destination stream.

        const source = fetchInit.body as Readable;
        let stream: Readable;
        if (source instanceof Readable) {
          stream = source;
        } else {
          stream = source.pipe(new PassThrough());
          if (typeof source.on === 'function') {
            source.on('error', err => stream.emit('error', err));
          }
        }

Comment on lines +713 to +736
const origText = res.text.bind(res);
const origJson = res.json.bind(res);
Object.defineProperty(res, 'body', {
get() {
nodeStream ||= Readable.fromWeb(rawBody);
return nodeStream;
},
configurable: true,
enumerable: true,
});
res.text = async () => {
if (!nodeStream) return origText();
const chunks: Buffer[] = [];
for await (const chunk of nodeStream) {
chunks.push(
Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk as Uint8Array),
);
}
return Buffer.concat(chunks).toString('utf8');
};
res.json = async () => {
if (!nodeStream) return origJson();
return JSON.parse(await res.text());
};

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.

high

When res.body is accessed, Readable.fromWeb(rawBody) is called, which locks the underlying Web ReadableStream. If a caller subsequently invokes res.arrayBuffer(), the native arrayBuffer() method will fail with a TypeError: body stream is locked error. To ensure full compatibility with standard Response methods (as noted in the repository's general rules), we should also override res.arrayBuffer to read from nodeStream when it is defined.

        const origText = res.text.bind(res);
        const origJson = res.json.bind(res);
        const origArrayBuffer = res.arrayBuffer.bind(res);
        Object.defineProperty(res, 'body', {
          get() {
            nodeStream ||= Readable.fromWeb(rawBody);
            return nodeStream;
          },
          configurable: true,
          enumerable: true,
        });
        res.text = async () => {
          if (!nodeStream) return origText();
          const chunks: Buffer[] = [];
          for await (const chunk of nodeStream) {
            chunks.push(
              Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk as Uint8Array),
            );
          }
          return Buffer.concat(chunks).toString('utf8');
        };
        res.json = async () => {
          if (!nodeStream) return origJson();
          return JSON.parse(await res.text());
        };
        res.arrayBuffer = async () => {
          if (!nodeStream) return origArrayBuffer();
          const chunks: Buffer[] = [];
          for await (const chunk of nodeStream) {
            chunks.push(
              Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk as Uint8Array),
            );
          }
          const buf = Buffer.concat(chunks);
          return buf.buffer.slice(
            buf.byteOffset,
            buf.byteOffset + buf.byteLength,
          );
        };
References
  1. When using google-auth-library v11+ (which uses gaxios v7+), GaxiosResponse supports standard Response methods like arrayBuffer().

@danieljbruce

Copy link
Copy Markdown
Contributor Author

/gemini review

@gemini-code-assist gemini-code-assist Bot left a comment

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.

Code Review

This pull request removes the Bun plugin shim and introduces native support for Bun's fetch environment in Gaxios. It adds a custom Bun fetch wrapper (#createBunFetch) to handle the conversion of Node.js Readable request bodies to Web ReadableStreams, and Web ReadableStream response bodies back to Node.js Readable streams. Additionally, it ensures proxy and TLS options are correctly passed to the fetch implementation. The review feedback highlights an issue in #createBunFetch where returning early when __googleCloudBunFetch is defined bypasses the stream conversion logic. The reviewer suggests resolving a fetchImpl reference to ensure stream conversions are consistently applied and tested.

Comment on lines +695 to +699
const bunFetch = (globalThis as {__googleCloudBunFetch?: typeof fetch})
.__googleCloudBunFetch;
if (typeof bunFetch === 'function') {
return bunFetch(input, init);
}

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.

high

The current implementation returns early when __googleCloudBunFetch is defined, completely bypassing the request and response stream conversion logic. According to the PR description, the intention is to delegate to globalThis.fetch (or globalThis.__googleCloudBunFetch when --fetch-shim is active) while still performing the stream conversions. By resolving a fetchImpl reference instead of returning early, we can ensure that the stream conversion logic is consistently applied and fully tested under the test suite's --fetch-shim environment.

      const bunFetch = (globalThis as {__googleCloudBunFetch?: typeof fetch})
        .__googleCloudBunFetch;
      const fetchImpl = typeof bunFetch === 'function' ? bunFetch : globalThis.fetch;

Comment on lines +731 to +734
const res = await globalThis.fetch(
input,
fetchInit as RequestInit | undefined,
);

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.

high

Use the resolved fetchImpl instead of hardcoding globalThis.fetch to ensure that the shimmed fetch is called when active, while still benefiting from the stream conversion logic.

      const res = await fetchImpl(
        input,
        fetchInit as RequestInit | undefined,
      );

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant