diff --git a/lib/adapters/fetch.js b/lib/adapters/fetch.js index 99dedbcc..6588ddf0 100644 --- a/lib/adapters/fetch.js +++ b/lib/adapters/fetch.js @@ -66,8 +66,10 @@ const factory = (env) => { test(() => { let duplexAccessed = false; + const body = new ReadableStream(); + const hasContentType = new Request(platform.origin, { - body: new ReadableStream(), + body, method: 'POST', get duplex() { duplexAccessed = true; @@ -75,6 +77,8 @@ const factory = (env) => { }, }).headers.has('Content-Type'); + body.cancel(); + return duplexAccessed && !hasContentType; }); diff --git a/tests/unit/adapters/fetch.test.js b/tests/unit/adapters/fetch.test.js index d6de2ff3..8b1bc3fd 100644 --- a/tests/unit/adapters/fetch.test.js +++ b/tests/unit/adapters/fetch.test.js @@ -9,6 +9,7 @@ import { makeEchoStream, } from '../../setup/server.js'; import axios from '../../../index.js'; +import { getFetch } from '../../../lib/adapters/fetch.js'; import stream from 'stream'; import { AbortController } from 'abortcontroller-polyfill/dist/cjs-ponyfill.js'; import util from 'util'; @@ -652,4 +653,32 @@ describe.runIf(typeof fetch === 'function')('supports fetch with nodejs', () => } }); }); + + describe('capability probe cleanup', () => { + it('should cancel the ReadableStream created during the request stream probe', () => { + // The fetch adapter factory probes for request-stream support by creating + // a ReadableStream as a Request body. Previously the stream was never + // cancelled, leaving a dangling pull-algorithm promise (async resource leak + // visible via `--detect-async-leaks` or Node.js async_hooks). + // + // Calling getFetch with a unique env triggers a fresh factory() execution + // (including the probe). We spy on ReadableStream.prototype.cancel to + // verify it is invoked during the probe. + + const cancelSpy = vi.spyOn(ReadableStream.prototype, 'cancel'); + + try { + // Unique fetch function ensures cache miss → factory() re-runs the probe. + const uniqueFetch = async () => new Response('ok'); + getFetch({ env: { fetch: uniqueFetch } }); + + assert.ok( + cancelSpy.mock.calls.length > 0, + 'ReadableStream.prototype.cancel should be called during the capability probe' + ); + } finally { + cancelSpy.mockRestore(); + } + }); + }); });