Skip to content

Commit

Permalink
feat: add optional error codes to networkError
Browse files Browse the repository at this point in the history
  • Loading branch information
GerkinDev committed Oct 30, 2024
1 parent 1b58f0f commit 1000937
Show file tree
Hide file tree
Showing 4 changed files with 183 additions and 47 deletions.
117 changes: 75 additions & 42 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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);
},
};

Expand Down
6 changes: 3 additions & 3 deletions src/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
101 changes: 101 additions & 0 deletions test/network_error.spec.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
const axios = require("axios");
const expect = require("chai").expect;
const http = require("http");

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

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

describe("Without code", function() {
it("mocks networkErrors", function () {
mock.onGet("/foo").networkError();

Expand Down Expand Up @@ -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");
});
});
});
6 changes: 4 additions & 2 deletions types/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ type ResponseSpecFunc = <T = any>(
headers?: AxiosHeaders
) => MockAdapter;

type NetErr = 'ENOTFOUND' | 'ECONNREFUSED' | 'ECONNRESET' | 'ECONNABORTED' | 'ETIMEDOUT'

declare namespace MockAdapter {
export interface RequestHandler {
withDelayInMs(delay: number): RequestHandler;
Expand All @@ -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;
}
Expand Down

0 comments on commit 1000937

Please sign in to comment.