From 11680b1b68a59658975f72d0e553c3edc3657041 Mon Sep 17 00:00:00 2001 From: Vsevolod Strukchinsky Date: Sat, 18 Jul 2015 12:34:47 +0500 Subject: [PATCH] Explicit Error classes Closes #83 and closes #86 --- index.js | 69 +++++++++++++++++++++++++----------------- package.json | 2 +- readme.md | 25 +++++++++++++-- test/test-arguments.js | 7 ----- test/test-error.js | 10 +++--- test/test-gzip.js | 4 ++- test/test-helpers.js | 1 - test/test-http.js | 2 +- test/test-json.js | 12 +++----- test/test-redirects.js | 5 +-- test/test-stream.js | 1 - 11 files changed, 82 insertions(+), 56 deletions(-) diff --git a/index.js b/index.js index fb44c80..ae1cbe0 100644 --- a/index.js +++ b/index.js @@ -3,7 +3,6 @@ var EventEmitter = require('events').EventEmitter; var http = require('http'); var https = require('https'); var urlLib = require('url'); -var util = require('util'); var querystring = require('querystring'); var objectAssign = require('object-assign'); var duplexify = require('duplexify'); @@ -13,17 +12,9 @@ var timedOut = require('timed-out'); var prependHttp = require('prepend-http'); var lowercaseKeys = require('lowercase-keys'); var isRedirect = require('is-redirect'); -var NestedErrorStacks = require('nested-error-stacks'); var pinkiePromise = require('pinkie-promise'); var unzipResponse = require('unzip-response'); - -function GotError(message, nested) { - NestedErrorStacks.call(this, message, nested); - objectAssign(this, nested, {nested: this.nested}); -} - -util.inherits(GotError, NestedErrorStacks); -GotError.prototype.name = 'GotError'; +var createErrorClass = require('create-error-class'); function requestAsEventEmitter(opts) { opts = opts || {}; @@ -33,7 +24,6 @@ function requestAsEventEmitter(opts) { var get = function (opts) { var fn = opts.protocol === 'https:' ? https : http; - var url = urlLib.format(opts); var req = fn.request(opts, function (res) { var statusCode = res.statusCode; @@ -41,11 +31,11 @@ function requestAsEventEmitter(opts) { res.resume(); if (++redirectCount > 10) { - ee.emit('error', new GotError('Redirected 10 times. Aborting.'), undefined, res); + ee.emit('error', new got.MaxRedirectsError(statusCode, opts), null, res); return; } - var redirectUrl = urlLib.resolve(url, res.headers.location); + var redirectUrl = urlLib.resolve(urlLib.format(opts), res.headers.location); var redirectOpts = objectAssign({}, opts, urlLib.parse(redirectUrl)); ee.emit('redirect', res, redirectOpts); @@ -56,7 +46,7 @@ function requestAsEventEmitter(opts) { ee.emit('response', unzipResponse(res)); }).once('error', function (err) { - ee.emit('error', new GotError('Request to ' + url + ' failed', err)); + ee.emit('error', new got.RequestError(err, opts)); }); if (opts.timeout) { @@ -72,7 +62,6 @@ function requestAsEventEmitter(opts) { function asCallback(opts, cb) { var ee = requestAsEventEmitter(opts); - var url = urlLib.format(opts); ee.on('request', function (req) { if (isStream.readable(opts.body)) { @@ -87,22 +76,21 @@ function asCallback(opts, cb) { ee.on('response', function (res) { readAllStream(res, opts.encoding, function (err, data) { if (err) { - cb(new GotError('Reading ' + url + ' response failed', err), null, res); + cb(new got.ReadError(err, opts), null, res); return; } var statusCode = res.statusCode; if (statusCode < 200 || statusCode > 299) { - err = new GotError(opts.method + ' ' + url + ' response code is ' + statusCode + ' (' + http.STATUS_CODES[statusCode] + ')', err); - err.code = statusCode; + err = new got.HTTPError(statusCode, opts); } if (opts.json && statusCode !== 204) { try { data = JSON.parse(data); } catch (e) { - err = new GotError('Parsing ' + url + ' response failed', new GotError(e.message, err)); + err = new got.ParseError(e, opts); } } @@ -135,7 +123,7 @@ function asStream(opts) { var proxy = duplexify(); if (opts.json) { - throw new GotError('got can not be used as stream when options.json is used'); + throw new Error('got can not be used as stream when options.json is used'); } if (opts.body) { @@ -179,11 +167,11 @@ function asStream(opts) { function normalizeArguments(url, opts) { if (typeof url !== 'string' && typeof url !== 'object') { - throw new GotError('Parameter `url` must be a string or object, not ' + typeof url); + throw new Error('Parameter `url` must be a string or object, not ' + typeof url); } opts = objectAssign( - {protocol: 'http:'}, + {protocol: 'http:', path: ''}, typeof url === 'string' ? urlLib.parse(prependHttp(url)) : url, opts ); @@ -193,17 +181,13 @@ function normalizeArguments(url, opts) { 'accept-encoding': 'gzip,deflate' }, lowercaseKeys(opts.headers)); - if (opts.pathname) { - opts.path = opts.pathname; - } - var query = opts.query; if (query) { if (typeof query !== 'string') { opts.query = querystring.stringify(query); } - opts.path = opts.pathname + '?' + opts.query; + opts.path = opts.path.split('?')[0] + '?' + opts.query; delete opts.query; } @@ -214,7 +198,7 @@ function normalizeArguments(url, opts) { var body = opts.body; if (body) { if (typeof body !== 'string' && !Buffer.isBuffer(body) && !isStream.readable(body)) { - throw new GotError('options.body must be a ReadableStream, string or Buffer'); + throw new Error('options.body must be a ReadableStream, string or Buffer'); } opts.method = opts.method || 'POST'; @@ -276,4 +260,33 @@ helpers.forEach(function (el) { }; }); +function stdError(error, opts) { + objectAssign(this, { + message: error.message, + code: error.code, + host: opts.host, + hostname: opts.hostname, + method: opts.method, + path: opts.path + }); +} + +got.RequestError = createErrorClass('RequestError', stdError); +got.ReadError = createErrorClass('ReadError', stdError); +got.ParseError = createErrorClass('ParseError', stdError); + +got.HTTPError = createErrorClass('HTTPError', function (statusCode, opts) { + stdError.call(this, {}, opts); + this.statusCode = statusCode; + this.statusMessage = http.STATUS_CODES[this.statusCode]; + this.message = 'Response code ' + this.statusCode + ' (' + this.statusMessage + ')'; +}); + +got.MaxRedirectsError = createErrorClass('MaxRedirectsError', function (statusCode, opts) { + stdError.call(this, {}, opts); + this.statusCode = statusCode; + this.statusMessage = http.STATUS_CODES[this.statusCode]; + this.message = 'Redirected 10 times. Aborting.'; +}); + module.exports = got; diff --git a/package.json b/package.json index 78fbcef..0b7cb58 100644 --- a/package.json +++ b/package.json @@ -41,11 +41,11 @@ "fetch" ], "dependencies": { + "create-error-class": "^2.0.0", "duplexify": "^3.2.0", "is-redirect": "^1.0.0", "is-stream": "^1.0.0", "lowercase-keys": "^1.0.0", - "nested-error-stacks": "^1.0.0", "object-assign": "^3.0.0", "pinkie-promise": "^1.0.0", "prepend-http": "^1.0.0", diff --git a/readme.md b/readme.md index 8a72383..b336a58 100644 --- a/readme.md +++ b/readme.md @@ -152,6 +152,29 @@ When in stream mode, you can listen for events: Sets `options.method` to the method name and makes a request. +## Errors + +Each Error contains (if available) `host`, `hostname`, `method` and `path` properties to make debug easier. + +#### got.RequestError + +Happens, when making request failed. Should contain `code` property with error class code (like `ECONNREFUSED`). + +#### got.ReadError + +Happens, when reading from response stream failed. + +#### got.ParseError + +Happens, when `json` option is enabled and `JSON.parse` failed. + +#### got.HTTPError + +Happens, when server response code is not 2xx. Contains `statusCode` and `statusMessage`. + +#### got.MaxRedirectsError + +Happens, when server redirects you more than 10 times. ## Proxy @@ -201,8 +224,6 @@ This should only ever be done if you have Node version 0.10.x and at the top-lev ## Related - [gh-got](https://github.com/sindresorhus/gh-got) - Convenience wrapper for interacting with the GitHub API -- [got-promise](https://github.com/floatdrop/got-promise) - Promise wrapper - ## Created by diff --git a/test/test-arguments.js b/test/test-arguments.js index cff95af..8d727cc 100644 --- a/test/test-arguments.js +++ b/test/test-arguments.js @@ -46,13 +46,6 @@ test('overrides querystring from opts', function (t) { }); }); -test('pathname confusion', function (t) { - got({protocol: 'http:', hostname: s.host, port: s.port, pathname: '/test'}, function (err) { - t.error(err); - t.end(); - }); -}); - test('cleanup', function (t) { s.close(); t.end(); diff --git a/test/test-error.js b/test/test-error.js index 7773966..da05a14 100644 --- a/test/test-error.js +++ b/test/test-error.js @@ -18,7 +18,9 @@ test('setup', function (t) { test('error message', function (t) { got(s.url, function (err) { t.ok(err); - t.equal(err.message, 'GET http://localhost:6767/ response code is 404 (Not Found)'); + t.equal(err.message, 'Response code 404 (Not Found)'); + t.equal(err.host, 'localhost:6767'); + t.equal(err.method, 'GET'); t.end(); }); }); @@ -26,9 +28,9 @@ test('error message', function (t) { test('dns error message', function (t) { got('.com', function (err) { t.ok(err); - t.equal(err.message, 'Request to http://.com/ failed'); - t.ok(err.nested); - t.ok(/getaddrinfo ENOTFOUND/.test(err.nested.message)); + t.ok(/getaddrinfo ENOTFOUND/.test(err.message)); + t.equal(err.host, '.com'); + t.equal(err.method, 'GET'); t.end(); }); }); diff --git a/test/test-gzip.js b/test/test-gzip.js index a04bfd4..86c6a7c 100644 --- a/test/test-gzip.js +++ b/test/test-gzip.js @@ -39,7 +39,9 @@ test('ungzip content', function (t) { test('ungzip error', function (t) { got(s.url + '/corrupted', function (err) { t.ok(err); - t.equal(err.message, 'Reading ' + s.url + '/corrupted response failed'); + t.equal(err.message, 'incorrect header check'); + t.equal(err.path, '/corrupted'); + t.equal(err.name, 'ReadError'); t.end(); }); }); diff --git a/test/test-helpers.js b/test/test-helpers.js index beda268..710e986 100644 --- a/test/test-helpers.js +++ b/test/test-helpers.js @@ -37,7 +37,6 @@ test('promise mode', function (t) { got.get(s.url + '/404') .catch(function (err) { - t.equal(err.message, 'GET http://localhost:6767/404 response code is 404 (Not Found)'); t.equal(err.response.body, 'not found'); }); }); diff --git a/test/test-http.js b/test/test-http.js index 10d1340..d7fde34 100644 --- a/test/test-http.js +++ b/test/test-http.js @@ -56,7 +56,7 @@ test('empty response', function (t) { test('error with code', function (t) { got(s.url + '/404', function (err, data) { t.ok(err); - t.equal(err.code, 404); + t.equal(err.statusCode, 404); t.equal(data, 'not'); t.end(); }); diff --git a/test/test-json.js b/test/test-json.js index 539d199..808da03 100644 --- a/test/test-json.js +++ b/test/test-json.js @@ -51,9 +51,8 @@ test('json option should not parse responses without a body', function (t) { test('json option wrap parsing errors', function (t) { got(s.url + '/invalid', {json: true}, function (err) { t.ok(err); - t.equal(err.message, 'Parsing ' + s.url + '/invalid response failed'); - t.ok(err.nested); - t.equal(err.nested.message, 'Unexpected token /'); + t.equal(err.message, 'Unexpected token /'); + t.equal(err.path, '/invalid'); t.end(); }); }); @@ -70,11 +69,8 @@ test('json option should catch errors on invalid non-200 responses', function (t got(s.url + '/non200-invalid', {json: true}, function (err, json) { t.ok(err); t.deepEqual(json, 'Internal error'); - t.equal(err.message, 'Parsing http://localhost:6767/non200-invalid response failed'); - t.ok(err.nested); - t.equal(err.nested.message, 'Unexpected token I'); - t.ok(err.nested.nested); - t.equal(err.nested.nested.message, 'GET http://localhost:6767/non200-invalid response code is 500 (Internal Server Error)'); + t.equal(err.message, 'Unexpected token I'); + t.equal(err.path, '/non200-invalid'); t.end(); }); }); diff --git a/test/test-redirects.js b/test/test-redirects.js index 56e39bb..53e90be 100644 --- a/test/test-redirects.js +++ b/test/test-redirects.js @@ -84,8 +84,9 @@ test('hostname+path in options are not breaking redirects', function (t) { test('redirect only GET and HEAD requests', function (t) { got(s.url + '/relative', {body: 'wow'}, function (err) { - t.equal(err.message, 'POST http://localhost:6767/relative response code is 302 (Moved Temporarily)'); - t.equal(err.code, 302); + t.equal(err.message, 'Response code 302 (Moved Temporarily)'); + t.equal(err.path, '/relative'); + t.equal(err.statusCode, 302); t.end(); }); }); diff --git a/test/test-stream.js b/test/test-stream.js index 5cf57ac..5a7128e 100644 --- a/test/test-stream.js +++ b/test/test-stream.js @@ -1,7 +1,6 @@ 'use strict'; var test = require('tap').test; -var from2Array = require('from2-array'); var got = require('../'); var server = require('./server.js'); var s = server.createServer();