mirror of
https://github.com/tenrok/axios.git
synced 2026-06-20 20:00:40 +03:00
fix(fetch): cancel ReadableStream body after request stream capability probe (#7515)
The module-level capability probe in the fetch adapter creates a ReadableStream as a Request body to test for streaming support, but never cancels it. The Request constructor sets up an internal pull pipeline on the stream; since the stream is never consumed or cancelled, the [[pullAlgorithm]] Promise remains pending indefinitely, causing an async resource leak detectable by Node.js async_hooks and Vitest --detect-async-leaks. Extract the ReadableStream to a variable and call body.cancel() after the probe completes to properly tear down the stream's internal pipeline.
This commit is contained in:
@@ -66,8 +66,10 @@ const factory = (env) => {
|
|||||||
test(() => {
|
test(() => {
|
||||||
let duplexAccessed = false;
|
let duplexAccessed = false;
|
||||||
|
|
||||||
|
const body = new ReadableStream();
|
||||||
|
|
||||||
const hasContentType = new Request(platform.origin, {
|
const hasContentType = new Request(platform.origin, {
|
||||||
body: new ReadableStream(),
|
body,
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
get duplex() {
|
get duplex() {
|
||||||
duplexAccessed = true;
|
duplexAccessed = true;
|
||||||
@@ -75,6 +77,8 @@ const factory = (env) => {
|
|||||||
},
|
},
|
||||||
}).headers.has('Content-Type');
|
}).headers.has('Content-Type');
|
||||||
|
|
||||||
|
body.cancel();
|
||||||
|
|
||||||
return duplexAccessed && !hasContentType;
|
return duplexAccessed && !hasContentType;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import {
|
|||||||
makeEchoStream,
|
makeEchoStream,
|
||||||
} from '../../setup/server.js';
|
} from '../../setup/server.js';
|
||||||
import axios from '../../../index.js';
|
import axios from '../../../index.js';
|
||||||
|
import { getFetch } from '../../../lib/adapters/fetch.js';
|
||||||
import stream from 'stream';
|
import stream from 'stream';
|
||||||
import { AbortController } from 'abortcontroller-polyfill/dist/cjs-ponyfill.js';
|
import { AbortController } from 'abortcontroller-polyfill/dist/cjs-ponyfill.js';
|
||||||
import util from 'util';
|
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();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user