Support for program checkable error by introducing custom errors with error types/codes

This finally closes #7
This commit is contained in:
Christian Pekeler 2016-04-05 07:20:42 -06:00 committed by David Frank
parent 98b167c35d
commit b6f3913499
4 changed files with 72 additions and 17 deletions

View File

@ -16,6 +16,7 @@ var Body = require('./lib/body');
var Response = require('./lib/response');
var Headers = require('./lib/headers');
var Request = require('./lib/request');
var FetchError = require('./lib/fetch-error');
// commonjs
module.exports = Fetch;
@ -114,14 +115,14 @@ function Fetch(url, opts) {
req.once('socket', function(socket) {
reqTimeout = setTimeout(function() {
req.abort();
reject(new Error('network timeout at: ' + options.url));
reject(new FetchError('network timeout at: ' + options.url, 'socket-timeout'));
}, options.timeout);
});
}
req.on('error', function(err) {
clearTimeout(reqTimeout);
reject(new Error('request to ' + options.url + ' failed, reason: ' + err.message));
reject(new FetchError('request to ' + options.url + ' failed, reason: ' + err.message, 'system', err));
});
req.on('response', function(res) {
@ -130,12 +131,12 @@ function Fetch(url, opts) {
// handle redirect
if (self.isRedirect(res.statusCode)) {
if (options.counter >= options.follow) {
reject(new Error('maximum redirect reached at: ' + options.url));
reject(new FetchError('maximum redirect reached at: ' + options.url, 'max-redirect'));
return;
}
if (!res.headers.location) {
reject(new Error('redirect location header missing at: ' + options.url));
reject(new FetchError('redirect location header missing at: ' + options.url, 'invalid-redirect'));
return;
}

View File

@ -7,6 +7,7 @@
var convert = require('encoding').convert;
var bodyStream = require('is-stream');
var PassThrough = require('stream').PassThrough;
var FetchError = require('./fetch-error');
module.exports = Body;
@ -91,13 +92,13 @@ Body.prototype._decode = function() {
if (self.timeout) {
resTimeout = setTimeout(function() {
self._abort = true;
reject(new Error('response timeout at ' + self.url + ' over limit: ' + self.timeout));
reject(new FetchError('response timeout at ' + self.url + ' over limit: ' + self.timeout, 'body-timeout'));
}, self.timeout);
}
// handle stream error, such as incorrect content-encoding
self.body.on('error', function(err) {
reject(new Error('invalid response body at: ' + self.url + ' reason: ' + err.message));
reject(new FetchError('invalid response body at: ' + self.url + ' reason: ' + err.message, 'system', err));
});
self.body.on('data', function(chunk) {
@ -107,7 +108,7 @@ Body.prototype._decode = function() {
if (self.size && self._bytes + chunk.length > self.size) {
self._abort = true;
reject(new Error('content size at ' + self.url + ' over limit: ' + self.size));
reject(new FetchError('content size at ' + self.url + ' over limit: ' + self.size, 'max-size'));
return;
}

25
lib/fetch-error.js Normal file
View File

@ -0,0 +1,25 @@
/**
* fetch-error.js
*
* FetchError class for operational errors
*/
module.exports = FetchError;
/**
* Create FetchError
*
* @param String reason String type Error optionalSystemError
* @return FetchError
*/
function FetchError(message, type, optionalSystemError) {
Error.captureStackTrace(this, this.constructor);
this.name = this.constructor.name;
this.message = message;
this.type = type;
if (optionalSystemError) {
this.code = this.errno = optionalSystemError.code;
}
}
require('util').inherits(FetchError, Error);

View File

@ -20,6 +20,7 @@ var Headers = require('../lib/headers.js');
var Response = require('../lib/response.js');
var Request = require('../lib/request.js');
var Body = require('../lib/body.js');
var FetchError = require('../lib/fetch-error.js');
// test with native promise on node 0.11, and bluebird for node 0.10
fetch.Promise = fetch.Promise || bluebird;
@ -86,7 +87,9 @@ describe('node-fetch', function() {
it('should reject with error on network failure', function() {
url = 'http://localhost:50000/';
return expect(fetch(url)).to.eventually.be.rejectedWith(Error);
return expect(fetch(url)).to.eventually.be.rejected
.and.be.an.instanceOf(FetchError)
.and.include({type: 'system', code: 'ECONNREFUSED', errno: 'ECONNREFUSED'});
});
it('should resolve into response', function() {
@ -280,7 +283,9 @@ describe('node-fetch', function() {
opts = {
follow: 1
}
return expect(fetch(url, opts)).to.eventually.be.rejectedWith(Error);
return expect(fetch(url, opts)).to.eventually.be.rejected
.and.be.an.instanceOf(FetchError)
.and.have.property('type', 'max-redirect');
});
it('should allow not following redirect', function() {
@ -288,7 +293,9 @@ describe('node-fetch', function() {
opts = {
follow: 0
}
return expect(fetch(url, opts)).to.eventually.be.rejectedWith(Error);
return expect(fetch(url, opts)).to.eventually.be.rejected
.and.be.an.instanceOf(FetchError)
.and.have.property('type', 'max-redirect');
});
it('should follow redirect code 301 and keep existing headers', function() {
@ -306,7 +313,9 @@ describe('node-fetch', function() {
it('should reject broken redirect', function() {
url = base + '/error/redirect';
return expect(fetch(url)).to.eventually.be.rejectedWith(Error);
return expect(fetch(url)).to.eventually.be.rejected
.and.be.an.instanceOf(FetchError)
.and.have.property('type', 'invalid-redirect');
});
it('should handle client-error response', function() {
@ -341,7 +350,16 @@ describe('node-fetch', function() {
it('should handle network-error response', function() {
url = base + '/error/reset';
return expect(fetch(url)).to.eventually.be.rejectedWith(Error);
return expect(fetch(url)).to.eventually.be.rejected
.and.be.an.instanceOf(FetchError)
.and.have.property('code', 'ECONNRESET');
});
it('should handle DNS-error response', function() {
url = 'http://invalid.commm';
return expect(fetch(url)).to.eventually.be.rejected
.and.be.an.instanceOf(FetchError)
.and.have.property('code', 'ENOTFOUND');
});
it('should reject invalid json response', function() {
@ -402,7 +420,9 @@ describe('node-fetch', function() {
url = base + '/invalid-content-encoding';
return fetch(url).then(function(res) {
expect(res.headers.get('content-type')).to.equal('text/plain');
return expect(res.text()).to.eventually.be.rejectedWith(Error);
return expect(res.text()).to.eventually.be.rejected
.and.be.an.instanceOf(FetchError)
.and.have.property('code', 'Z_DATA_ERROR');
});
});
@ -426,7 +446,9 @@ describe('node-fetch', function() {
opts = {
timeout: 100
};
return expect(fetch(url, opts)).to.eventually.be.rejectedWith(Error);
return expect(fetch(url, opts)).to.eventually.be.rejected
.and.be.an.instanceOf(FetchError)
.and.have.property('type', 'socket-timeout');
});
it('should allow custom timeout on response body', function() {
@ -437,7 +459,9 @@ describe('node-fetch', function() {
};
return fetch(url, opts).then(function(res) {
expect(res.ok).to.be.true;
return expect(res.text()).to.eventually.be.rejectedWith(Error);
return expect(res.text()).to.eventually.be.rejected
.and.be.an.instanceOf(FetchError)
.and.have.property('type', 'body-timeout');
});
});
@ -639,7 +663,9 @@ describe('node-fetch', function() {
return fetch(url, opts).then(function(res) {
expect(res.status).to.equal(200);
expect(res.headers.get('content-type')).to.equal('text/plain');
return expect(res.text()).to.eventually.be.rejectedWith(Error);
return expect(res.text()).to.eventually.be.rejected
.and.be.an.instanceOf(FetchError)
.and.have.property('type', 'max-size');
});
});
@ -651,7 +677,9 @@ describe('node-fetch', function() {
return fetch(url, opts).then(function(res) {
expect(res.status).to.equal(200);
expect(res.headers.get('content-type')).to.equal('text/plain');
return expect(res.text()).to.eventually.be.rejectedWith(Error);
return expect(res.text()).to.eventually.be.rejected
.and.be.an.instanceOf(FetchError)
.and.have.property('type', 'max-size');
});
});