mirror of
https://github.com/tenrok/axios.git
synced 2026-06-17 19:21:29 +03:00
fix(fetch): preserve abort reasons in fetch adapter (#10806)
Based on the work in axios/axios#7191. Co-authored-by: Mostafa-Khairy0 <mostafakhairy0305@gmail.com> Co-authored-by: Jason Saayman <jasonsaayman@gmail.com>
This commit is contained in:
@@ -409,6 +409,17 @@ const factory = (env) => {
|
||||
} catch (err) {
|
||||
unsubscribe && unsubscribe();
|
||||
|
||||
// Safari can surface fetch aborts as a DOMException-like object whose
|
||||
// branded getters throw. Prefer our composed signal reason before reading
|
||||
// the caught error, preserving timeout vs cancellation semantics.
|
||||
if (composedSignal && composedSignal.aborted && composedSignal.reason instanceof AxiosError) {
|
||||
const canceledError = composedSignal.reason;
|
||||
canceledError.config = config;
|
||||
request && (canceledError.request = request);
|
||||
err !== canceledError && (canceledError.cause = err);
|
||||
throw canceledError;
|
||||
}
|
||||
|
||||
if (err && err.name === 'TypeError' && /Load failed|fetch/i.test(err.message)) {
|
||||
throw Object.assign(
|
||||
new AxiosError(
|
||||
|
||||
@@ -15,7 +15,7 @@ const createAbortedError = () => {
|
||||
};
|
||||
|
||||
describe('timeout', () => {
|
||||
test('timeout: 50 with never-resolving fetch mock rejects with ECONNABORTED', async () => {
|
||||
test('timeout: 50 with never-resolving fetch mock rejects with ETIMEDOUT', async () => {
|
||||
const fetch = (input: unknown, init?: RequestInit) =>
|
||||
new Promise<Response>((_resolve, reject) => {
|
||||
const signal = init?.signal || (input instanceof Request ? input.signal : undefined);
|
||||
@@ -45,6 +45,6 @@ describe('timeout', () => {
|
||||
.catch((e: any) => e);
|
||||
|
||||
expect(axios.isAxiosError(err)).toBe(true);
|
||||
expect(err.code).toBe('ECONNABORTED');
|
||||
expect(err.code).toBe('ETIMEDOUT');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -26,6 +26,29 @@ const fetchAxios = axios.create({
|
||||
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(
|
||||
@@ -470,7 +493,7 @@ describe.runIf(typeof fetch === 'function')('supports fetch with nodejs', () =>
|
||||
await setTimeoutAsync(1000);
|
||||
res.end('OK');
|
||||
},
|
||||
{ port: SERVER_PORT }
|
||||
{ port: 0 }
|
||||
);
|
||||
|
||||
try {
|
||||
@@ -492,6 +515,100 @@ describe.runIf(typeof fetch === 'function')('supports fetch with nodejs', () =>
|
||||
}
|
||||
});
|
||||
|
||||
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;
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
it('should surface ETIMEDOUT when fetch rejects with a broken DOMException on abort (Safari)', 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 {
|
||||
|
||||
Reference in New Issue
Block a user