Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add optional error codes to networkError #384

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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' | (string & Record<never, never>)

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