2
0
mirror of https://github.com/tenrok/axios.git synced 2026-06-17 19:21:29 +03:00
Files
axios/tests/unit/adapters/fetch.test.js
T
Jay 9d92bcd326 fix: gadgets and smaller issues (#10833)
* chore: remove un-needed ghsa in the comments of files

* fix: auth header

* fix: escape regex chars in cookies.read

* fix: read-side merge and descriptors

* fix: enable redaction in the .toJson for errors

* fix: general IPv4-mapped IPv6 normalization in NO_PROXY

* fix: added regression tests for scenarios already covered

* chore: remove un-needed comments

* fix: harden proxy host detection and error redaction

* fix: make form-data header change opt-in

* fix: apply suggestions form github review

* fix: cubic review

* fix: widen the regexs for matches

Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com>

* fix: smaller issue found by cubic

* fix: address prototype chain

* fix: update as per cubic

---------

Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com>
2026-05-02 12:40:59 +02:00

1141 lines
32 KiB
JavaScript

import { describe, it, vi } from 'vitest';
import assert from 'assert';
import {
startHTTPServer,
stopHTTPServer,
setTimeoutAsync,
makeReadableStream,
generateReadable,
makeEchoStream,
} from '../../setup/server.js';
import axios from '../../../index.js';
import utils from '../../../lib/utils.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';
import NodeFormData from 'form-data';
import { VERSION } from '../../../lib/env/data.js';
const SERVER_PORT = 8010;
const LOCAL_SERVER_URL = `http://localhost:${SERVER_PORT}`;
const pipelineAsync = util.promisify(stream.pipeline);
const fetchAxios = axios.create({
baseURL: LOCAL_SERVER_URL,
adapter: 'fetch',
});
const getFetchSignal = (input, init) => (init && init.signal) || (input && input.signal);
const createBrokenDOMExceptionLikeError = () =>
Object.defineProperties(
{},
{
name: {
get() {
throw new TypeError(
'The DOMException.name getter can only be used on instances of DOMException'
);
},
},
message: {
get() {
throw new TypeError(
'The DOMException.message getter can only be used on instances of DOMException'
);
},
},
}
);
describe.runIf(typeof fetch === 'function')('supports fetch with nodejs', () => {
it('should sanitize request headers containing CRLF characters', async () => {
const server = await startHTTPServer(
(req, res) => {
res.setHeader('Content-Type', 'application/json');
res.end(
JSON.stringify({
xTest: req.headers['x-test'],
injected: req.headers.injected ?? null,
})
);
},
{
port: SERVER_PORT,
}
);
try {
const { data } = await fetchAxios.get(`${LOCAL_SERVER_URL}/`, {
headers: {
'x-test': '\tok\r\nInjected: yes ',
},
});
assert.strictEqual(data.xTest, 'okInjected: yes');
assert.strictEqual(data.injected, null);
} finally {
await stopHTTPServer(server);
}
});
describe('responses', () => {
it('should support text response type', async () => {
const originalData = 'my data';
const server = await startHTTPServer((req, res) => res.end(originalData), {
port: SERVER_PORT,
});
try {
const { data } = await fetchAxios.get(`http://localhost:${server.address().port}/`, {
responseType: 'text',
});
assert.deepStrictEqual(data, originalData);
} finally {
await stopHTTPServer(server);
}
});
it('should support arraybuffer response type', async () => {
const originalData = 'my data';
const server = await startHTTPServer((req, res) => res.end(originalData), {
port: SERVER_PORT,
});
try {
const { data } = await fetchAxios.get(`http://localhost:${server.address().port}/`, {
responseType: 'arraybuffer',
});
assert.deepStrictEqual(
data,
Uint8Array.from(await new TextEncoder().encode(originalData)).buffer
);
} finally {
await stopHTTPServer(server);
}
});
it('should support blob response type', async () => {
const originalData = 'my data';
const server = await startHTTPServer((req, res) => res.end(originalData), {
port: SERVER_PORT,
});
try {
const { data } = await fetchAxios.get(`http://localhost:${server.address().port}/`, {
responseType: 'blob',
});
assert.deepStrictEqual(data, new Blob([originalData]));
} finally {
await stopHTTPServer(server);
}
});
it('should support stream response type', async () => {
const originalData = 'my data';
const server = await startHTTPServer((req, res) => res.end(originalData), {
port: SERVER_PORT,
});
try {
const { data } = await fetchAxios.get(`http://localhost:${server.address().port}/`, {
responseType: 'stream',
});
assert.ok(data instanceof ReadableStream, 'data is not instanceof ReadableStream');
const response = new Response(data);
assert.deepStrictEqual(await response.text(), originalData);
} finally {
await stopHTTPServer(server);
}
});
it('should support formData response type', async () => {
const originalData = new FormData();
originalData.append('x', '123');
const server = await startHTTPServer(
async (req, res) => {
const response = await new Response(originalData);
res.setHeader('Content-Type', response.headers.get('Content-Type'));
res.end(await response.text());
},
{ port: SERVER_PORT }
);
try {
const { data } = await fetchAxios.get(`http://localhost:${server.address().port}/`, {
responseType: 'formdata',
});
assert.ok(data instanceof FormData, 'data is not instanceof FormData');
assert.deepStrictEqual(
Object.fromEntries(data.entries()),
Object.fromEntries(originalData.entries())
);
} finally {
await stopHTTPServer(server);
}
}, 5000);
it('should support json response type', async () => {
const originalData = { x: 'my data' };
const server = await startHTTPServer((req, res) => res.end(JSON.stringify(originalData)), {
port: SERVER_PORT,
});
try {
const { data } = await fetchAxios.get(`http://localhost:${server.address().port}/`, {
responseType: 'json',
});
assert.deepStrictEqual(data, originalData);
} finally {
await stopHTTPServer(server);
}
});
});
describe('progress', () => {
describe('upload', () => {
it('should support upload progress capturing', async () => {
const server = await startHTTPServer(
{
rate: 100 * 1024,
},
{ port: SERVER_PORT }
);
try {
let content = '';
const count = 10;
const chunk = 'test';
const chunkLength = Buffer.byteLength(chunk);
const contentLength = count * chunkLength;
const readable = stream.Readable.from(
(async function* () {
let i = count;
while (i-- > 0) {
await setTimeoutAsync(1100);
content += chunk;
yield chunk;
}
})()
);
const samples = [];
const { data } = await fetchAxios.post(
`http://localhost:${server.address().port}/`,
readable,
{
onUploadProgress: ({ loaded, total, progress, bytes, upload }) => {
console.log(
`Upload Progress ${loaded} from ${total} bytes (${(progress * 100).toFixed(1)}%)`
);
samples.push({
loaded,
total,
progress,
bytes,
upload,
});
},
headers: {
'Content-Length': contentLength,
},
responseType: 'text',
}
);
await setTimeoutAsync(500);
assert.strictEqual(data, content);
assert.deepStrictEqual(
samples,
Array.from(
(function* () {
for (let i = 1; i <= 10; i++) {
yield {
loaded: chunkLength * i,
total: contentLength,
progress: (chunkLength * i) / contentLength,
bytes: 4,
upload: true,
};
}
})()
)
);
} finally {
await stopHTTPServer(server);
}
}, 15000);
it('should not fail with get method', async () => {
const server = await startHTTPServer((req, res) => res.end('OK'), { port: SERVER_PORT });
try {
const { data } = await fetchAxios.get(`http://localhost:${server.address().port}/`, {
onUploadProgress() {},
});
assert.strictEqual(data, 'OK');
} finally {
await stopHTTPServer(server);
}
});
});
describe('download', () => {
it('should support download progress capturing', async () => {
const server = await startHTTPServer(
{
rate: 100 * 1024,
},
{
port: SERVER_PORT,
}
);
try {
let content = '';
const count = 10;
const chunk = 'test';
const chunkLength = Buffer.byteLength(chunk);
const contentLength = count * chunkLength;
const readable = stream.Readable.from(
(async function* () {
let i = count;
while (i-- > 0) {
await setTimeoutAsync(1100);
content += chunk;
yield chunk;
}
})()
);
const samples = [];
const { data } = await fetchAxios.post(
`http://localhost:${server.address().port}/`,
readable,
{
onDownloadProgress: ({ loaded, total, progress, bytes, download }) => {
console.log(
`Download Progress ${loaded} from ${total} bytes (${(progress * 100).toFixed(1)}%)`
);
samples.push({
loaded,
total,
progress,
bytes,
download,
});
},
headers: {
'Content-Length': contentLength,
},
responseType: 'text',
maxRedirects: 0,
}
);
await setTimeoutAsync(500);
assert.strictEqual(data, content);
assert.deepStrictEqual(
samples,
Array.from(
(function* () {
for (let i = 1; i <= 10; i++) {
yield {
loaded: chunkLength * i,
total: contentLength,
progress: (chunkLength * i) / contentLength,
bytes: 4,
download: true,
};
}
})()
)
);
} finally {
await stopHTTPServer(server);
}
}, 15000);
});
});
it('should support basic auth', async () => {
const server = await startHTTPServer((req, res) => res.end(req.headers.authorization), {
port: SERVER_PORT,
});
try {
const user = 'foo';
const headers = { Authorization: 'Bearer 1234' };
const res = await axios.get(`http://${user}@localhost:${server.address().port}/`, {
headers,
});
const base64 = Buffer.from(`${user}:`, 'utf8').toString('base64');
assert.equal(res.data, `Basic ${base64}`);
} finally {
await stopHTTPServer(server);
}
});
it('should support stream.Readable as a payload', async () => {
const server = await startHTTPServer(async (req, res) => res.end('OK'), { port: SERVER_PORT });
try {
const { data } = await fetchAxios.post(
`http://localhost:${server.address().port}/`,
stream.Readable.from('OK')
);
assert.strictEqual(data, 'OK');
} finally {
await stopHTTPServer(server);
}
});
describe('request aborting', () => {
it('should be able to abort the request stream', async () => {
const server = await startHTTPServer(
{
rate: 100000,
useBuffering: true,
},
{ port: SERVER_PORT }
);
try {
const controller = new AbortController();
setTimeout(() => {
controller.abort();
}, 500);
await assert.rejects(async () => {
await fetchAxios.post(
`http://localhost:${server.address().port}/`,
makeReadableStream(),
{
responseType: 'stream',
signal: controller.signal,
}
);
}, /CanceledError/);
} finally {
await stopHTTPServer(server);
}
});
it('should be able to abort the response stream', async () => {
const server = await startHTTPServer(
(req, res) => {
pipelineAsync(generateReadable(10000, 10), res).catch(() => {
// Client-side abort intentionally closes the stream early in this test.
});
},
{ port: SERVER_PORT }
);
try {
const controller = new AbortController();
setTimeout(() => {
controller.abort(new Error('test'));
}, 800);
const { data } = await fetchAxios.get(`http://localhost:${server.address().port}/`, {
responseType: 'stream',
signal: controller.signal,
});
await assert.rejects(async () => {
await data.pipeTo(makeEchoStream());
}, /^(AbortError|CanceledError):/);
} finally {
await stopHTTPServer(server);
}
});
});
it('should support a timeout', async () => {
const server = await startHTTPServer(
async (req, res) => {
await setTimeoutAsync(1000);
res.end('OK');
},
{ port: 0 }
);
try {
const timeout = 500;
const ts = Date.now();
await assert.rejects(async () => {
await fetchAxios(`http://localhost:${server.address().port}/`, {
timeout,
});
}, /timeout/);
const passed = Date.now() - ts;
assert.ok(passed >= timeout - 5, `early cancellation detected (${passed} ms)`);
} finally {
await stopHTTPServer(server);
}
});
describe('fetch adapter - timeout normalization', () => {
it('should reject with an AxiosError(ETIMEDOUT) on timeout', async () => {
const server = await startHTTPServer(
async (req, res) => {
await setTimeoutAsync(1000);
res.end('OK');
},
{ port: 0 }
);
try {
await assert.rejects(
() =>
fetchAxios(`http://localhost:${server.address().port}/`, {
timeout: 200,
}),
(err) => {
assert.strictEqual(err.name, 'AxiosError');
assert.strictEqual(err.code, 'ETIMEDOUT');
assert.match(err.message, /timeout of 200ms exceeded/);
return true;
}
);
} finally {
await stopHTTPServer(server);
}
});
it('should not classify a user-initiated abort as a timeout', async () => {
const safariFetch = (url, init) => {
const signal = getFetchSignal(url, init);
return new Promise((_resolve, reject) => {
const onAbort = () => {
signal.removeEventListener('abort', onAbort);
reject(createBrokenDOMExceptionLikeError());
};
if (signal.aborted) return onAbort();
signal.addEventListener('abort', onAbort);
});
};
const controller = new AbortController();
const request = fetchAxios.get('/', {
signal: controller.signal,
env: { fetch: safariFetch },
});
controller.abort();
await assert.rejects(
() => request,
(err) => {
assert.strictEqual(err.name, 'CanceledError');
assert.strictEqual(err.code, 'ERR_CANCELED');
assert.strictEqual(axios.isCancel(err), true);
return true;
}
);
});
// Timing-sensitive: a 50ms abort race observed by a fake fetch can flake
// under CI runner load even though the production code is fine. Retry as
// a backstop.
it(
'should surface ETIMEDOUT when fetch rejects with a broken DOMException on abort (Safari)',
{ retry: 2 },
async () => {
const safariFetch = (url, init) => {
const signal = getFetchSignal(url, init);
return new Promise((_resolve, reject) => {
const onAbort = () => {
signal.removeEventListener('abort', onAbort);
reject(createBrokenDOMExceptionLikeError());
};
if (signal.aborted) return onAbort();
signal.addEventListener('abort', onAbort);
});
};
await assert.rejects(
() =>
fetchAxios.get('/', {
timeout: 50,
env: { fetch: safariFetch },
}),
(err) => {
assert.strictEqual(err.name, 'AxiosError');
assert.strictEqual(err.code, 'ETIMEDOUT');
assert.match(err.message, /timeout of 50ms exceeded/);
return true;
}
);
}
);
});
it('should combine baseURL and url', async () => {
const server = await startHTTPServer(async (req, res) => res.end('OK'), { port: SERVER_PORT });
try {
const res = await fetchAxios('/foo');
assert.equal(res.config.baseURL, LOCAL_SERVER_URL);
assert.equal(res.config.url, '/foo');
} finally {
await stopHTTPServer(server);
}
});
it('should send QUERY requests with a body through the fetch adapter', async () => {
const server = await startHTTPServer(
(req, res) => {
let body = '';
req.on('data', (chunk) => {
body += chunk;
});
req.on('end', () => {
res.setHeader('Content-Type', 'application/json');
res.end(JSON.stringify({ method: req.method, url: req.url, body }));
});
},
{ port: 0 }
);
try {
const { data } = await fetchAxios.query(`http://localhost:${server.address().port}/search`, {
selector: 'field1',
});
assert.strictEqual(data.method, 'QUERY');
assert.strictEqual(data.url, '/search');
assert.deepStrictEqual(JSON.parse(data.body), { selector: 'field1' });
} finally {
await stopHTTPServer(server);
}
});
it('should support params', async () => {
const server = await startHTTPServer((req, res) => res.end(req.url), { port: SERVER_PORT });
try {
const { data } = await fetchAxios.get(`http://localhost:${server.address().port}/?test=1`, {
params: {
foo: 1,
bar: 2,
},
});
assert.strictEqual(data, '/?test=1&foo=1&bar=2');
} finally {
await stopHTTPServer(server);
}
});
it('should handle fetch failed error as an AxiosError with ERR_NETWORK code', async () => {
try {
await fetchAxios('http://notExistsUrl.in.nowhere');
assert.fail('should fail');
} catch (err) {
assert.strictEqual(String(err), 'AxiosError: Network Error');
assert.strictEqual(err.cause && err.cause.code, 'ENOTFOUND');
}
});
it('should get response headers', async () => {
const server = await startHTTPServer(
(req, res) => {
res.setHeader('foo', 'bar');
res.end(req.url);
},
{ port: SERVER_PORT }
);
try {
const { headers } = await fetchAxios.get(`http://localhost:${server.address().port}/`, {
responseType: 'stream',
});
assert.strictEqual(headers.get('foo'), 'bar');
} finally {
await stopHTTPServer(server);
}
});
describe('fetch adapter - Content-Type handling', () => {
it('should set correct Content-Type for FormData automatically', async () => {
const form = new NodeFormData();
form.append('foo', 'bar');
const server = await startHTTPServer(
(req, res) => {
const contentType = req.headers['content-type'];
assert.match(contentType, /^multipart\/form-data; boundary=/i);
res.end('OK');
},
{ port: SERVER_PORT }
);
try {
await fetchAxios.post(`http://localhost:${server.address().port}/form`, form);
} finally {
await stopHTTPServer(server);
}
});
it('should remove manually set Content-Type without boundary for FormData', async () => {
const form = new FormData();
form.append('foo', 'bar');
const server = await startHTTPServer(
(req, res) => {
const contentType = req.headers['content-type'];
assert.match(contentType, /^multipart\/form-data; boundary=/i);
res.end('OK');
},
{ port: SERVER_PORT }
);
try {
await fetchAxios.post(`http://localhost:${server.address().port}/form`, form, {
headers: { 'Content-Type': 'multipart/form-data' },
});
} finally {
await stopHTTPServer(server);
}
});
it('should preserve Content-Type if it already has boundary', async () => {
const form = new FormData();
form.append('foo', 'bar');
const customBoundary = '----CustomBoundary123';
const server = await startHTTPServer(
(req, res) => {
const contentType = req.headers['content-type'];
assert.ok(contentType.includes(customBoundary));
res.end('OK');
},
{ port: SERVER_PORT }
);
try {
await fetchAxios.post(`http://localhost:${server.address().port}/form`, form, {
headers: {
'Content-Type': `multipart/form-data; boundary=${customBoundary}`,
},
});
} finally {
await stopHTTPServer(server);
}
});
});
describe('fetch adapter - User-Agent header', () => {
it('should set User-Agent header to axios/<version> by default', async () => {
const server = await startHTTPServer(
(req, res) => {
res.setHeader('Content-Type', 'application/json');
res.end(JSON.stringify({ userAgent: req.headers['user-agent'] }));
},
{ port: SERVER_PORT }
);
try {
const { data } = await fetchAxios.post(`http://localhost:${server.address().port}/`, {
payload: 'test',
});
assert.strictEqual(data.userAgent, `axios/${VERSION}`);
} finally {
await stopHTTPServer(server);
}
});
it('should not override a user-provided User-Agent header', async () => {
const customUA = 'my-custom-agent/1.0';
const server = await startHTTPServer(
(req, res) => {
res.setHeader('Content-Type', 'application/json');
res.end(JSON.stringify({ userAgent: req.headers['user-agent'] }));
},
{ port: SERVER_PORT }
);
try {
const { data } = await fetchAxios.post(
`http://localhost:${server.address().port}/`,
{ payload: 'test' },
{ headers: { 'User-Agent': customUA } }
);
assert.strictEqual(data.userAgent, customUA);
} finally {
await stopHTTPServer(server);
}
});
});
describe('env config', () => {
it('should fallback to globalThis when utils.global is temporarily undefined', () => {
const originalGlobal = utils.global;
try {
utils.global = undefined;
assert.doesNotThrow(() =>
getFetch({
env: {
fetch() {},
},
})
);
} finally {
utils.global = originalGlobal;
}
});
it('should respect env fetch API configuration', async () => {
const { data, headers } = await fetchAxios.get('/', {
env: {
fetch() {
return {
headers: {
foo: '1',
},
text: async () => 'test',
};
},
},
});
assert.strictEqual(headers.get('foo'), '1');
assert.strictEqual(data, 'test');
});
it('should be able to request with lack of Request object', async () => {
const form = new FormData();
form.append('x', '1');
const { data, headers } = await fetchAxios.post('/', form, {
onUploadProgress() {
// dummy listener to activate streaming
},
env: {
Request: null,
fetch() {
return {
headers: {
foo: '1',
},
text: async () => 'test',
};
},
},
});
assert.strictEqual(headers.get('foo'), '1');
assert.strictEqual(data, 'test');
});
it('should be able to handle response with lack of Response object', async () => {
const { data, headers } = await fetchAxios.get('/', {
onDownloadProgress() {
// dummy listener to activate streaming
},
env: {
Request: null,
Response: null,
fetch() {
return {
headers: {
foo: '1',
},
text: async () => 'test',
};
},
},
});
assert.strictEqual(headers.get('foo'), '1');
assert.strictEqual(data, 'test');
});
it('should fallback to the global on undefined env value', async () => {
const server = await startHTTPServer((req, res) => res.end('OK'), { port: SERVER_PORT });
try {
const { data } = await fetchAxios.get(`http://localhost:${server.address().port}/`, {
env: {
fetch: undefined,
},
});
assert.strictEqual(data, 'OK');
} finally {
await stopHTTPServer(server);
}
});
it('should use current global fetch when env fetch is not specified', async () => {
const globalFetch = global.fetch;
vi.stubGlobal('fetch', async () => {
return {
headers: {
foo: '1',
},
text: async () => 'global',
};
});
const server = await startHTTPServer((req, res) => res.end('OK'), { port: SERVER_PORT });
try {
const { data } = await fetchAxios.get(`http://localhost:${server.address().port}/`, {
env: {
fetch: undefined,
},
});
assert.strictEqual(data, 'global');
} finally {
vi.stubGlobal('fetch', globalFetch);
await stopHTTPServer(server);
}
});
});
describe('size limits', () => {
it('should reject an outbound body that exceeds maxBodyLength with ERR_BAD_REQUEST', async () => {
const server = await startHTTPServer(
(req, res) => {
res.end('ok');
},
{ port: SERVER_PORT }
);
try {
await assert.rejects(
fetchAxios.post(`${LOCAL_SERVER_URL}/`, 'A'.repeat(2048), {
maxBodyLength: 1024,
}),
(err) => {
assert.strictEqual(err.code, 'ERR_BAD_REQUEST');
assert.match(err.message, /Request body larger than maxBodyLength limit/);
return true;
}
);
} finally {
await stopHTTPServer(server);
}
});
it('should reject a response whose Content-Length exceeds maxContentLength with ERR_BAD_RESPONSE', async () => {
const payload = 'A'.repeat(8 * 1024);
const server = await startHTTPServer(
(req, res) => {
res.setHeader('Content-Length', Buffer.byteLength(payload));
res.end(payload);
},
{ port: SERVER_PORT }
);
try {
await assert.rejects(
fetchAxios.get(`${LOCAL_SERVER_URL}/`, {
maxContentLength: 1024,
}),
(err) => {
assert.strictEqual(err.code, 'ERR_BAD_RESPONSE');
assert.match(err.message, /maxContentLength size of 1024 exceeded/);
return true;
}
);
} finally {
await stopHTTPServer(server);
}
});
it('should reject a chunked response that exceeds maxContentLength during streaming', async () => {
const server = await startHTTPServer(
(req, res) => {
// Omit content-length so the cheap pre-check cannot fire; force
// the stream-based enforcement path.
res.setHeader('Transfer-Encoding', 'chunked');
const chunk = 'B'.repeat(1024);
let sent = 0;
const writeNext = () => {
if (sent >= 8) {
return res.end();
}
sent++;
res.write(chunk, writeNext);
};
writeNext();
},
{ port: SERVER_PORT }
);
try {
await assert.rejects(
fetchAxios.get(`${LOCAL_SERVER_URL}/`, {
maxContentLength: 512,
}),
(err) => {
assert.strictEqual(err.code, 'ERR_BAD_RESPONSE');
assert.match(err.message, /maxContentLength size of 512 exceeded/);
return true;
}
);
} finally {
await stopHTTPServer(server);
}
});
it('should reject a data: URL whose decoded size exceeds maxContentLength (base64)', async () => {
const payload = 'A'.repeat(4096);
const dataUrl =
'data:application/octet-stream;base64,' + Buffer.from(payload).toString('base64');
// Use a dedicated instance without baseURL — combineURLs would otherwise
// prepend baseURL to a data: URL and neutralise the pre-check.
const bareAxios = axios.create({ adapter: 'fetch' });
await assert.rejects(bareAxios.get(dataUrl, { maxContentLength: 16 }), (err) => {
assert.strictEqual(err.code, 'ERR_BAD_RESPONSE');
assert.match(err.message, /maxContentLength size of 16 exceeded/);
return true;
});
});
it('should reject a data: URL whose body size exceeds maxContentLength (non-base64)', async () => {
const dataUrl = 'data:text/plain,' + 'X'.repeat(4096);
const bareAxios = axios.create({ adapter: 'fetch' });
await assert.rejects(bareAxios.get(dataUrl, { maxContentLength: 16 }), (err) => {
assert.strictEqual(err.code, 'ERR_BAD_RESPONSE');
assert.match(err.message, /maxContentLength size of 16 exceeded/);
return true;
});
});
it('should allow a response at or below maxContentLength', async () => {
const payload = 'ok';
const server = await startHTTPServer(
(req, res) => {
res.end(payload);
},
{ port: SERVER_PORT }
);
try {
const { data } = await fetchAxios.get(`${LOCAL_SERVER_URL}/`, {
maxContentLength: 1024,
});
assert.strictEqual(data, payload);
} finally {
await stopHTTPServer(server);
}
});
it('should allow a body at or below maxBodyLength', async () => {
const payload = 'hello';
let received;
const server = await startHTTPServer(
(req, res) => {
const chunks = [];
req.on('data', (c) => chunks.push(c));
req.on('end', () => {
received = Buffer.concat(chunks).toString();
res.end('ok');
});
},
{ port: SERVER_PORT }
);
try {
await fetchAxios.post(`${LOCAL_SERVER_URL}/`, payload, {
maxBodyLength: 1024,
});
assert.strictEqual(received, payload);
} finally {
await stopHTTPServer(server);
}
});
});
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();
}
});
});
});