Thanks to visit codestin.com
Credit goes to github.com

Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions lib/adapters/fetch.js
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
4 changes: 2 additions & 2 deletions tests/smoke/bun/tests/timeout.smoke.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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');
});
});
119 changes: 118 additions & 1 deletion tests/unit/adapters/fetch.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -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 {
Expand All @@ -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 {
Expand Down
Loading