Skip to content

Commit 78b290c

Browse files
authored
feat(adapter): surface low‑level network error details; attach original error via cause (#6982)
* feat(adapter): surface low‑level network error details; attach original error via `cause` Node http adapter: - Promote low-level `err.code` to `AxiosError.code`, prefixing message (e.g. `ECONNREFUSED – …`) - Keep original error on standard `Error.cause` XHR adapter: - Preserve browser `ProgressEvent` on `error.event` - Use event message when available Tests: - Add Node ESM tests under `test/unit/adapters` to assert `code` and `cause` behavior Types: - Ensure `AxiosError.cause?: unknown` and `event?: ProgressEvent` are present * fix(adapter): use fs instead of fs/promises for sync file read in tests to fix GitHub Actions
1 parent 2c2a56a commit 78b290c

File tree

5 files changed

+111
-15
lines changed

5 files changed

+111
-15
lines changed

index.d.cts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ type ContentType = axios.AxiosHeaderValue | 'text/html' | 'text/plain' | 'multip
1616

1717
type CommonResponseHeadersList = 'Server' | 'Content-Type' | 'Content-Length' | 'Cache-Control'| 'Content-Encoding';
1818

19+
type BrowserProgressEvent = any;
20+
1921
declare class AxiosHeaders {
2022
constructor(
2123
headers?: RawAxiosHeaders | AxiosHeaders | string
@@ -98,7 +100,8 @@ declare class AxiosError<T = unknown, D = any> extends Error {
98100
isAxiosError: boolean;
99101
status?: number;
100102
toJSON: () => object;
101-
cause?: Error;
103+
cause?: unknown;
104+
event?: BrowserProgressEvent;
102105
static from<T = unknown, D = any>(
103106
error: Error | unknown,
104107
code?: string,
@@ -352,8 +355,6 @@ declare namespace axios {
352355

353356
type MaxDownloadRate = number;
354357

355-
type BrowserProgressEvent = any;
356-
357358
interface AxiosProgressEvent {
358359
loaded: number;
359360
total?: number;

index.d.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -418,7 +418,8 @@ export class AxiosError<T = unknown, D = any> extends Error {
418418
isAxiosError: boolean;
419419
status?: number;
420420
toJSON: () => object;
421-
cause?: Error;
421+
cause?: unknown;
422+
event?: BrowserProgressEvent;
422423
static from<T = unknown, D = any>(
423424
error: Error | unknown,
424425
code?: string,

lib/adapters/xhr.js

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -104,15 +104,18 @@ export default isXHRAdapterSupported && function (config) {
104104
};
105105

106106
// Handle low level network errors
107-
request.onerror = function handleError() {
108-
// Real errors are hidden from us by the browser
109-
// onerror should only fire if it's a network error
110-
reject(new AxiosError('Network Error', AxiosError.ERR_NETWORK, config, request));
111-
112-
// Clean up request
113-
request = null;
107+
request.onerror = function handleError(event) {
108+
// Browsers deliver a ProgressEvent in XHR onerror
109+
// (message may be empty; when present, surface it)
110+
// See https://developer.mozilla.org/docs/Web/API/XMLHttpRequest/error_event
111+
const msg = event && event.message ? event.message : 'Network Error';
112+
const err = new AxiosError(msg, AxiosError.ERR_NETWORK, config, request);
113+
// attach the underlying event for consumers who want details
114+
err.event = event || null;
115+
reject(err);
116+
request = null;
114117
};
115-
118+
116119
// Handle timeout
117120
request.ontimeout = function handleTimeout() {
118121
let timeoutErrorMessage = _config.timeout ? 'timeout of ' + _config.timeout + 'ms exceeded' : 'timeout exceeded';

lib/core/AxiosError.js

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -89,11 +89,18 @@ AxiosError.from = (error, code, config, request, response, customProps) => {
8989
return prop !== 'isAxiosError';
9090
});
9191

92-
AxiosError.call(axiosError, error.message, code, config, request, response);
92+
const msg = error && error.message ? error.message : 'Error';
9393

94-
axiosError.cause = error;
94+
// Prefer explicit code; otherwise copy the low-level error's code (e.g. ECONNREFUSED)
95+
const errCode = code == null && error ? error.code : code;
96+
AxiosError.call(axiosError, msg, errCode, config, request, response);
9597

96-
axiosError.name = error.name;
98+
// Chain the original error on the standard field; non-enumerable to avoid JSON noise
99+
if (error && axiosError.cause == null) {
100+
Object.defineProperty(axiosError, 'cause', { value: error, configurable: true });
101+
}
102+
103+
axiosError.name = (error && error.name) || 'Error';
97104

98105
customProps && Object.assign(axiosError, customProps);
99106

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
/* eslint-env mocha */
2+
import assert from 'assert';
3+
import https from 'https';
4+
import net from 'net';
5+
import fs from 'fs';
6+
import path from 'path';
7+
import {fileURLToPath} from 'url';
8+
import axios from '../../../index.js';
9+
10+
/** __dirname replacement for ESM */
11+
const __filename = fileURLToPath(import.meta.url);
12+
const __dirname = path.dirname(__filename);
13+
14+
/** Get a port that will refuse connections: bind to a random port and close it. */
15+
async function getClosedPort() {
16+
return await new Promise((resolve) => {
17+
const srv = net.createServer();
18+
srv.listen(0, '127.0.0.1', () => {
19+
const {port} = srv.address();
20+
srv.close(() => resolve(port));
21+
});
22+
});
23+
}
24+
25+
describe('adapters – network-error details', function () {
26+
this.timeout(5000);
27+
28+
it('should expose ECONNREFUSED and set error.cause on connection refusal', async function () {
29+
const port = await getClosedPort();
30+
31+
try {
32+
await axios.get(`http://127.0.0.1:${port}`, { timeout: 500 });
33+
assert.fail('request unexpectedly succeeded');
34+
} catch (err) {
35+
assert.ok(err instanceof Error, 'should be an Error');
36+
assert.strictEqual(err.isAxiosError, true, 'isAxiosError should be true');
37+
38+
// New behavior: Node error code is surfaced and original error is linked via cause
39+
assert.strictEqual(err.code, 'ECONNREFUSED');
40+
assert.ok('cause' in err, 'error.cause should exist');
41+
assert.ok(err.cause instanceof Error, 'cause should be an Error');
42+
assert.strictEqual(err.cause && err.cause.code, 'ECONNREFUSED');
43+
44+
// Message remains a string (content may include the code prefix)
45+
assert.strictEqual(typeof err.message, 'string');
46+
}
47+
});
48+
49+
it('should expose self-signed TLS error and set error.cause', async function () {
50+
// Use the same certs already present for adapter tests in this folder
51+
const keyPath = path.join(__dirname, 'key.pem');
52+
const certPath = path.join(__dirname, 'cert.pem');
53+
54+
const key = fs.readFileSync(keyPath);
55+
const cert = fs.readFileSync(certPath);
56+
57+
const httpsServer = https.createServer({ key, cert }, (req, res) => res.end('ok'));
58+
59+
await new Promise((resolve) => httpsServer.listen(0, '127.0.0.1', resolve));
60+
const {port} = httpsServer.address();
61+
62+
try {
63+
await axios.get(`https://127.0.0.1:${port}`, {
64+
timeout: 500,
65+
httpsAgent: new https.Agent({ rejectUnauthorized: true }) // Explicit: reject self-signed
66+
});
67+
assert.fail('request unexpectedly succeeded');
68+
} catch (err) {
69+
const codeStr = String(err.code);
70+
// OpenSSL/Node variants: SELF_SIGNED_CERT_IN_CHAIN, DEPTH_ZERO_SELF_SIGNED_CERT, UNABLE_TO_VERIFY_LEAF_SIGNATURE
71+
assert.ok(/SELF_SIGNED|UNABLE_TO_VERIFY_LEAF_SIGNATURE|DEPTH_ZERO/.test(codeStr), 'unexpected TLS code: ' + codeStr);
72+
73+
assert.ok('cause' in err, 'error.cause should exist');
74+
assert.ok(err.cause instanceof Error, 'cause should be an Error');
75+
76+
const causeCode = String(err.cause && err.cause.code);
77+
assert.ok(/SELF_SIGNED|UNABLE_TO_VERIFY_LEAF_SIGNATURE|DEPTH_ZERO/.test(causeCode), 'unexpected cause code: ' + causeCode);
78+
79+
assert.strictEqual(typeof err.message, 'string');
80+
} finally {
81+
await new Promise((resolve) => httpsServer.close(resolve));
82+
}
83+
});
84+
});

0 commit comments

Comments
 (0)