Skip to content

Commit 1000937

Browse files
committed
feat: add optional error codes to networkError
Closes #365
1 parent 1b58f0f commit 1000937

File tree

4 files changed

+183
-47
lines changed

4 files changed

+183
-47
lines changed

src/index.js

Lines changed: 75 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,66 @@ const VERBS = [
1515
"unlink",
1616
];
1717

18+
function throwNetErrorFactory(code) {
19+
if (typeof code === "string") {
20+
return function (config) {
21+
let url = { hostname: "UNKNOWN", host: "UNKNOWN" };
22+
try {
23+
url = new URL(config.url, config.baseURL);
24+
} catch (_error) {}
25+
let error = undefined;
26+
switch (code) {
27+
case "ENOTFOUND": {
28+
error = utils.createAxiosError(`getaddrinfo ENOTFOUND ${url.hostname}`, config, undefined, "ENOTFOUND", {
29+
errno: -3008,
30+
code: "ENOTFOUND",
31+
syscall: "getaddrinfo",
32+
hostname: url.hostname,
33+
}
34+
);
35+
} break;
36+
37+
case "ECONNREFUSED": {
38+
error = utils.createAxiosError(`connect ECONNREFUSED ${url.host}`, config, undefined, "ECONNREFUSED", {
39+
code: "ECONNREFUSED",
40+
syscall: "connect",
41+
port: url.port ? parseInt(url.port, 10) : undefined,
42+
address: url.hostname,
43+
errno: -111
44+
}
45+
);
46+
} break;
47+
48+
case "ECONNRESET": {
49+
error = utils.createAxiosError("socket hang up", config, undefined, "ECONNRESET", { code: "ECONNRESET" });
50+
} break;
51+
52+
case "ECONNABORTED":
53+
case "ETIMEDOUT": {
54+
error = Object.assign(utils.createAxiosError(
55+
config.timeoutErrorMessage ||
56+
`timeout of ${config.timeout}ms exceeded`,
57+
config,
58+
undefined,
59+
config.transitional && config.transitional.clarifyTimeoutError
60+
? "ETIMEDOUT"
61+
: "ECONNABORTED"
62+
), { name: "AxiosError" });
63+
} break;
64+
65+
default: {
66+
error = utils.createAxiosError(`Error ${code}`, config, undefined, code);
67+
} break;
68+
}
69+
return Promise.reject(error);
70+
};
71+
} else {
72+
return function (config) {
73+
return Promise.reject(utils.createAxiosError("Network Error", config));
74+
};
75+
}
76+
}
77+
1878
function getVerbArray() {
1979
const arr = [];
2080
VERBS.forEach(function (verb) {
@@ -177,64 +237,37 @@ VERBS.concat("any").forEach(function (method) {
177237
return self;
178238
},
179239
abortRequest () {
240+
const throwNetError = throwNetErrorFactory("ECONNABORTED");
180241
return reply(async function (config) {
181-
throw utils.createAxiosError(
182-
"Request aborted",
183-
config,
184-
undefined,
185-
"ECONNABORTED"
186-
);
242+
return throwNetError(Object.assign({ timeoutErrorMessage: "Request aborted" }, config));
187243
});
188244
},
189245
abortRequestOnce () {
246+
const throwNetError = throwNetErrorFactory("ECONNABORTED");
247+
190248
return replyOnce(async function (config) {
191-
throw utils.createAxiosError(
192-
"Request aborted",
193-
config,
194-
undefined,
195-
"ECONNABORTED"
196-
);
249+
return throwNetError(Object.assign({ timeoutErrorMessage: "Request aborted" }, config));
197250
});
198251
},
199252

200-
networkError () {
201-
return reply(async function (config) {
202-
throw utils.createAxiosError("Network Error", config);
203-
});
253+
networkError (code) {
254+
const throwNetError = throwNetErrorFactory(code);
255+
return reply(throwNetError);
204256
},
205257

206-
networkErrorOnce () {
207-
return replyOnce(async function (config) {
208-
throw utils.createAxiosError("Network Error", config);
209-
});
258+
networkErrorOnce (code) {
259+
const throwNetError = throwNetErrorFactory(code);
260+
return replyOnce(throwNetError);
210261
},
211262

212263
timeout () {
213-
return reply(async function (config) {
214-
throw utils.createAxiosError(
215-
config.timeoutErrorMessage ||
216-
`timeout of ${config.timeout }ms exceeded`,
217-
config,
218-
undefined,
219-
config.transitional && config.transitional.clarifyTimeoutError
220-
? "ETIMEDOUT"
221-
: "ECONNABORTED"
222-
);
223-
});
264+
const throwNetError = throwNetErrorFactory("ETIMEDOUT");
265+
return reply(throwNetError);
224266
},
225267

226268
timeoutOnce () {
227-
return replyOnce(async function (config) {
228-
throw utils.createAxiosError(
229-
config.timeoutErrorMessage ||
230-
`timeout of ${config.timeout }ms exceeded`,
231-
config,
232-
undefined,
233-
config.transitional && config.transitional.clarifyTimeoutError
234-
? "ETIMEDOUT"
235-
: "ECONNABORTED"
236-
);
237-
});
269+
const throwNetError = throwNetErrorFactory("ETIMEDOUT");
270+
return replyOnce(throwNetError);
238271
},
239272
};
240273

src/utils.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -155,14 +155,14 @@ async function settle(config, response, delay) {
155155
}
156156
}
157157

158-
function createAxiosError(message, config, response, code) {
158+
function createAxiosError(message, config, response, code, cause) {
159159
// axios v0.27.0+ defines AxiosError as constructor
160160
if (typeof axios.AxiosError === "function") {
161-
return axios.AxiosError.from(new Error(message), code, config, null, response);
161+
return axios.AxiosError.from(Object.assign(new Error(message), cause), code, config, null, response);
162162
}
163163

164164
// handling for axios v0.26.1 and below
165-
const error = new Error(message);
165+
const error = Object.assign(new Error(message), cause);
166166
error.isAxiosError = true;
167167
error.config = config;
168168
if (response !== undefined) {

test/network_error.spec.js

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
const axios = require("axios");
22
const expect = require("chai").expect;
3+
const http = require("http");
34

45
const MockAdapter = require("../src");
56

@@ -12,6 +13,7 @@ describe("networkError spec", function () {
1213
mock = new MockAdapter(instance);
1314
});
1415

16+
describe("Without code", function() {
1517
it("mocks networkErrors", function () {
1618
mock.onGet("/foo").networkError();
1719

@@ -42,5 +44,104 @@ describe("networkError spec", function () {
4244
.then(function (response) {
4345
expect(response.status).to.equal(200);
4446
});
47+
});
48+
});
49+
describe("With code", function () {
50+
function filterErrorKeys(key) {
51+
return key !== "config" && key !== "request" && key !== "stack";
52+
}
53+
54+
async function compareErrors() {
55+
const url = arguments[0];
56+
const params = Array.from(arguments).slice(1);
57+
const errors = await Promise.all([
58+
axios.get.apply(axios, [instance.defaults.baseURL + url].concat(params)).then(function () {
59+
expect.fail("Should have rejected");
60+
}, function (error) {
61+
return error;
62+
}),
63+
instance.get.apply(instance, [url].concat(params)).then(function () {
64+
expect.fail("Should have rejected");
65+
}, function (error) {
66+
return error;
67+
})
68+
]);
69+
const base = errors[0];
70+
const mocked = errors[1];
71+
const baseKeys = Object.keys(base).filter(filterErrorKeys);
72+
for (let i = 0; i < baseKeys.length; i++) {
73+
const key = baseKeys[i];
74+
expect(mocked[key], `Property ${key}`).to.deep.equal(base[key]);
75+
}
76+
}
77+
78+
it("should look like base axios ENOTFOUND responses", function() {
79+
instance.defaults.baseURL = "https://not-exi.st:1234";
80+
mock.onGet("/some-url").networkError("ENOTFOUND");
81+
82+
return compareErrors("/some-url");
83+
});
84+
85+
it("should look like base axios ECONNREFUSED responses", function() {
86+
instance.defaults.baseURL = "http://127.0.0.1:4321";
87+
mock.onGet("/some-url").networkError("ECONNREFUSED");
88+
89+
return compareErrors("/some-url");
90+
});
91+
92+
it("should look like base axios ECONNRESET responses", function() {
93+
return new Promise(function(resolve) {
94+
const server = http.createServer(function(request) {
95+
request.destroy();
96+
}).listen(function() {
97+
resolve(server);
98+
});
99+
}).then(function(server) {
100+
instance.defaults.baseURL = `http://localhost:${ server.address().port}`;
101+
mock.onGet("/some-url").networkError("ECONNRESET");
102+
103+
return compareErrors("/some-url").finally(function() {
104+
server.close();
105+
});
106+
});
107+
});
108+
109+
it("should look like base axios ECONNABORTED responses", function() {
110+
return new Promise(function(resolve) {
111+
const server = http.createServer(function() {}).listen(function() {
112+
resolve(server);
113+
});
114+
}).then(function(server) {
115+
instance.defaults.baseURL = `http://localhost:${ server.address().port}`;
116+
mock.onGet("/some-url").networkError("ECONNABORTED");
117+
118+
return compareErrors("/some-url", { timeout: 1 }).finally(function() {
119+
server.close();
120+
});
121+
});
122+
});
123+
124+
it("should look like base axios ETIMEDOUT responses", function() {
125+
return new Promise(function(resolve) {
126+
const server = http.createServer(function() {}).listen(function() {
127+
resolve(server);
128+
});
129+
}).then(function(server) {
130+
instance.defaults.baseURL = `http://localhost:${ server.address().port}`;
131+
mock.onGet("/some-url").networkError("ETIMEDOUT");
132+
133+
return compareErrors("/some-url", { timeout: 1 }).finally(function() {
134+
server.close();
135+
});
136+
});
137+
});
138+
139+
// Did not found a way to simulate this
140+
it.skip("should look like base axios EHOSTUNREACH responses", function() {
141+
instance.defaults.baseURL = "TODO";
142+
mock.onGet("/some-url").networkError("EHOSTUNREACH");
143+
144+
return compareErrors("/some-url");
145+
});
45146
});
46147
});

types/index.d.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,8 @@ type ResponseSpecFunc = <T = any>(
2929
headers?: AxiosHeaders
3030
) => MockAdapter;
3131

32+
type NetErr = 'ENOTFOUND' | 'ECONNREFUSED' | 'ECONNRESET' | 'ECONNABORTED' | 'ETIMEDOUT'
33+
3234
declare namespace MockAdapter {
3335
export interface RequestHandler {
3436
withDelayInMs(delay: number): RequestHandler;
@@ -37,8 +39,8 @@ declare namespace MockAdapter {
3739
passThrough(): MockAdapter;
3840
abortRequest(): MockAdapter;
3941
abortRequestOnce(): MockAdapter;
40-
networkError(): MockAdapter;
41-
networkErrorOnce(): MockAdapter;
42+
networkError(code?: NetErr): MockAdapter;
43+
networkErrorOnce(code?: NetErr): MockAdapter;
4244
timeout(): MockAdapter;
4345
timeoutOnce(): MockAdapter;
4446
}

0 commit comments

Comments
 (0)