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 Apr 19, 2024
1 parent 4492f3c commit b983f62
Show file tree
Hide file tree
Showing 3 changed files with 214 additions and 76 deletions.
130 changes: 81 additions & 49 deletions src/index.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
"use strict";

var axios = require("axios");
var handleRequest = require("./handle_request");
var utils = require("./utils");

Expand Down Expand Up @@ -32,6 +33,71 @@ function getVerbObject() {
}, {});
}

function throwNetErrorFactory(code) {
if (typeof code === 'string') {
return function (config) {
var url = { hostname: 'UNKNOWN', host: 'UNKNOWN' };
try {
url = new URL(config.url, config.baseURL);
} catch (error) {}
var error = undefined;
switch (code) {
case 'ENOTFOUND': {
error = Object.assign(
utils.createAxiosError('getaddrinfo ENOTFOUND ' + url.hostname, config, undefined, 'ENOTFOUND'),
{
syscall: 'getaddrinfo',
hostname: url.hostname,
errno: -3008,
}
);
} break;

case 'ECONNREFUSED': {
error = Object.assign(
utils.createAxiosError('connect ECONNREFUSED ' + url.host, config, undefined, '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, code);
} 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: axios.AxiosError.name }
);
} 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 reset() {
resetHandlers.call(this);
resetHistory.call(this);
Expand Down Expand Up @@ -131,72 +197,38 @@ VERBS.concat("any").forEach(function (method) {
},

abortRequest: function () {
var throwNetError = throwNetErrorFactory('ECONNABORTED');
return reply(function (config) {
var error = utils.createAxiosError(
"Request aborted",
config,
undefined,
"ECONNABORTED"
);
return Promise.reject(error);
return throwNetError(Object.assign({ timeoutErrorMessage: 'Request aborted' }, config));
});
},

abortRequestOnce: function () {
var throwNetError = throwNetErrorFactory('ECONNABORTED');
return replyOnce(function (config) {
var error = utils.createAxiosError(
"Request aborted",
config,
undefined,
"ECONNABORTED"
);
return Promise.reject(error);
return throwNetError(Object.assign({ timeoutErrorMessage: 'Request aborted' }, config));
});
},

networkError: function () {
return reply(function (config) {
var error = utils.createAxiosError("Network Error", config);
return Promise.reject(error);
});
networkError: function (code) {
var throwNetError = throwNetErrorFactory(code);
return reply(throwNetError);
},

networkErrorOnce: function () {
return replyOnce(function (config) {
var error = utils.createAxiosError("Network Error", config);
return Promise.reject(error);
});
networkErrorOnce: function (code) {
var throwNetError = throwNetErrorFactory(code);
return replyOnce(throwNetError);
},

timeout: function () {
return reply(function (config) {
var error = utils.createAxiosError(
config.timeoutErrorMessage ||
"timeout of " + config.timeout + "ms exceeded",
config,
undefined,
config.transitional && config.transitional.clarifyTimeoutError
? "ETIMEDOUT"
: "ECONNABORTED"
);
return Promise.reject(error);
});
var throwNetError = throwNetErrorFactory('ETIMEDOUT');
return reply(throwNetError);
},

timeoutOnce: function () {
return replyOnce(function (config) {
var error = utils.createAxiosError(
config.timeoutErrorMessage ||
"timeout of " + config.timeout + "ms exceeded",
config,
undefined,
config.transitional && config.transitional.clarifyTimeoutError
? "ETIMEDOUT"
: "ECONNABORTED"
);
return Promise.reject(error);
});
},
var throwNetError = throwNetErrorFactory('ETIMEDOUT');
return replyOnce(throwNetError);
}
};
};
});
Expand Down
154 changes: 129 additions & 25 deletions test/network_error.spec.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
var axios = require("axios");
var expect = require("chai").expect;
var http = require('http');

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

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

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

return instance.get("/foo").then(
function () {
expect.fail("should not be called");
},
function (error) {
expect(error.config).to.exist;
expect(error.response).to.not.exist;
expect(error.message).to.equal("Network Error");
expect(error.isAxiosError).to.be.true;
}
);
describe("Without code", function() {
it("mocks networkErrors", function () {
mock.onGet("/foo").networkError();

return instance.get("/foo").then(
function () {
expect.fail("should not be called");
},
function (error) {
expect(error.config).to.exist;
expect(error.response).to.not.exist;
expect(error.message).to.equal("Network Error");
expect(error.isAxiosError).to.be.true;
}
);
});

it("can mock a network error only once", function () {
mock.onGet("/foo").networkErrorOnce().onGet("/foo").reply(200);

return instance
.get("/foo")
.then(
function () {},
function () {
return instance.get("/foo");
}
)
.then(function (response) {
expect(response.status).to.equal(200);
});
});
});

it("can mock a network error only once", function () {
mock.onGet("/foo").networkErrorOnce().onGet("/foo").reply(200);
describe("With code", function () {
function filterErrorKeys(key) {
return key !== 'config' && key !== 'request' && key !== 'stack';
}

return instance
.get("/foo")
.then(
function () {},
function () {
return instance.get("/foo");
function compareErrors() {
var url = arguments[0];
var params = Array.from(arguments).slice(1);
return 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;
})
]).then(function (errors) {
var base = errors[0];
var mocked = errors[1];

var baseKeys = Object.keys(base).filter(filterErrorKeys);
for (var i = 0; i < baseKeys.length; i++) {
var key = baseKeys[i];
expect(mocked[key], 'Property ' + key).to.equal(base[key]);
}
)
.then(function (response) {
expect(response.status).to.equal(200);
});
}

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) {
var 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) {
var 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) {
var 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 @@ -10,15 +10,17 @@ type ResponseSpecFunc = <T = any>(
headers?: any
) => MockAdapter;

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

declare namespace MockAdapter {
export interface RequestHandler {
reply: ResponseSpecFunc;
replyOnce: ResponseSpecFunc;
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 b983f62

Please sign in to comment.