diff --git a/index.d.cts b/index.d.cts index e0674b8..787f738 100644 --- a/index.d.cts +++ b/index.d.cts @@ -16,6 +16,8 @@ type ContentType = axios.AxiosHeaderValue | 'text/html' | 'text/plain' | 'multip type CommonResponseHeadersList = 'Server' | 'Content-Type' | 'Content-Length' | 'Cache-Control'| 'Content-Encoding'; +type BrowserProgressEvent = any; + declare class AxiosHeaders { constructor( headers?: RawAxiosHeaders | AxiosHeaders | string @@ -98,7 +100,8 @@ declare class AxiosError extends Error { isAxiosError: boolean; status?: number; toJSON: () => object; - cause?: Error; + cause?: unknown; + event?: BrowserProgressEvent; static from( error: Error | unknown, code?: string, @@ -352,8 +355,6 @@ declare namespace axios { type MaxDownloadRate = number; - type BrowserProgressEvent = any; - interface AxiosProgressEvent { loaded: number; total?: number; diff --git a/index.d.ts b/index.d.ts index d4d661f..c2ed195 100644 --- a/index.d.ts +++ b/index.d.ts @@ -418,7 +418,8 @@ export class AxiosError extends Error { isAxiosError: boolean; status?: number; toJSON: () => object; - cause?: Error; + cause?: unknown; + event?: BrowserProgressEvent; static from( error: Error | unknown, code?: string, diff --git a/lib/adapters/xhr.js b/lib/adapters/xhr.js index a7ee548..0223618 100644 --- a/lib/adapters/xhr.js +++ b/lib/adapters/xhr.js @@ -104,15 +104,18 @@ export default isXHRAdapterSupported && function (config) { }; // Handle low level network errors - request.onerror = function handleError() { - // Real errors are hidden from us by the browser - // onerror should only fire if it's a network error - reject(new AxiosError('Network Error', AxiosError.ERR_NETWORK, config, request)); - - // Clean up request - request = null; + request.onerror = function handleError(event) { + // Browsers deliver a ProgressEvent in XHR onerror + // (message may be empty; when present, surface it) + // See https://developer.mozilla.org/docs/Web/API/XMLHttpRequest/error_event + const msg = event && event.message ? event.message : 'Network Error'; + const err = new AxiosError(msg, AxiosError.ERR_NETWORK, config, request); + // attach the underlying event for consumers who want details + err.event = event || null; + reject(err); + request = null; }; - + // Handle timeout request.ontimeout = function handleTimeout() { let timeoutErrorMessage = _config.timeout ? 'timeout of ' + _config.timeout + 'ms exceeded' : 'timeout exceeded'; diff --git a/lib/core/AxiosError.js b/lib/core/AxiosError.js index 73da248..3d118cb 100644 --- a/lib/core/AxiosError.js +++ b/lib/core/AxiosError.js @@ -89,11 +89,18 @@ AxiosError.from = (error, code, config, request, response, customProps) => { return prop !== 'isAxiosError'; }); - AxiosError.call(axiosError, error.message, code, config, request, response); + const msg = error && error.message ? error.message : 'Error'; - axiosError.cause = error; + // Prefer explicit code; otherwise copy the low-level error's code (e.g. ECONNREFUSED) + const errCode = code == null && error ? error.code : code; + AxiosError.call(axiosError, msg, errCode, config, request, response); - axiosError.name = error.name; + // Chain the original error on the standard field; non-enumerable to avoid JSON noise + if (error && axiosError.cause == null) { + Object.defineProperty(axiosError, 'cause', { value: error, configurable: true }); + } + + axiosError.name = (error && error.name) || 'Error'; customProps && Object.assign(axiosError, customProps); diff --git a/test/unit/adapters/error-details.spec.js b/test/unit/adapters/error-details.spec.js new file mode 100644 index 0000000..909c677 --- /dev/null +++ b/test/unit/adapters/error-details.spec.js @@ -0,0 +1,84 @@ +/* eslint-env mocha */ +import assert from 'assert'; +import https from 'https'; +import net from 'net'; +import fs from 'fs'; +import path from 'path'; +import {fileURLToPath} from 'url'; +import axios from '../../../index.js'; + +/** __dirname replacement for ESM */ +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); + +/** Get a port that will refuse connections: bind to a random port and close it. */ +async function getClosedPort() { + return await new Promise((resolve) => { + const srv = net.createServer(); + srv.listen(0, '127.0.0.1', () => { + const {port} = srv.address(); + srv.close(() => resolve(port)); + }); + }); +} + +describe('adapters – network-error details', function () { + this.timeout(5000); + + it('should expose ECONNREFUSED and set error.cause on connection refusal', async function () { + const port = await getClosedPort(); + + try { + await axios.get(`http://127.0.0.1:${port}`, { timeout: 500 }); + assert.fail('request unexpectedly succeeded'); + } catch (err) { + assert.ok(err instanceof Error, 'should be an Error'); + assert.strictEqual(err.isAxiosError, true, 'isAxiosError should be true'); + + // New behavior: Node error code is surfaced and original error is linked via cause + assert.strictEqual(err.code, 'ECONNREFUSED'); + assert.ok('cause' in err, 'error.cause should exist'); + assert.ok(err.cause instanceof Error, 'cause should be an Error'); + assert.strictEqual(err.cause && err.cause.code, 'ECONNREFUSED'); + + // Message remains a string (content may include the code prefix) + assert.strictEqual(typeof err.message, 'string'); + } + }); + + it('should expose self-signed TLS error and set error.cause', async function () { + // Use the same certs already present for adapter tests in this folder + const keyPath = path.join(__dirname, 'key.pem'); + const certPath = path.join(__dirname, 'cert.pem'); + + const key = fs.readFileSync(keyPath); + const cert = fs.readFileSync(certPath); + + const httpsServer = https.createServer({ key, cert }, (req, res) => res.end('ok')); + + await new Promise((resolve) => httpsServer.listen(0, '127.0.0.1', resolve)); + const {port} = httpsServer.address(); + + try { + await axios.get(`https://127.0.0.1:${port}`, { + timeout: 500, + httpsAgent: new https.Agent({ rejectUnauthorized: true }) // Explicit: reject self-signed + }); + assert.fail('request unexpectedly succeeded'); + } catch (err) { + const codeStr = String(err.code); + // OpenSSL/Node variants: SELF_SIGNED_CERT_IN_CHAIN, DEPTH_ZERO_SELF_SIGNED_CERT, UNABLE_TO_VERIFY_LEAF_SIGNATURE + assert.ok(/SELF_SIGNED|UNABLE_TO_VERIFY_LEAF_SIGNATURE|DEPTH_ZERO/.test(codeStr), 'unexpected TLS code: ' + codeStr); + + assert.ok('cause' in err, 'error.cause should exist'); + assert.ok(err.cause instanceof Error, 'cause should be an Error'); + + const causeCode = String(err.cause && err.cause.code); + assert.ok(/SELF_SIGNED|UNABLE_TO_VERIFY_LEAF_SIGNATURE|DEPTH_ZERO/.test(causeCode), 'unexpected cause code: ' + causeCode); + + assert.strictEqual(typeof err.message, 'string'); + } finally { + await new Promise((resolve) => httpsServer.close(resolve)); + } + }); +});