diff --git a/src/index.js b/src/index.js index b27afb0..cf30d13 100644 --- a/src/index.js +++ b/src/index.js @@ -15,6 +15,66 @@ const VERBS = [ "unlink", ]; +function throwNetErrorFactory(code) { + if (typeof code === "string") { + return function (config) { + let url = { hostname: "UNKNOWN", host: "UNKNOWN" }; + try { + url = new URL(config.url, config.baseURL); + } catch (_error) {} + let error = undefined; + switch (code) { + case "ENOTFOUND": { + error = utils.createAxiosError(`getaddrinfo ENOTFOUND ${url.hostname}`, config, undefined, "ENOTFOUND", { + errno: -3008, + code: "ENOTFOUND", + syscall: "getaddrinfo", + hostname: url.hostname, + } + ); + } break; + + case "ECONNREFUSED": { + error = utils.createAxiosError(`connect ECONNREFUSED ${url.host}`, config, undefined, "ECONNREFUSED", { + code: "ECONNREFUSED", + syscall: "connect", + port: url.port ? parseInt(url.port, 10) : undefined, + address: url.hostname, + errno: -111 + } + ); + } break; + + case "ECONNRESET": { + error = utils.createAxiosError("socket hang up", config, undefined, "ECONNRESET", { code: "ECONNRESET" }); + } break; + + case "ECONNABORTED": + case "ETIMEDOUT": { + error = Object.assign(utils.createAxiosError( + config.timeoutErrorMessage || + `timeout of ${config.timeout}ms exceeded`, + config, + undefined, + config.transitional && config.transitional.clarifyTimeoutError + ? "ETIMEDOUT" + : "ECONNABORTED" + ), { name: "AxiosError" }); + } break; + + default: { + error = utils.createAxiosError(`Error ${code}`, config, undefined, code); + } break; + } + return Promise.reject(error); + }; + } else { + return function (config) { + return Promise.reject(utils.createAxiosError("Network Error", config)); + }; + } +} + function getVerbArray() { const arr = []; VERBS.forEach(function (verb) { @@ -177,64 +237,37 @@ VERBS.concat("any").forEach(function (method) { return self; }, abortRequest () { + const throwNetError = throwNetErrorFactory("ECONNABORTED"); return reply(async function (config) { - throw utils.createAxiosError( - "Request aborted", - config, - undefined, - "ECONNABORTED" - ); + return throwNetError(Object.assign({ timeoutErrorMessage: "Request aborted" }, config)); }); }, abortRequestOnce () { + const throwNetError = throwNetErrorFactory("ECONNABORTED"); + return replyOnce(async function (config) { - throw utils.createAxiosError( - "Request aborted", - config, - undefined, - "ECONNABORTED" - ); + return throwNetError(Object.assign({ timeoutErrorMessage: "Request aborted" }, config)); }); }, - networkError () { - return reply(async function (config) { - throw utils.createAxiosError("Network Error", config); - }); + networkError (code) { + const throwNetError = throwNetErrorFactory(code); + return reply(throwNetError); }, - networkErrorOnce () { - return replyOnce(async function (config) { - throw utils.createAxiosError("Network Error", config); - }); + networkErrorOnce (code) { + const throwNetError = throwNetErrorFactory(code); + return replyOnce(throwNetError); }, timeout () { - return reply(async function (config) { - throw utils.createAxiosError( - config.timeoutErrorMessage || - `timeout of ${config.timeout }ms exceeded`, - config, - undefined, - config.transitional && config.transitional.clarifyTimeoutError - ? "ETIMEDOUT" - : "ECONNABORTED" - ); - }); + const throwNetError = throwNetErrorFactory("ETIMEDOUT"); + return reply(throwNetError); }, timeoutOnce () { - return replyOnce(async function (config) { - throw utils.createAxiosError( - config.timeoutErrorMessage || - `timeout of ${config.timeout }ms exceeded`, - config, - undefined, - config.transitional && config.transitional.clarifyTimeoutError - ? "ETIMEDOUT" - : "ECONNABORTED" - ); - }); + const throwNetError = throwNetErrorFactory("ETIMEDOUT"); + return replyOnce(throwNetError); }, }; diff --git a/src/utils.js b/src/utils.js index 51f5285..209a4a9 100644 --- a/src/utils.js +++ b/src/utils.js @@ -155,14 +155,14 @@ async function settle(config, response, delay) { } } -function createAxiosError(message, config, response, code) { +function createAxiosError(message, config, response, code, cause) { // axios v0.27.0+ defines AxiosError as constructor if (typeof axios.AxiosError === "function") { - return axios.AxiosError.from(new Error(message), code, config, null, response); + return axios.AxiosError.from(Object.assign(new Error(message), cause), code, config, null, response); } // handling for axios v0.26.1 and below - const error = new Error(message); + const error = Object.assign(new Error(message), cause); error.isAxiosError = true; error.config = config; if (response !== undefined) { diff --git a/test/network_error.spec.js b/test/network_error.spec.js index e7408e4..41d711e 100644 --- a/test/network_error.spec.js +++ b/test/network_error.spec.js @@ -1,5 +1,6 @@ const axios = require("axios"); const expect = require("chai").expect; +const http = require("http"); const MockAdapter = require("../src"); @@ -12,6 +13,7 @@ describe("networkError spec", function () { mock = new MockAdapter(instance); }); + describe("Without code", function() { it("mocks networkErrors", function () { mock.onGet("/foo").networkError(); @@ -42,5 +44,104 @@ describe("networkError spec", function () { .then(function (response) { expect(response.status).to.equal(200); }); + }); + }); + describe("With code", function () { + function filterErrorKeys(key) { + return key !== "config" && key !== "request" && key !== "stack"; + } + + async function compareErrors() { + const url = arguments[0]; + const params = Array.from(arguments).slice(1); + const errors = await Promise.all([ + axios.get.apply(axios, [instance.defaults.baseURL + url].concat(params)).then(function () { + expect.fail("Should have rejected"); + }, function (error) { + return error; + }), + instance.get.apply(instance, [url].concat(params)).then(function () { + expect.fail("Should have rejected"); + }, function (error) { + return error; + }) + ]); + const base = errors[0]; + const mocked = errors[1]; + const baseKeys = Object.keys(base).filter(filterErrorKeys); + for (let i = 0; i < baseKeys.length; i++) { + const key = baseKeys[i]; + expect(mocked[key], `Property ${key}`).to.deep.equal(base[key]); + } + } + + it("should look like base axios ENOTFOUND responses", function() { + instance.defaults.baseURL = "https://not-exi.st:1234"; + mock.onGet("/some-url").networkError("ENOTFOUND"); + + return compareErrors("/some-url"); + }); + + it("should look like base axios ECONNREFUSED responses", function() { + instance.defaults.baseURL = "http://127.0.0.1:4321"; + mock.onGet("/some-url").networkError("ECONNREFUSED"); + + return compareErrors("/some-url"); + }); + + it("should look like base axios ECONNRESET responses", function() { + return new Promise(function(resolve) { + const server = http.createServer(function(request) { + request.destroy(); + }).listen(function() { + resolve(server); + }); + }).then(function(server) { + instance.defaults.baseURL = `http://localhost:${ server.address().port}`; + mock.onGet("/some-url").networkError("ECONNRESET"); + + return compareErrors("/some-url").finally(function() { + server.close(); + }); + }); + }); + + it("should look like base axios ECONNABORTED responses", function() { + return new Promise(function(resolve) { + const server = http.createServer(function() {}).listen(function() { + resolve(server); + }); + }).then(function(server) { + instance.defaults.baseURL = `http://localhost:${ server.address().port}`; + mock.onGet("/some-url").networkError("ECONNABORTED"); + + return compareErrors("/some-url", { timeout: 1 }).finally(function() { + server.close(); + }); + }); + }); + + it("should look like base axios ETIMEDOUT responses", function() { + return new Promise(function(resolve) { + const server = http.createServer(function() {}).listen(function() { + resolve(server); + }); + }).then(function(server) { + instance.defaults.baseURL = `http://localhost:${ server.address().port}`; + mock.onGet("/some-url").networkError("ETIMEDOUT"); + + return compareErrors("/some-url", { timeout: 1 }).finally(function() { + server.close(); + }); + }); + }); + + // Did not found a way to simulate this + it.skip("should look like base axios EHOSTUNREACH responses", function() { + instance.defaults.baseURL = "TODO"; + mock.onGet("/some-url").networkError("EHOSTUNREACH"); + + return compareErrors("/some-url"); + }); }); }); diff --git a/types/index.d.ts b/types/index.d.ts index 01f0ec8..0a971c3 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -29,6 +29,8 @@ type ResponseSpecFunc = ( headers?: AxiosHeaders ) => MockAdapter; +type NetErr = 'ENOTFOUND' | 'ECONNREFUSED' | 'ECONNRESET' | 'ECONNABORTED' | 'ETIMEDOUT' + declare namespace MockAdapter { export interface RequestHandler { withDelayInMs(delay: number): RequestHandler; @@ -37,8 +39,8 @@ declare namespace MockAdapter { passThrough(): MockAdapter; abortRequest(): MockAdapter; abortRequestOnce(): MockAdapter; - networkError(): MockAdapter; - networkErrorOnce(): MockAdapter; + networkError(code?: NetErr): MockAdapter; + networkErrorOnce(code?: NetErr): MockAdapter; timeout(): MockAdapter; timeoutOnce(): MockAdapter; }