diff --git a/CHANGELOG.md b/CHANGELOG.md index 13b700d..803ddd0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,13 @@ Changelog # 1.x release -## v1.4.1 (master) +## v1.5.0 (master) + +- Enhance: rejected promise now use custom `Error` (thx to @pekeler) +- Enhance: `FetchError` contains `err.type` and `err.code`, allows for better error handling (thx to @pekeler) +- Enhance: basic support for redirect mode `manual` and `error`, allows for location header extraction (thx to @jimmywarting for the initial PR) + +## v1.4.1 - Fix: wrapping Request instance with FormData body again should preserve the body as-is diff --git a/LICENSE.md b/LICENSE.md index 7c0c89b..660ffec 100644 --- a/LICENSE.md +++ b/LICENSE.md @@ -1,6 +1,6 @@ The MIT License (MIT) -Copyright (c) 2015 David Frank +Copyright (c) 2016 David Frank Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/LIMITS.md b/LIMITS.md index 8278f18..18b1bfb 100644 --- a/LIMITS.md +++ b/LIMITS.md @@ -14,6 +14,8 @@ Known differences - Similarly, `req.body` can either be a string or a readable stream. +- Also, you can handle rejected fetch requests through checking `err.type` and `err.code`. + - Only support `res.text()` and `res.json()` at the moment, until there are good use-cases for blob. - There is currently no built-in caching, as server-side caching varies by use-cases. diff --git a/README.md b/README.md index 634c298..74353c9 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ node-fetch [![build status][travis-image]][travis-url] [![coverage status][coveralls-image]][coveralls-url] -A light-weight module that brings `window.fetch` to node.js & io.js +A light-weight module that brings `window.fetch` to Node.js # Motivation @@ -15,7 +15,7 @@ I really like the notion of Matt Andrews' [isomorphic-fetch](https://github.com/ Instead of implementing `XMLHttpRequest` in node to run browser-specific [fetch polyfill](https://github.com/github/fetch), why not go from node's `http` to `fetch` API directly? Node has native stream support, browserify build targets (browsers) don't, so underneath they are going to be vastly different anyway. -Hence `node-fetch`, minimal code for a `window.fetch` compatible API on node.js/io.js runtime. +Hence `node-fetch`, minimal code for a `window.fetch` compatible API on Node.js runtime. # Features @@ -25,7 +25,7 @@ Hence `node-fetch`, minimal code for a `window.fetch` compatible API on node.js/ - Use native promise, but allow substituting it with [insert your favorite promise library]. - Use native stream for body, on both request and response. - Decode content encoding (gzip/deflate) properly, and convert string output (such as `res.text()` and `res.json()`) to utf-8 automatically. -- Useful extensions such as timeout, redirect limit, response size limit. +- Useful extensions such as timeout, redirect limit, response size limit, explicit reject errors. # Difference from client-side fetch @@ -157,7 +157,7 @@ default values are shown, note that only `method`, `headers` and `body` are allo , compress: true // support gzip/deflate content encoding, false to disable , size: 0 // maximum response body size in bytes, 0 to disable , body: empty // request body, can be a string or readable stream - , agent: null // custom http.Agent instance + , agent: null // http.Agent instance, allows custom proxy, certificate etc. } ``` diff --git a/index.js b/index.js index 3e31976..4a738a6 100644 --- a/index.js +++ b/index.js @@ -115,7 +115,7 @@ function Fetch(url, opts) { req.once('socket', function(socket) { reqTimeout = setTimeout(function() { req.abort(); - reject(new FetchError('network timeout at: ' + options.url, 'socket-timeout')); + reject(new FetchError('network timeout at: ' + options.url, 'request-timeout')); }, options.timeout); }); } @@ -129,7 +129,12 @@ function Fetch(url, opts) { clearTimeout(reqTimeout); // handle redirect - if (self.isRedirect(res.statusCode)) { + if (self.isRedirect(res.statusCode) && options.redirect !== 'manual') { + if (options.redirect === 'error') { + reject(new FetchError('redirect mode is set to error: ' + options.url, 'no-redirect')); + return; + } + if (options.counter >= options.follow) { reject(new FetchError('maximum redirect reached at: ' + options.url, 'max-redirect')); return; @@ -169,6 +174,11 @@ function Fetch(url, opts) { } } + // normalize location header for manual redirect mode + if (options.redirect === 'manual') { + headers.set('location', resolve_url(options.url, headers.get('location'))); + } + // response object var output = new Response(body, { url: options.url diff --git a/lib/body.js b/lib/body.js index 9352103..44f4400 100644 --- a/lib/body.js +++ b/lib/body.js @@ -1,7 +1,8 @@ + /** - * response.js + * body.js * - * Response class provides content decoding + * Body interface provides common methods for Request and Response */ var convert = require('encoding').convert; @@ -12,7 +13,7 @@ var FetchError = require('./fetch-error'); module.exports = Body; /** - * Response class + * Body class * * @param Stream body Readable stream * @param Object opts Response options diff --git a/lib/fetch-error.js b/lib/fetch-error.js index f44ea4d..7cabfb3 100644 --- a/lib/fetch-error.js +++ b/lib/fetch-error.js @@ -1,25 +1,34 @@ + /** * fetch-error.js * - * FetchError class for operational errors + * FetchError interface for operational errors */ module.exports = FetchError; /** - * Create FetchError + * Create FetchError instance * - * @param String reason String type Error optionalSystemError + * @param String message Error message for human + * @param String type Error type for machine + * @param String systemError For Node.js system error * @return FetchError */ -function FetchError(message, type, optionalSystemError) { +function FetchError(message, type, systemError) { + + // hide custom error implementation details from end-users Error.captureStackTrace(this, this.constructor); + this.name = this.constructor.name; this.message = message; this.type = type; - if (optionalSystemError) { - this.code = this.errno = optionalSystemError.code; + + // when err.type is `system`, err.code contains system error code + if (systemError) { + this.code = this.errno = systemError.code; } + } require('util').inherits(FetchError, Error); diff --git a/lib/headers.js b/lib/headers.js index e5c6c7c..fd7a14e 100644 --- a/lib/headers.js +++ b/lib/headers.js @@ -1,3 +1,4 @@ + /** * headers.js * diff --git a/lib/request.js b/lib/request.js index f6ab3af..37dbf82 100644 --- a/lib/request.js +++ b/lib/request.js @@ -1,3 +1,4 @@ + /** * request.js * @@ -43,6 +44,7 @@ function Request(input, init) { // fetch spec options this.method = init.method || input.method || 'GET'; + this.redirect = init.redirect || input.redirect || 'follow'; this.headers = new Headers(init.headers || input.headers || {}); this.url = url; diff --git a/lib/response.js b/lib/response.js index 62c353d..99d1184 100644 --- a/lib/response.js +++ b/lib/response.js @@ -1,3 +1,4 @@ + /** * response.js * diff --git a/test/test.js b/test/test.js index aa4c91f..fcb2758 100644 --- a/test/test.js +++ b/test/test.js @@ -89,7 +89,7 @@ describe('node-fetch', function() { url = 'http://localhost:50000/'; return expect(fetch(url)).to.eventually.be.rejected .and.be.an.instanceOf(FetchError) - .and.include({type: 'system', code: 'ECONNREFUSED', errno: 'ECONNREFUSED'}); + .and.include({ type: 'system', code: 'ECONNREFUSED', errno: 'ECONNREFUSED' }); }); it('should resolve into response', function() { @@ -298,6 +298,28 @@ describe('node-fetch', function() { .and.have.property('type', 'max-redirect'); }); + it('should support redirect mode, manual flag', function() { + url = base + '/redirect/301'; + opts = { + redirect: 'manual' + }; + return fetch(url, opts).then(function(res) { + expect(res.url).to.equal(base + '/redirect/301'); + expect(res.status).to.equal(301); + expect(res.headers.get('location')).to.equal(base + '/inspect'); + }); + }); + + it('should support redirect mode, error flag', function() { + url = base + '/redirect/301'; + opts = { + redirect: 'error' + }; + return expect(fetch(url, opts)).to.eventually.be.rejected + .and.be.an.instanceOf(FetchError) + .and.have.property('type', 'no-redirect'); + }); + it('should follow redirect code 301 and keep existing headers', function() { url = base + '/redirect/301'; opts = { @@ -356,7 +378,7 @@ describe('node-fetch', function() { }); it('should handle DNS-error response', function() { - url = 'http://invalid.commm'; + url = 'http://domain.invalid'; return expect(fetch(url)).to.eventually.be.rejected .and.be.an.instanceOf(FetchError) .and.have.property('code', 'ENOTFOUND'); @@ -448,7 +470,7 @@ describe('node-fetch', function() { }; return expect(fetch(url, opts)).to.eventually.be.rejected .and.be.an.instanceOf(FetchError) - .and.have.property('type', 'socket-timeout'); + .and.have.property('type', 'request-timeout'); }); it('should allow custom timeout on response body', function() { @@ -1145,6 +1167,7 @@ describe('node-fetch', function() { var req = new Request(url, { body: body , method: 'POST' + , redirect: 'manual' , headers: { b: '2' } @@ -1155,6 +1178,7 @@ describe('node-fetch', function() { var cl = req.clone(); expect(cl.url).to.equal(url); expect(cl.method).to.equal('POST'); + expect(cl.redirect).to.equal('manual'); expect(cl.headers.get('b')).to.equal('2'); expect(cl.follow).to.equal(3); expect(cl.compress).to.equal(false); @@ -1175,6 +1199,20 @@ describe('node-fetch', function() { expect(body).to.have.property('json'); });
 + it('should create custom FetchError', function() { + var systemError = new Error('system'); + systemError.code = 'ESOMEERROR'; + + var err = new FetchError('test message', 'test-error', systemError); + expect(err).to.be.an.instanceof(Error); + expect(err).to.be.an.instanceof(FetchError); + expect(err.name).to.equal('FetchError'); + expect(err.message).to.equal('test message'); + expect(err.type).to.equal('test-error'); + expect(err.code).to.equal('ESOMEERROR'); + expect(err.errno).to.equal('ESOMEERROR'); + });
 + it('should support https request', function() { this.timeout(5000); url = 'https://github.com/';