From 7c26fa94796687957a11f562802f2f24a838b57d Mon Sep 17 00:00:00 2001 From: Timothy Gu Date: Tue, 4 Oct 2016 02:37:49 -0700 Subject: [PATCH 01/61] Add Babel infrastructure No actual code has been changed yet. --- .gitignore | 6 +++++- package.json | 43 ++++++++++++++++++++++++++++++++----- {lib => src}/body.js | 0 {lib => src}/fetch-error.js | 0 {lib => src}/headers.js | 0 index.js => src/index.js | 10 ++++----- {lib => src}/request.js | 0 {lib => src}/response.js | 0 test/test.js | 14 ++++++------ 9 files changed, 55 insertions(+), 18 deletions(-) rename {lib => src}/body.js (100%) rename {lib => src}/fetch-error.js (100%) rename {lib => src}/headers.js (100%) rename index.js => src/index.js (97%) rename {lib => src}/request.js (100%) rename {lib => src}/response.js (100%) diff --git a/.gitignore b/.gitignore index a2234e0..99c8c2d 100644 --- a/.gitignore +++ b/.gitignore @@ -10,7 +10,8 @@ pids # Directory for instrumented libs generated by jscoverage/JSCover lib-cov -# Coverage directory used by tools like istanbul +# Coverage directory used by tools like nyc and istanbul +.nyc_output coverage # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) @@ -32,3 +33,6 @@ node_modules # Coveralls token files .coveralls.yml + +# Babel-compiled files +lib diff --git a/package.json b/package.json index 2627080..c112826 100644 --- a/package.json +++ b/package.json @@ -2,11 +2,13 @@ "name": "node-fetch", "version": "1.6.3", "description": "A light-weight module that brings window.fetch to node.js and io.js", - "main": "index.js", + "main": "lib/index.js", "scripts": { - "test": "mocha test/test.js", - "report": "istanbul cover _mocha -- -R spec test/test.js", - "coverage": "istanbul cover _mocha --report lcovonly -- -R spec test/test.js && cat ./coverage/lcov.info | coveralls" + "build": "babel -d lib src", + "prepublish": "npm run build", + "test": "mocha --compilers js:babel-register test/test.js", + "report": "cross-env BABEL_ENV=test nyc --reporter lcov --reporter text mocha -R spec test/test.js", + "coverage": "cross-env BABEL_ENV=test nyc --reporter lcovonly mocha -R spec test/test.js && cat ./coverage/lcov.info | coveralls" }, "repository": { "type": "git", @@ -24,13 +26,19 @@ }, "homepage": "https://github.com/bitinn/node-fetch", "devDependencies": { + "babel-cli": "^6.16.0", + "babel-plugin-istanbul": "^2.0.1", + "babel-plugin-transform-runtime": "^6.15.0", + "babel-preset-es2015": "^6.16.0", + "babel-register": "^6.16.3", "bluebird": "^3.3.4", "chai": "^3.5.0", "chai-as-promised": "^5.2.0", "coveralls": "^2.11.2", + "cross-env": "^3.0.0", "form-data": ">=1.0.0", - "istanbul": "^0.4.2", "mocha": "^2.1.0", + "nyc": "^8.3.0", "parted": "^0.1.1", "promise": "^7.1.1", "resumer": "0.0.0" @@ -38,5 +46,30 @@ "dependencies": { "encoding": "^0.1.11", "is-stream": "^1.0.1" + }, + "babel": { + "presets": [ + "es2015" + ], + "plugins": [ + "transform-runtime" + ], + "env": { + "test": { + "plugins": [ + "istanbul" + ] + } + } + }, + "nyc": { + "include": [ + "src/*.js" + ], + "require": [ + "babel-register" + ], + "sourceMap": false, + "instrument": false } } diff --git a/lib/body.js b/src/body.js similarity index 100% rename from lib/body.js rename to src/body.js diff --git a/lib/fetch-error.js b/src/fetch-error.js similarity index 100% rename from lib/fetch-error.js rename to src/fetch-error.js diff --git a/lib/headers.js b/src/headers.js similarity index 100% rename from lib/headers.js rename to src/headers.js diff --git a/index.js b/src/index.js similarity index 97% rename from index.js rename to src/index.js index df89c80..676b57a 100644 --- a/index.js +++ b/src/index.js @@ -12,11 +12,11 @@ var https = require('https'); var zlib = require('zlib'); var stream = require('stream'); -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'); +var Body = require('./body'); +var Response = require('./response'); +var Headers = require('./headers'); +var Request = require('./request'); +var FetchError = require('./fetch-error'); // commonjs module.exports = Fetch; diff --git a/lib/request.js b/src/request.js similarity index 100% rename from lib/request.js rename to src/request.js diff --git a/lib/response.js b/src/response.js similarity index 100% rename from lib/response.js rename to src/response.js diff --git a/test/test.js b/test/test.js index 6067ccd..a7ce43e 100644 --- a/test/test.js +++ b/test/test.js @@ -16,12 +16,12 @@ var fs = require('fs'); var TestServer = require('./server'); // test subjects -var fetch = require('../index.js'); -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'); +var fetch = require('../src/index.js'); +var Headers = require('../src/headers.js'); +var Response = require('../src/response.js'); +var Request = require('../src/request.js'); +var Body = require('../src/body.js'); +var FetchError = require('../src/fetch-error.js'); // test with native promise on node 0.11, and bluebird for node 0.10 fetch.Promise = fetch.Promise || bluebird; @@ -1122,7 +1122,7 @@ describe('node-fetch', function() { result.push([key, val]); }); - expected = [ + var expected = [ ["a", "1"] , ["b", "2"] , ["b", "3"] From 2874af4218c059e9990de47596d0d9da1872cdf9 Mon Sep 17 00:00:00 2001 From: Timothy Gu Date: Tue, 4 Oct 2016 03:07:44 -0700 Subject: [PATCH 02/61] Use cross-env@2.0.1 to support Node.js < v4 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index c112826..7014d43 100644 --- a/package.json +++ b/package.json @@ -35,7 +35,7 @@ "chai": "^3.5.0", "chai-as-promised": "^5.2.0", "coveralls": "^2.11.2", - "cross-env": "^3.0.0", + "cross-env": "2.0.1", "form-data": ">=1.0.0", "mocha": "^2.1.0", "nyc": "^8.3.0", From 993d4cdea1b63db4ec41a994873e6044f26cde85 Mon Sep 17 00:00:00 2001 From: Timothy Gu Date: Sat, 8 Oct 2016 20:51:01 -0700 Subject: [PATCH 03/61] Convert Headers to ES2015 and implement Iterable interface (#180) Closes #127, #174. --- src/headers.js | 310 ++++++++++++++++++++++++++++++------------------ src/index.js | 2 +- src/request.js | 2 +- src/response.js | 2 +- test/test.js | 113 ++++++++++++++---- 5 files changed, 283 insertions(+), 146 deletions(-) diff --git a/src/headers.js b/src/headers.js index fd7a14e..ad2e9e4 100644 --- a/src/headers.js +++ b/src/headers.js @@ -5,137 +5,211 @@ * Headers class offers convenient helpers */ -module.exports = Headers; +export const MAP = Symbol('map'); -/** - * Headers class - * - * @param Object headers Response headers - * @return Void - */ -function Headers(headers) { +export default class Headers { + /** + * Headers class + * + * @param Object headers Response headers + * @return Void + */ + constructor(headers) { + this[MAP] = {}; - var self = this; - this._headers = {}; - - // Headers - if (headers instanceof Headers) { - headers = headers.raw(); - } - - // plain object - for (var prop in headers) { - if (!headers.hasOwnProperty(prop)) { - continue; + // Headers + if (headers instanceof Headers) { + headers = headers.raw(); } - if (typeof headers[prop] === 'string') { - this.set(prop, headers[prop]); + // plain object + for (const prop in headers) { + if (!headers.hasOwnProperty(prop)) { + continue; + } - } else if (typeof headers[prop] === 'number' && !isNaN(headers[prop])) { - this.set(prop, headers[prop].toString()); + if (typeof headers[prop] === 'string') { + this.set(prop, headers[prop]); + } else if (typeof headers[prop] === 'number' && !isNaN(headers[prop])) { + this.set(prop, headers[prop].toString()); + } else if (headers[prop] instanceof Array) { + headers[prop].forEach(item => { + this.append(prop, item.toString()); + }); + } + } + } - } else if (headers[prop] instanceof Array) { - headers[prop].forEach(function(item) { - self.append(prop, item.toString()); + /** + * Return first header value given name + * + * @param String name Header name + * @return Mixed + */ + get(name) { + const list = this[MAP][name.toLowerCase()]; + return list ? list[0] : null; + } + + /** + * Return all header values given name + * + * @param String name Header name + * @return Array + */ + getAll(name) { + if (!this.has(name)) { + return []; + } + + return this[MAP][name.toLowerCase()]; + } + + /** + * Iterate over all headers + * + * @param Function callback Executed for each item with parameters (value, name, thisArg) + * @param Boolean thisArg `this` context for callback function + * @return Void + */ + forEach(callback, thisArg) { + Object.getOwnPropertyNames(this[MAP]).forEach(name => { + this[MAP][name].forEach(value => { + callback.call(thisArg, value, name, this); }); + }); + } + + /** + * Overwrite header values given name + * + * @param String name Header name + * @param String value Header value + * @return Void + */ + set(name, value) { + this[MAP][name.toLowerCase()] = [value]; + } + + /** + * Append a value onto existing header + * + * @param String name Header name + * @param String value Header value + * @return Void + */ + append(name, value) { + if (!this.has(name)) { + this.set(name, value); + return; } + + this[MAP][name.toLowerCase()].push(value); } + /** + * Check for header name existence + * + * @param String name Header name + * @return Boolean + */ + has(name) { + return this[MAP].hasOwnProperty(name.toLowerCase()); + } + + /** + * Delete all header values given name + * + * @param String name Header name + * @return Void + */ + delete(name) { + delete this[MAP][name.toLowerCase()]; + }; + + /** + * Return raw headers (non-spec api) + * + * @return Object + */ + raw() { + return this[MAP]; + } + + /** + * Get an iterator on keys. + * + * @return Iterator + */ + keys() { + const keys = []; + this.forEach((_, name) => keys.push(name)); + return new Iterator(keys); + } + + /** + * Get an iterator on values. + * + * @return Iterator + */ + values() { + const values = []; + this.forEach(value => values.push(value)); + return new Iterator(values); + } + + /** + * Get an iterator on entries. + * + * @return Iterator + */ + entries() { + const entries = []; + this.forEach((value, name) => entries.push([name, value])); + return new Iterator(entries); + } + + /** + * Get an iterator on entries. + * + * This is the default iterator of the Headers object. + * + * @return Iterator + */ + [Symbol.iterator]() { + return this.entries(); + } + + /** + * Tag used by `Object.prototype.toString()`. + */ + get [Symbol.toStringTag]() { + return 'Headers'; + } } -/** - * Return first header value given name - * - * @param String name Header name - * @return Mixed - */ -Headers.prototype.get = function(name) { - var list = this._headers[name.toLowerCase()]; - return list ? list[0] : null; -}; - -/** - * Return all header values given name - * - * @param String name Header name - * @return Array - */ -Headers.prototype.getAll = function(name) { - if (!this.has(name)) { - return []; +const ITEMS = Symbol('items'); +class Iterator { + constructor(items) { + this[ITEMS] = items; } - return this._headers[name.toLowerCase()]; -}; + next() { + if (!this[ITEMS].length) { + return { + value: undefined, + done: true + }; + } -/** - * Iterate over all headers - * - * @param Function callback Executed for each item with parameters (value, name, thisArg) - * @param Boolean thisArg `this` context for callback function - * @return Void - */ -Headers.prototype.forEach = function(callback, thisArg) { - Object.getOwnPropertyNames(this._headers).forEach(function(name) { - this._headers[name].forEach(function(value) { - callback.call(thisArg, value, name, this) - }, this) - }, this) + return { + value: this[ITEMS].shift(), + done: false + }; + + } + + [Symbol.iterator]() { + return this; + } } - -/** - * Overwrite header values given name - * - * @param String name Header name - * @param String value Header value - * @return Void - */ -Headers.prototype.set = function(name, value) { - this._headers[name.toLowerCase()] = [value]; -}; - -/** - * Append a value onto existing header - * - * @param String name Header name - * @param String value Header value - * @return Void - */ -Headers.prototype.append = function(name, value) { - if (!this.has(name)) { - this.set(name, value); - return; - } - - this._headers[name.toLowerCase()].push(value); -}; - -/** - * Check for header name existence - * - * @param String name Header name - * @return Boolean - */ -Headers.prototype.has = function(name) { - return this._headers.hasOwnProperty(name.toLowerCase()); -}; - -/** - * Delete all header values given name - * - * @param String name Header name - * @return Void - */ -Headers.prototype['delete'] = function(name) { - delete this._headers[name.toLowerCase()]; -}; - -/** - * Return raw headers (non-spec api) - * - * @return Object - */ -Headers.prototype.raw = function() { - return this._headers; -}; diff --git a/src/index.js b/src/index.js index 676b57a..d30c2d8 100644 --- a/src/index.js +++ b/src/index.js @@ -14,7 +14,7 @@ var stream = require('stream'); var Body = require('./body'); var Response = require('./response'); -var Headers = require('./headers'); +import Headers from './headers'; var Request = require('./request'); var FetchError = require('./fetch-error'); diff --git a/src/request.js b/src/request.js index 1a29c29..ec80635 100644 --- a/src/request.js +++ b/src/request.js @@ -6,7 +6,7 @@ */ var parse_url = require('url').parse; -var Headers = require('./headers'); +import Headers from './headers'; var Body = require('./body'); module.exports = Request; diff --git a/src/response.js b/src/response.js index f96aa85..b079bd0 100644 --- a/src/response.js +++ b/src/response.js @@ -6,7 +6,7 @@ */ var http = require('http'); -var Headers = require('./headers'); +import Headers from './headers'; var Body = require('./body'); module.exports = Response; diff --git a/test/test.js b/test/test.js index a7ce43e..b593407 100644 --- a/test/test.js +++ b/test/test.js @@ -17,7 +17,7 @@ var TestServer = require('./server'); // test subjects var fetch = require('../src/index.js'); -var Headers = require('../src/headers.js'); +import Headers from '../src/headers.js'; var Response = require('../src/response.js'); var Request = require('../src/request.js'); var Body = require('../src/body.js'); @@ -1131,6 +1131,65 @@ describe('node-fetch', function() { expect(result).to.deep.equal(expected); }); + it('should allow iterating through all headers', function() { + var headers = new Headers({ + a: 1 + , b: [2, 3] + , c: [4] + }); + expect(headers).to.have.property(Symbol.iterator); + expect(headers).to.have.property('keys'); + expect(headers).to.have.property('values'); + expect(headers).to.have.property('entries'); + + var result, expected; + + result = []; + for (let [key, val] of headers) { + result.push([key, val]); + } + + expected = [ + ["a", "1"] + , ["b", "2"] + , ["b", "3"] + , ["c", "4"] + ]; + expect(result).to.deep.equal(expected); + + result = []; + for (let [key, val] of headers.entries()) { + result.push([key, val]); + } + expect(result).to.deep.equal(expected); + + result = []; + for (let key of headers.keys()) { + result.push(key); + } + + expected = [ + "a" + , "b" + , "b" + , "c" + ]; + expect(result).to.deep.equal(expected); + + result = []; + for (let key of headers.values()) { + result.push(key); + } + + expected = [ + "1" + , "2" + , "3" + , "4" + ]; + expect(result).to.deep.equal(expected); + }); + it('should allow deleting header', function() { url = base + '/cookie'; return fetch(url).then(function(res) { @@ -1178,49 +1237,53 @@ describe('node-fetch', function() { res.m = new Buffer('test'); var h1 = new Headers(res); + var h1Raw = h1.raw(); - expect(h1._headers['a']).to.include('string'); - expect(h1._headers['b']).to.include('1'); - expect(h1._headers['b']).to.include('2'); - expect(h1._headers['c']).to.include(''); - expect(h1._headers['d']).to.be.undefined; + expect(h1Raw['a']).to.include('string'); + expect(h1Raw['b']).to.include('1'); + expect(h1Raw['b']).to.include('2'); + expect(h1Raw['c']).to.include(''); + expect(h1Raw['d']).to.be.undefined; - expect(h1._headers['e']).to.include('1'); - expect(h1._headers['f']).to.include('1'); - expect(h1._headers['f']).to.include('2'); + expect(h1Raw['e']).to.include('1'); + expect(h1Raw['f']).to.include('1'); + expect(h1Raw['f']).to.include('2'); - expect(h1._headers['g']).to.be.undefined; - expect(h1._headers['h']).to.be.undefined; - expect(h1._headers['i']).to.be.undefined; - expect(h1._headers['j']).to.be.undefined; - expect(h1._headers['k']).to.be.undefined; - expect(h1._headers['l']).to.be.undefined; - expect(h1._headers['m']).to.be.undefined; + expect(h1Raw['g']).to.be.undefined; + expect(h1Raw['h']).to.be.undefined; + expect(h1Raw['i']).to.be.undefined; + expect(h1Raw['j']).to.be.undefined; + expect(h1Raw['k']).to.be.undefined; + expect(h1Raw['l']).to.be.undefined; + expect(h1Raw['m']).to.be.undefined; - expect(h1._headers['z']).to.be.undefined; + expect(h1Raw['z']).to.be.undefined; }); it('should wrap headers', function() { var h1 = new Headers({ a: '1' }); + var h1Raw = h1.raw(); var h2 = new Headers(h1); h2.set('b', '1'); + var h2Raw = h2.raw(); var h3 = new Headers(h2); h3.append('a', '2'); + var h3Raw = h3.raw(); - expect(h1._headers['a']).to.include('1'); - expect(h1._headers['a']).to.not.include('2'); + expect(h1Raw['a']).to.include('1'); + expect(h1Raw['a']).to.not.include('2'); - expect(h2._headers['a']).to.include('1'); - expect(h2._headers['a']).to.not.include('2'); - expect(h2._headers['b']).to.include('1'); + expect(h2Raw['a']).to.include('1'); + expect(h2Raw['a']).to.not.include('2'); + expect(h2Raw['b']).to.include('1'); - expect(h3._headers['a']).to.include('1'); - expect(h3._headers['a']).to.include('2'); - expect(h3._headers['b']).to.include('1'); + expect(h3Raw['a']).to.include('1'); + expect(h3Raw['a']).to.include('2'); + expect(h3Raw['b']).to.include('1'); }); it('should support fetch with Request instance', function() { From 838071247d986a3f706392640ea299ec37f67073 Mon Sep 17 00:00:00 2001 From: Timothy Gu Date: Mon, 10 Oct 2016 11:50:04 -0700 Subject: [PATCH 04/61] Convert all files to ES2015 (#182) Elements of this commit come from #140 by @gwicke. --- package.json | 5 +- src/body.js | 412 +++++++++++++-------------- src/fetch-error.js | 4 +- src/index.js | 109 +++---- src/request.js | 112 ++++---- src/response.js | 69 +++-- test/server.js | 588 +++++++++++++++++++------------------- test/test.js | 691 ++++++++++++++++++++++----------------------- 8 files changed, 986 insertions(+), 1004 deletions(-) diff --git a/package.json b/package.json index 7014d43..1552704 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,7 @@ "scripts": { "build": "babel -d lib src", "prepublish": "npm run build", - "test": "mocha --compilers js:babel-register test/test.js", + "test": "mocha --compilers js:babel-polyfill --compilers js:babel-register test/test.js", "report": "cross-env BABEL_ENV=test nyc --reporter lcov --reporter text mocha -R spec test/test.js", "coverage": "cross-env BABEL_ENV=test nyc --reporter lcovonly mocha -R spec test/test.js && cat ./coverage/lcov.info | coveralls" }, @@ -29,6 +29,7 @@ "babel-cli": "^6.16.0", "babel-plugin-istanbul": "^2.0.1", "babel-plugin-transform-runtime": "^6.15.0", + "babel-polyfill": "^6.16.0", "babel-preset-es2015": "^6.16.0", "babel-register": "^6.16.3", "bluebird": "^3.3.4", @@ -44,6 +45,7 @@ "resumer": "0.0.0" }, "dependencies": { + "babel-runtime": "^6.11.6", "encoding": "^0.1.11", "is-stream": "^1.0.1" }, @@ -67,6 +69,7 @@ "src/*.js" ], "require": [ + "babel-polyfill", "babel-register" ], "sourceMap": false, diff --git a/src/body.js b/src/body.js index e7bbe1d..ab76421 100644 --- a/src/body.js +++ b/src/body.js @@ -5,12 +5,17 @@ * Body interface provides common methods for Request and Response */ -var convert = require('encoding').convert; -var bodyStream = require('is-stream'); -var PassThrough = require('stream').PassThrough; -var FetchError = require('./fetch-error'); +import {convert} from 'encoding'; +import bodyStream from 'is-stream'; +import {PassThrough} from 'stream'; +import FetchError from './fetch-error.js'; -module.exports = Body; +const DISTURBED = Symbol('disturbed'); +const BYTES = Symbol('bytes'); +const RAW = Symbol('raw'); +const ABORT = Symbol('abort'); +const CONVERT = Symbol('convert'); +const DECODE = Symbol('decode'); /** * Body class @@ -19,221 +24,210 @@ module.exports = Body; * @param Object opts Response options * @return Void */ -function Body(body, opts) { +export default class Body { + constructor(body, { + size = 0, + timeout = 0 + } = {}) { + this.body = body; + this[DISTURBED] = false; + this.size = size; + this[BYTES] = 0; + this.timeout = timeout; + this[RAW] = []; + this[ABORT] = false; + } - opts = opts || {}; + get bodyUsed() { + return this[DISTURBED]; + } - this.body = body; - this.bodyUsed = false; - this.size = opts.size || 0; - this.timeout = opts.timeout || 0; - this._raw = []; - this._abort = false; + /** + * Decode response as json + * + * @return Promise + */ + json() { + // for 204 No Content response, buffer will be empty, parsing it will throw error + if (this.status === 204) { + return Body.Promise.resolve({}); + } + + return this[DECODE]().then(buffer => JSON.parse(buffer.toString())); + } + + /** + * Decode response as text + * + * @return Promise + */ + text() { + return this[DECODE]().then(buffer => buffer.toString()); + } + + /** + * Decode response as buffer (non-spec api) + * + * @return Promise + */ + buffer() { + return this[DECODE](); + } + + /** + * Decode buffers into utf-8 string + * + * @return Promise + */ + [DECODE]() { + if (this[DISTURBED]) { + return Body.Promise.reject(new Error(`body used already for: ${this.url}`)); + } + + this[DISTURBED] = true; + this[BYTES] = 0; + this[ABORT] = false; + this[RAW] = []; + + return new Body.Promise((resolve, reject) => { + let resTimeout; + + // body is string + if (typeof this.body === 'string') { + this[BYTES] = this.body.length; + this[RAW] = [new Buffer(this.body)]; + return resolve(this[CONVERT]()); + } + + // body is buffer + if (this.body instanceof Buffer) { + this[BYTES] = this.body.length; + this[RAW] = [this.body]; + return resolve(this[CONVERT]()); + } + + // allow timeout on slow response body + if (this.timeout) { + resTimeout = setTimeout(() => { + this[ABORT] = true; + reject(new FetchError('response timeout at ' + this.url + ' over limit: ' + this.timeout, 'body-timeout')); + }, this.timeout); + } + + // handle stream error, such as incorrect content-encoding + this.body.on('error', err => { + reject(new FetchError('invalid response body at: ' + this.url + ' reason: ' + err.message, 'system', err)); + }); + + // body is stream + this.body.on('data', chunk => { + if (this[ABORT] || chunk === null) { + return; + } + + if (this.size && this[BYTES] + chunk.length > this.size) { + this[ABORT] = true; + reject(new FetchError(`content size at ${this.url} over limit: ${this.size}`, 'max-size')); + return; + } + + this[BYTES] += chunk.length; + this[RAW].push(chunk); + }); + + this.body.on('end', () => { + if (this[ABORT]) { + return; + } + + clearTimeout(resTimeout); + resolve(this[CONVERT]()); + }); + }); + } + + /** + * Detect buffer encoding and convert to target encoding + * ref: http://www.w3.org/TR/2011/WD-html5-20110113/parsing.html#determining-the-character-encoding + * + * @param String encoding Target encoding + * @return String + */ + [CONVERT](encoding = 'utf-8') { + const ct = this.headers.get('content-type'); + let charset = 'utf-8'; + let res, str; + + // header + if (ct) { + // skip encoding detection altogether if not html/xml/plain text + if (!/text\/html|text\/plain|\+xml|\/xml/i.test(ct)) { + return Buffer.concat(this[RAW]); + } + + res = /charset=([^;]*)/i.exec(ct); + } + + // no charset in content type, peek at response body for at most 1024 bytes + if (!res && this[RAW].length > 0) { + for (let i = 0; i < this[RAW].length; i++) { + str += this[RAW][i].toString() + if (str.length > 1024) { + break; + } + } + str = str.substr(0, 1024); + } + + // html5 + if (!res && str) { + res = / self.size) { - self._abort = true; - reject(new FetchError('content size at ' + self.url + ' over limit: ' + self.size, 'max-size')); - return; - } - - self._bytes += chunk.length; - self._raw.push(chunk); - }); - - self.body.on('end', function() { - if (self._abort) { - return; - } - - clearTimeout(resTimeout); - resolve(self._convert()); - }); - }); - -}; - -/** - * Detect buffer encoding and convert to target encoding - * ref: http://www.w3.org/TR/2011/WD-html5-20110113/parsing.html#determining-the-character-encoding - * - * @param String encoding Target encoding - * @return String - */ -Body.prototype._convert = function(encoding) { - - encoding = encoding || 'utf-8'; - - var ct = this.headers.get('content-type'); - var charset = 'utf-8'; - var res, str; - - // header - if (ct) { - // skip encoding detection altogether if not html/xml/plain text - if (!/text\/html|text\/plain|\+xml|\/xml/i.test(ct)) { - return Buffer.concat(this._raw); - } - - res = /charset=([^;]*)/i.exec(ct); - } - - // no charset in content type, peek at response body for at most 1024 bytes - if (!res && this._raw.length > 0) { - for (var i = 0; i < this._raw.length; i++) { - str += this._raw[i].toString() - if (str.length > 1024) { - break; - } - } - str = str.substr(0, 1024); - } - - // html5 - if (!res && str) { - res = / { // build request object - var options = new Request(url, opts); + const options = new Request(url, opts); if (!options.protocol || !options.hostname) { throw new Error('only absolute urls are supported'); @@ -58,15 +46,10 @@ function Fetch(url, opts) { throw new Error('only http(s) protocols are supported'); } - var send; - if (options.protocol === 'https:') { - send = https.request; - } else { - send = http.request; - } + const send = (options.protocol === 'https:' ? https : http).request; // normalize headers - var headers = new Headers(options.headers); + const headers = new Headers(options.headers); if (options.compress) { headers.set('accept-encoding', 'gzip,deflate'); @@ -86,7 +69,7 @@ function Fetch(url, opts) { // detect form data input from form-data module, this hack avoid the need to pass multipart header manually if (!headers.has('content-type') && options.body && typeof options.body.getBoundary === 'function') { - headers.set('content-type', 'multipart/form-data; boundary=' + options.body.getBoundary()); + headers.set('content-type', `multipart/form-data; boundary=${options.body.getBoundary()}`); } // bring node-fetch closer to browser behavior by setting content-length automatically @@ -116,40 +99,40 @@ function Fetch(url, opts) { } // send request - var req = send(options); - var reqTimeout; + const req = send(options); + let reqTimeout; if (options.timeout) { - req.once('socket', function(socket) { - reqTimeout = setTimeout(function() { + req.once('socket', socket => { + reqTimeout = setTimeout(() => { req.abort(); - reject(new FetchError('network timeout at: ' + options.url, 'request-timeout')); + reject(new FetchError(`network timeout at: ${options.url}`, 'request-timeout')); }, options.timeout); }); } - req.on('error', function(err) { + req.on('error', err => { clearTimeout(reqTimeout); - reject(new FetchError('request to ' + options.url + ' failed, reason: ' + err.message, 'system', err)); + reject(new FetchError(`request to ${options.url} failed, reason: ${err.message}`, 'system', err)); }); - req.on('response', function(res) { + req.on('response', res => { clearTimeout(reqTimeout); // handle redirect - if (self.isRedirect(res.statusCode) && options.redirect !== 'manual') { + if (fetch.isRedirect(res.statusCode) && options.redirect !== 'manual') { if (options.redirect === 'error') { - reject(new FetchError('redirect mode is set to error: ' + options.url, 'no-redirect')); + 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')); + reject(new FetchError(`maximum redirect reached at: ${options.url}`, 'max-redirect')); return; } if (!res.headers.location) { - reject(new FetchError('redirect location header missing at: ' + options.url, 'invalid-redirect')); + reject(new FetchError(`redirect location header missing at: ${options.url}`, 'invalid-redirect')); return; } @@ -164,19 +147,19 @@ function Fetch(url, opts) { options.counter++; - resolve(Fetch(resolve_url(options.url, res.headers.location), options)); + resolve(fetch(resolve_url(options.url, res.headers.location), options)); return; } // normalize location header for manual redirect mode - var headers = new Headers(res.headers); + const headers = new Headers(res.headers); if (options.redirect === 'manual' && headers.has('location')) { headers.set('location', resolve_url(options.url, headers.get('location'))); } // prepare response - var body = res.pipe(new stream.PassThrough()); - var response_options = { + let body = res.pipe(new PassThrough()); + const response_options = { url: options.url , status: res.statusCode , statusText: res.statusMessage @@ -186,7 +169,7 @@ function Fetch(url, opts) { }; // response object - var output; + let output; // in following scenarios we ignore compression support // 1. compression support is disabled @@ -201,7 +184,7 @@ function Fetch(url, opts) { } // otherwise, check for gzip or deflate - var name = headers.get('content-encoding'); + let name = headers.get('content-encoding'); // for gzip if (name == 'gzip' || name == 'x-gzip') { @@ -214,8 +197,8 @@ function Fetch(url, opts) { } else if (name == 'deflate' || name == 'x-deflate') { // handle the infamous raw deflate response from old servers // a hack for old IIS and Apache servers - var raw = res.pipe(new stream.PassThrough()); - raw.once('data', function(chunk) { + const raw = res.pipe(new PassThrough()); + raw.once('data', chunk => { // see http://stackoverflow.com/questions/37519828 if ((chunk[0] & 0x0F) === 0x08) { body = body.pipe(zlib.createInflate()); @@ -254,18 +237,18 @@ function Fetch(url, opts) { }; +module.exports = fetch; + /** * Redirect code matching * * @param Number code Status code * @return Boolean */ -Fetch.prototype.isRedirect = function(code) { - return code === 301 || code === 302 || code === 303 || code === 307 || code === 308; -} +fetch.isRedirect = code => code === 301 || code === 302 || code === 303 || code === 307 || code === 308; // expose Promise -Fetch.Promise = global.Promise; -Fetch.Response = Response; -Fetch.Headers = Headers; -Fetch.Request = Request; +fetch.Promise = global.Promise; +fetch.Response = Response; +fetch.Headers = Headers; +fetch.Request = Request; diff --git a/src/request.js b/src/request.js index ec80635..1508d67 100644 --- a/src/request.js +++ b/src/request.js @@ -5,11 +5,9 @@ * Request class contains server only options */ -var parse_url = require('url').parse; -import Headers from './headers'; -var Body = require('./body'); - -module.exports = Request; +import { parse as parse_url } from 'url'; +import Headers from './headers.js'; +import Body, { clone } from './body'; /** * Request class @@ -18,58 +16,62 @@ module.exports = Request; * @param Object init Custom options * @return Void */ -function Request(input, init) { - var url, url_parsed; +export default class Request extends Body { + constructor(input, init = {}) { + let url, url_parsed; - // normalize input - if (!(input instanceof Request)) { - url = input; - url_parsed = parse_url(url); - input = {}; - } else { - url = input.url; - url_parsed = parse_url(url); + // normalize input + if (!(input instanceof Request)) { + url = input; + url_parsed = parse_url(url); + input = {}; + } else { + url = input.url; + url_parsed = parse_url(url); + } + + super(init.body || clone(input), { + timeout: init.timeout || input.timeout || 0, + size: init.size || input.size || 0 + }); + + // 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; + + // server only options + this.follow = init.follow !== undefined ? + init.follow : input.follow !== undefined ? + input.follow : 20; + this.compress = init.compress !== undefined ? + init.compress : input.compress !== undefined ? + input.compress : true; + this.counter = init.counter || input.counter || 0; + this.agent = init.agent || input.agent; + + // server request options + this.protocol = url_parsed.protocol; + this.hostname = url_parsed.hostname; + this.port = url_parsed.port; + this.path = url_parsed.path; + this.auth = url_parsed.auth; } - // normalize init - init = init || {}; + /** + * Clone this request + * + * @return Request + */ + clone() { + return new Request(this); + } - // 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; - - // server only options - this.follow = init.follow !== undefined ? - init.follow : input.follow !== undefined ? - input.follow : 20; - this.compress = init.compress !== undefined ? - init.compress : input.compress !== undefined ? - input.compress : true; - this.counter = init.counter || input.counter || 0; - this.agent = init.agent || input.agent; - - Body.call(this, init.body || this._clone(input), { - timeout: init.timeout || input.timeout || 0, - size: init.size || input.size || 0 - }); - - // server request options - this.protocol = url_parsed.protocol; - this.hostname = url_parsed.hostname; - this.port = url_parsed.port; - this.path = url_parsed.path; - this.auth = url_parsed.auth; + /** + * Tag used by `Object.prototype.toString()`. + */ + get [Symbol.toStringTag]() { + return 'Request'; + } } - -Request.prototype = Object.create(Body.prototype); - -/** - * Clone this request - * - * @return Request - */ -Request.prototype.clone = function() { - return new Request(this); -}; diff --git a/src/response.js b/src/response.js index b079bd0..fb53a78 100644 --- a/src/response.js +++ b/src/response.js @@ -5,11 +5,9 @@ * Response class provides content decoding */ -var http = require('http'); -import Headers from './headers'; -var Body = require('./body'); - -module.exports = Response; +import { STATUS_CODES } from 'http'; +import Headers from './headers.js'; +import Body, { clone } from './body'; /** * Response class @@ -18,33 +16,44 @@ module.exports = Response; * @param Object opts Response options * @return Void */ -function Response(body, opts) { +export default class Response extends Body { + constructor(body, opts = {}) { + super(body, opts); - opts = opts || {}; + this.url = opts.url; + this.status = opts.status || 200; + this.statusText = opts.statusText || STATUS_CODES[this.status]; + this.headers = new Headers(opts.headers); + } - this.url = opts.url; - this.status = opts.status || 200; - this.statusText = opts.statusText || http.STATUS_CODES[this.status]; - this.headers = new Headers(opts.headers); - this.ok = this.status >= 200 && this.status < 300; + /** + * Convenience property representing if the request ended normally + */ + get ok() { + return this.status >= 200 && this.status < 300; + } - Body.call(this, body, opts); + /** + * Clone this response + * + * @return Response + */ + clone() { + return new Response(clone(this), { + url: this.url + , status: this.status + , statusText: this.statusText + , headers: this.headers + , ok: this.ok + }); + + } + + /** + * Tag used by `Object.prototype.toString()`. + */ + get [Symbol.toStringTag]() { + return 'Response'; + } } - -Response.prototype = Object.create(Body.prototype); - -/** - * Clone this response - * - * @return Response - */ -Response.prototype.clone = function() { - return new Response(this._clone(this), { - url: this.url - , status: this.status - , statusText: this.statusText - , headers: this.headers - , ok: this.ok - }); -}; diff --git a/test/server.js b/test/server.js index 08e582d..5a75b29 100644 --- a/test/server.js +++ b/test/server.js @@ -1,337 +1,331 @@ +import 'babel-polyfill'; +import * as http from 'http'; +import { parse } from 'url'; +import * as zlib from 'zlib'; +import * as stream from 'stream'; +import { convert } from 'encoding'; +import { multipart as Multipart } from 'parted'; -var http = require('http'); -var parse = require('url').parse; -var zlib = require('zlib'); -var stream = require('stream'); -var convert = require('encoding').convert; -var Multipart = require('parted').multipart; - -module.exports = TestServer; - -function TestServer() { - this.server = http.createServer(this.router); - this.port = 30001; - this.hostname = 'localhost'; - this.server.on('error', function(err) { - console.log(err.stack); - }); - this.server.on('connection', function(socket) { - socket.setTimeout(1500); - }); -} - -TestServer.prototype.start = function(cb) { - this.server.listen(this.port, this.hostname, cb); -} - -TestServer.prototype.stop = function(cb) { - this.server.close(cb); -} - -TestServer.prototype.router = function(req, res) { - - var p = parse(req.url).pathname; - - if (p === '/hello') { - res.statusCode = 200; - res.setHeader('Content-Type', 'text/plain'); - res.end('world'); - } - - if (p === '/plain') { - res.statusCode = 200; - res.setHeader('Content-Type', 'text/plain'); - res.end('text'); - } - - if (p === '/options') { - res.statusCode = 200; - res.setHeader('Allow', 'GET, HEAD, OPTIONS'); - res.end('hello world'); - } - - if (p === '/html') { - res.statusCode = 200; - res.setHeader('Content-Type', 'text/html'); - res.end(''); - } - - if (p === '/json') { - res.statusCode = 200; - res.setHeader('Content-Type', 'application/json'); - res.end(JSON.stringify({ - name: 'value' - })); - } - - if (p === '/gzip') { - res.statusCode = 200; - res.setHeader('Content-Type', 'text/plain'); - res.setHeader('Content-Encoding', 'gzip'); - zlib.gzip('hello world', function(err, buffer) { - res.end(buffer); +export default class TestServer { + constructor() { + this.server = http.createServer(this.router); + this.port = 30001; + this.hostname = 'localhost'; + this.server.on('error', function(err) { + console.log(err.stack); + }); + this.server.on('connection', function(socket) { + socket.setTimeout(1500); }); } - if (p === '/deflate') { - res.statusCode = 200; - res.setHeader('Content-Type', 'text/plain'); - res.setHeader('Content-Encoding', 'deflate'); - zlib.deflate('hello world', function(err, buffer) { - res.end(buffer); - }); + start(cb) { + this.server.listen(this.port, this.hostname, cb); } - if (p === '/deflate-raw') { - res.statusCode = 200; - res.setHeader('Content-Type', 'text/plain'); - res.setHeader('Content-Encoding', 'deflate'); - zlib.deflateRaw('hello world', function(err, buffer) { - res.end(buffer); - }); + stop(cb) { + this.server.close(cb); } - if (p === '/sdch') { - res.statusCode = 200; - res.setHeader('Content-Type', 'text/plain'); - res.setHeader('Content-Encoding', 'sdch'); - res.end('fake sdch string'); - } + router(req, res) { + let p = parse(req.url).pathname; - if (p === '/invalid-content-encoding') { - res.statusCode = 200; - res.setHeader('Content-Type', 'text/plain'); - res.setHeader('Content-Encoding', 'gzip'); - res.end('fake gzip string'); - } + if (p === '/hello') { + res.statusCode = 200; + res.setHeader('Content-Type', 'text/plain'); + res.end('world'); + } - if (p === '/timeout') { - setTimeout(function() { + if (p === '/plain') { res.statusCode = 200; res.setHeader('Content-Type', 'text/plain'); res.end('text'); - }, 1000); - } + } - if (p === '/slow') { - res.statusCode = 200; - res.setHeader('Content-Type', 'text/plain'); - res.write('test'); - setTimeout(function() { - res.end('test'); - }, 1000); - } + if (p === '/options') { + res.statusCode = 200; + res.setHeader('Allow', 'GET, HEAD, OPTIONS'); + res.end('hello world'); + } - if (p === '/cookie') { - res.statusCode = 200; - res.setHeader('Set-Cookie', ['a=1', 'b=1']); - res.end('cookie'); - } + if (p === '/html') { + res.statusCode = 200; + res.setHeader('Content-Type', 'text/html'); + res.end(''); + } - if (p === '/size/chunk') { - res.statusCode = 200; - res.setHeader('Content-Type', 'text/plain'); - setTimeout(function() { + if (p === '/json') { + res.statusCode = 200; + res.setHeader('Content-Type', 'application/json'); + res.end(JSON.stringify({ + name: 'value' + })); + } + + if (p === '/gzip') { + res.statusCode = 200; + res.setHeader('Content-Type', 'text/plain'); + res.setHeader('Content-Encoding', 'gzip'); + zlib.gzip('hello world', function(err, buffer) { + res.end(buffer); + }); + } + + if (p === '/deflate') { + res.statusCode = 200; + res.setHeader('Content-Type', 'text/plain'); + res.setHeader('Content-Encoding', 'deflate'); + zlib.deflate('hello world', function(err, buffer) { + res.end(buffer); + }); + } + + if (p === '/deflate-raw') { + res.statusCode = 200; + res.setHeader('Content-Type', 'text/plain'); + res.setHeader('Content-Encoding', 'deflate'); + zlib.deflateRaw('hello world', function(err, buffer) { + res.end(buffer); + }); + } + + if (p === '/sdch') { + res.statusCode = 200; + res.setHeader('Content-Type', 'text/plain'); + res.setHeader('Content-Encoding', 'sdch'); + res.end('fake sdch string'); + } + + if (p === '/invalid-content-encoding') { + res.statusCode = 200; + res.setHeader('Content-Type', 'text/plain'); + res.setHeader('Content-Encoding', 'gzip'); + res.end('fake gzip string'); + } + + if (p === '/timeout') { + setTimeout(function() { + res.statusCode = 200; + res.setHeader('Content-Type', 'text/plain'); + res.end('text'); + }, 1000); + } + + if (p === '/slow') { + res.statusCode = 200; + res.setHeader('Content-Type', 'text/plain'); res.write('test'); - }, 50); - setTimeout(function() { - res.end('test'); - }, 100); - } - - if (p === '/size/long') { - res.statusCode = 200; - res.setHeader('Content-Type', 'text/plain'); - res.end('testtest'); - } - - if (p === '/encoding/gbk') { - res.statusCode = 200; - res.setHeader('Content-Type', 'text/html'); - res.end(convert('
中文
', 'gbk')); - } - - if (p === '/encoding/gb2312') { - res.statusCode = 200; - res.setHeader('Content-Type', 'text/html'); - res.end(convert('
中文
', 'gb2312')); - } - - if (p === '/encoding/shift-jis') { - res.statusCode = 200; - res.setHeader('Content-Type', 'text/html; charset=Shift-JIS'); - res.end(convert('
日本語
', 'Shift_JIS')); - } - - if (p === '/encoding/euc-jp') { - res.statusCode = 200; - res.setHeader('Content-Type', 'text/xml'); - res.end(convert('日本語', 'EUC-JP')); - } - - if (p === '/encoding/utf8') { - res.statusCode = 200; - res.end('中文'); - } - - if (p === '/encoding/order1') { - res.statusCode = 200; - res.setHeader('Content-Type', 'charset=gbk; text/plain'); - res.end(convert('中文', 'gbk')); - } - - if (p === '/encoding/order2') { - res.statusCode = 200; - res.setHeader('Content-Type', 'text/plain; charset=gbk; qs=1'); - res.end(convert('中文', 'gbk')); - } - - if (p === '/encoding/chunked') { - res.statusCode = 200; - res.setHeader('Content-Type', 'text/html'); - res.setHeader('Transfer-Encoding', 'chunked'); - var padding = 'a'; - for (var i = 0; i < 10; i++) { - res.write(padding); + setTimeout(function() { + res.end('test'); + }, 1000); } - res.end(convert('
日本語
', 'Shift_JIS')); - } - if (p === '/encoding/invalid') { - res.statusCode = 200; - res.setHeader('Content-Type', 'text/html'); - res.setHeader('Transfer-Encoding', 'chunked'); - // because node v0.12 doesn't have str.repeat - var padding = new Array(120 + 1).join('a'); - for (var i = 0; i < 10; i++) { - res.write(padding); + if (p === '/cookie') { + res.statusCode = 200; + res.setHeader('Set-Cookie', ['a=1', 'b=1']); + res.end('cookie'); } - res.end(convert('中文', 'gbk')); - } - if (p === '/redirect/301') { - res.statusCode = 301; - res.setHeader('Location', '/inspect'); - res.end(); - } + if (p === '/size/chunk') { + res.statusCode = 200; + res.setHeader('Content-Type', 'text/plain'); + setTimeout(function() { + res.write('test'); + }, 50); + setTimeout(function() { + res.end('test'); + }, 100); + } - if (p === '/redirect/302') { - res.statusCode = 302; - res.setHeader('Location', '/inspect'); - res.end(); - } + if (p === '/size/long') { + res.statusCode = 200; + res.setHeader('Content-Type', 'text/plain'); + res.end('testtest'); + } - if (p === '/redirect/303') { - res.statusCode = 303; - res.setHeader('Location', '/inspect'); - res.end(); - } + if (p === '/encoding/gbk') { + res.statusCode = 200; + res.setHeader('Content-Type', 'text/html'); + res.end(convert('
中文
', 'gbk')); + } - if (p === '/redirect/307') { - res.statusCode = 307; - res.setHeader('Location', '/inspect'); - res.end(); - } + if (p === '/encoding/gb2312') { + res.statusCode = 200; + res.setHeader('Content-Type', 'text/html'); + res.end(convert('
中文
', 'gb2312')); + } - if (p === '/redirect/308') { - res.statusCode = 308; - res.setHeader('Location', '/inspect'); - res.end(); - } + if (p === '/encoding/shift-jis') { + res.statusCode = 200; + res.setHeader('Content-Type', 'text/html; charset=Shift-JIS'); + res.end(convert('
日本語
', 'Shift_JIS')); + } - if (p === '/redirect/chain') { - res.statusCode = 301; - res.setHeader('Location', '/redirect/301'); - res.end(); - } + if (p === '/encoding/euc-jp') { + res.statusCode = 200; + res.setHeader('Content-Type', 'text/xml'); + res.end(convert('日本語', 'EUC-JP')); + } - if (p === '/error/redirect') { - res.statusCode = 301; - //res.setHeader('Location', '/inspect'); - res.end(); - } + if (p === '/encoding/utf8') { + res.statusCode = 200; + res.end('中文'); + } - if (p === '/error/400') { - res.statusCode = 400; - res.setHeader('Content-Type', 'text/plain'); - res.end('client error'); - } + if (p === '/encoding/order1') { + res.statusCode = 200; + res.setHeader('Content-Type', 'charset=gbk; text/plain'); + res.end(convert('中文', 'gbk')); + } - if (p === '/error/404') { - res.statusCode = 404; - res.setHeader('Content-Encoding', 'gzip'); - res.end(); - } + if (p === '/encoding/order2') { + res.statusCode = 200; + res.setHeader('Content-Type', 'text/plain; charset=gbk; qs=1'); + res.end(convert('中文', 'gbk')); + } - if (p === '/error/500') { - res.statusCode = 500; - res.setHeader('Content-Type', 'text/plain'); - res.end('server error'); - } + if (p === '/encoding/chunked') { + res.statusCode = 200; + res.setHeader('Content-Type', 'text/html'); + res.setHeader('Transfer-Encoding', 'chunked'); + const padding = 'a'; + res.write(padding.repeat(10)); + res.end(convert('
日本語
', 'Shift_JIS')); + } - if (p === '/error/reset') { - res.destroy(); - } + if (p === '/encoding/invalid') { + res.statusCode = 200; + res.setHeader('Content-Type', 'text/html'); + res.setHeader('Transfer-Encoding', 'chunked'); + const padding = 'a'.repeat(120); + res.write(padding.repeat(10)); + res.end(convert('中文', 'gbk')); + } - if (p === '/error/json') { - res.statusCode = 200; - res.setHeader('Content-Type', 'application/json'); - res.end('invalid json'); - } + if (p === '/redirect/301') { + res.statusCode = 301; + res.setHeader('Location', '/inspect'); + res.end(); + } - if (p === '/no-content') { - res.statusCode = 204; - res.end(); - } + if (p === '/redirect/302') { + res.statusCode = 302; + res.setHeader('Location', '/inspect'); + res.end(); + } - if (p === '/no-content/gzip') { - res.statusCode = 204; - res.setHeader('Content-Encoding', 'gzip'); - res.end(); - } + if (p === '/redirect/303') { + res.statusCode = 303; + res.setHeader('Location', '/inspect'); + res.end(); + } - if (p === '/not-modified') { - res.statusCode = 304; - res.end(); - } + if (p === '/redirect/307') { + res.statusCode = 307; + res.setHeader('Location', '/inspect'); + res.end(); + } - if (p === '/not-modified/gzip') { - res.statusCode = 304; - res.setHeader('Content-Encoding', 'gzip'); - res.end(); - } + if (p === '/redirect/308') { + res.statusCode = 308; + res.setHeader('Location', '/inspect'); + res.end(); + } - if (p === '/inspect') { - res.statusCode = 200; - res.setHeader('Content-Type', 'application/json'); - var body = ''; - req.on('data', function(c) { body += c }); - req.on('end', function() { - res.end(JSON.stringify({ - method: req.method, - url: req.url, - headers: req.headers, - body: body - })); - }); - } + if (p === '/redirect/chain') { + res.statusCode = 301; + res.setHeader('Location', '/redirect/301'); + res.end(); + } - if (p === '/multipart') { - res.statusCode = 200; - res.setHeader('Content-Type', 'application/json'); - var parser = new Multipart(req.headers['content-type']); - var body = ''; - parser.on('part', function(field, part) { - body += field + '=' + part; - }); - parser.on('end', function() { - res.end(JSON.stringify({ - method: req.method, - url: req.url, - headers: req.headers, - body: body - })); - }); - req.pipe(parser); + if (p === '/error/redirect') { + res.statusCode = 301; + //res.setHeader('Location', '/inspect'); + res.end(); + } + + if (p === '/error/400') { + res.statusCode = 400; + res.setHeader('Content-Type', 'text/plain'); + res.end('client error'); + } + + if (p === '/error/404') { + res.statusCode = 404; + res.setHeader('Content-Encoding', 'gzip'); + res.end(); + } + + if (p === '/error/500') { + res.statusCode = 500; + res.setHeader('Content-Type', 'text/plain'); + res.end('server error'); + } + + if (p === '/error/reset') { + res.destroy(); + } + + if (p === '/error/json') { + res.statusCode = 200; + res.setHeader('Content-Type', 'application/json'); + res.end('invalid json'); + } + + if (p === '/no-content') { + res.statusCode = 204; + res.end(); + } + + if (p === '/no-content/gzip') { + res.statusCode = 204; + res.setHeader('Content-Encoding', 'gzip'); + res.end(); + } + + if (p === '/not-modified') { + res.statusCode = 304; + res.end(); + } + + if (p === '/not-modified/gzip') { + res.statusCode = 304; + res.setHeader('Content-Encoding', 'gzip'); + res.end(); + } + + if (p === '/inspect') { + res.statusCode = 200; + res.setHeader('Content-Type', 'application/json'); + let body = ''; + req.on('data', function(c) { body += c }); + req.on('end', function() { + res.end(JSON.stringify({ + method: req.method, + url: req.url, + headers: req.headers, + body + })); + }); + } + + if (p === '/multipart') { + res.statusCode = 200; + res.setHeader('Content-Type', 'application/json'); + const parser = new Multipart(req.headers['content-type']); + let body = ''; + parser.on('part', function(field, part) { + body += field + '=' + part; + }); + parser.on('end', function() { + res.end(JSON.stringify({ + method: req.method, + url: req.url, + headers: req.headers, + body: body + })); + }); + req.pipe(parser); + } } } diff --git a/test/test.js b/test/test.js index b593407..a1ea39a 100644 --- a/test/test.js +++ b/test/test.js @@ -1,54 +1,55 @@ // test tools -var chai = require('chai'); -var cap = require('chai-as-promised'); -chai.use(cap); -var expect = chai.expect; -var bluebird = require('bluebird'); -var then = require('promise'); -var spawn = require('child_process').spawn; -var stream = require('stream'); -var resumer = require('resumer'); -var FormData = require('form-data'); -var http = require('http'); -var fs = require('fs'); +import chai from 'chai'; +import cap from 'chai-as-promised'; +import bluebird from 'bluebird'; +import then from 'promise'; +import {spawn} from 'child_process'; +import * as stream from 'stream'; +import resumer from 'resumer'; +import FormData from 'form-data'; +import * as http from 'http'; +import * as fs from 'fs'; -var TestServer = require('./server'); +chai.use(cap); +const expect = chai.expect; + +import TestServer from './server'; // test subjects -var fetch = require('../src/index.js'); +import fetch from '../src/index.js'; import Headers from '../src/headers.js'; -var Response = require('../src/response.js'); -var Request = require('../src/request.js'); -var Body = require('../src/body.js'); -var FetchError = require('../src/fetch-error.js'); +import Response from '../src/response.js'; +import Request from '../src/request.js'; +import Body from '../src/body.js'; +import FetchError from '../src/fetch-error.js'; // test with native promise on node 0.11, and bluebird for node 0.10 fetch.Promise = fetch.Promise || bluebird; -var url, opts, local, base; +let url, opts, local, base; -describe('node-fetch', function() { +describe('node-fetch', () => { - before(function(done) { + before(done => { local = new TestServer(); base = 'http://' + local.hostname + ':' + local.port; local.start(done); }); - after(function(done) { + after(done => { local.stop(done); }); it('should return a promise', function() { url = 'http://example.com/'; - var p = fetch(url); + const p = fetch(url); expect(p).to.be.an.instanceof(fetch.Promise); expect(p).to.have.property('then'); }); it('should allow custom promise', function() { url = 'http://example.com/'; - var old = fetch.Promise; + const old = fetch.Promise; fetch.Promise = then; expect(fetch(url)).to.be.an.instanceof(then); expect(fetch(url)).to.not.be.an.instanceof(bluebird); @@ -57,9 +58,9 @@ describe('node-fetch', function() { it('should throw error when no promise implementation are found', function() { url = 'http://example.com/'; - var old = fetch.Promise; + const old = fetch.Promise; fetch.Promise = undefined; - expect(function() { + expect(() => { fetch(url) }).to.throw(Error); fetch.Promise = old; @@ -94,8 +95,8 @@ describe('node-fetch', function() { }); it('should resolve into response', function() { - url = base + '/hello'; - return fetch(url).then(function(res) { + url = `${base}/hello`; + return fetch(url).then(res => { expect(res).to.be.an.instanceof(Response); expect(res.headers).to.be.an.instanceof(Headers); expect(res.body).to.be.an.instanceof(stream.Transform); @@ -109,10 +110,10 @@ describe('node-fetch', function() { }); it('should accept plain text response', function() { - url = base + '/plain'; - return fetch(url).then(function(res) { + url = `${base}/plain`; + return fetch(url).then(res => { expect(res.headers.get('content-type')).to.equal('text/plain'); - return res.text().then(function(result) { + return res.text().then(result => { expect(res.bodyUsed).to.be.true; expect(result).to.be.a('string'); expect(result).to.equal('text'); @@ -121,10 +122,10 @@ describe('node-fetch', function() { }); it('should accept html response (like plain text)', function() { - url = base + '/html'; - return fetch(url).then(function(res) { + url = `${base}/html`; + return fetch(url).then(res => { expect(res.headers.get('content-type')).to.equal('text/html'); - return res.text().then(function(result) { + return res.text().then(result => { expect(res.bodyUsed).to.be.true; expect(result).to.be.a('string'); expect(result).to.equal(''); @@ -133,10 +134,10 @@ describe('node-fetch', function() { }); it('should accept json response', function() { - url = base + '/json'; - return fetch(url).then(function(res) { + url = `${base}/json`; + return fetch(url).then(res => { expect(res.headers.get('content-type')).to.equal('application/json'); - return res.json().then(function(result) { + return res.json().then(result => { expect(res.bodyUsed).to.be.true; expect(result).to.be.an('object'); expect(result).to.deep.equal({ name: 'value' }); @@ -145,102 +146,102 @@ describe('node-fetch', function() { }); it('should send request with custom headers', function() { - url = base + '/inspect'; + url = `${base}/inspect`; opts = { headers: { 'x-custom-header': 'abc' } }; - return fetch(url, opts).then(function(res) { + return fetch(url, opts).then(res => { return res.json(); - }).then(function(res) { + }).then(res => { expect(res.headers['x-custom-header']).to.equal('abc'); }); }); it('should accept headers instance', function() { - url = base + '/inspect'; + url = `${base}/inspect`; opts = { headers: new Headers({ 'x-custom-header': 'abc' }) }; - return fetch(url, opts).then(function(res) { + return fetch(url, opts).then(res => { return res.json(); - }).then(function(res) { + }).then(res => { expect(res.headers['x-custom-header']).to.equal('abc'); }); }); it('should accept custom host header', function() { - url = base + '/inspect'; + url = `${base}/inspect`; opts = { headers: { host: 'example.com' } }; - return fetch(url, opts).then(function(res) { + return fetch(url, opts).then(res => { return res.json(); - }).then(function(res) { + }).then(res => { expect(res.headers['host']).to.equal('example.com'); }); }); it('should follow redirect code 301', function() { - url = base + '/redirect/301'; - return fetch(url).then(function(res) { - expect(res.url).to.equal(base + '/inspect'); + url = `${base}/redirect/301`; + return fetch(url).then(res => { + expect(res.url).to.equal(`${base}/inspect`); expect(res.status).to.equal(200); expect(res.ok).to.be.true; }); }); it('should follow redirect code 302', function() { - url = base + '/redirect/302'; - return fetch(url).then(function(res) { - expect(res.url).to.equal(base + '/inspect'); + url = `${base}/redirect/302`; + return fetch(url).then(res => { + expect(res.url).to.equal(`${base}/inspect`); expect(res.status).to.equal(200); }); }); it('should follow redirect code 303', function() { - url = base + '/redirect/303'; - return fetch(url).then(function(res) { - expect(res.url).to.equal(base + '/inspect'); + url = `${base}/redirect/303`; + return fetch(url).then(res => { + expect(res.url).to.equal(`${base}/inspect`); expect(res.status).to.equal(200); }); }); it('should follow redirect code 307', function() { - url = base + '/redirect/307'; - return fetch(url).then(function(res) { - expect(res.url).to.equal(base + '/inspect'); + url = `${base}/redirect/307`; + return fetch(url).then(res => { + expect(res.url).to.equal(`${base}/inspect`); expect(res.status).to.equal(200); }); }); it('should follow redirect code 308', function() { - url = base + '/redirect/308'; - return fetch(url).then(function(res) { - expect(res.url).to.equal(base + '/inspect'); + url = `${base}/redirect/308`; + return fetch(url).then(res => { + expect(res.url).to.equal(`${base}/inspect`); expect(res.status).to.equal(200); }); }); it('should follow redirect chain', function() { - url = base + '/redirect/chain'; - return fetch(url).then(function(res) { - expect(res.url).to.equal(base + '/inspect'); + url = `${base}/redirect/chain`; + return fetch(url).then(res => { + expect(res.url).to.equal(`${base}/inspect`); expect(res.status).to.equal(200); }); }); it('should follow POST request redirect code 301 with GET', function() { - url = base + '/redirect/301'; + url = `${base}/redirect/301`; opts = { method: 'POST' , body: 'a=1' }; - return fetch(url, opts).then(function(res) { - expect(res.url).to.equal(base + '/inspect'); + return fetch(url, opts).then(res => { + expect(res.url).to.equal(`${base}/inspect`); expect(res.status).to.equal(200); - return res.json().then(function(result) { + return res.json().then(result => { expect(result.method).to.equal('GET'); expect(result.body).to.equal(''); }); @@ -248,15 +249,15 @@ describe('node-fetch', function() { }); it('should follow POST request redirect code 302 with GET', function() { - url = base + '/redirect/302'; + url = `${base}/redirect/302`; opts = { method: 'POST' , body: 'a=1' }; - return fetch(url, opts).then(function(res) { - expect(res.url).to.equal(base + '/inspect'); + return fetch(url, opts).then(res => { + expect(res.url).to.equal(`${base}/inspect`); expect(res.status).to.equal(200); - return res.json().then(function(result) { + return res.json().then(result => { expect(result.method).to.equal('GET'); expect(result.body).to.equal(''); }); @@ -264,15 +265,15 @@ describe('node-fetch', function() { }); it('should follow redirect code 303 with GET', function() { - url = base + '/redirect/303'; + url = `${base}/redirect/303`; opts = { method: 'PUT' , body: 'a=1' }; - return fetch(url, opts).then(function(res) { - expect(res.url).to.equal(base + '/inspect'); + return fetch(url, opts).then(res => { + expect(res.url).to.equal(`${base}/inspect`); expect(res.status).to.equal(200); - return res.json().then(function(result) { + return res.json().then(result => { expect(result.method).to.equal('GET'); expect(result.body).to.equal(''); }); @@ -280,7 +281,7 @@ describe('node-fetch', function() { }); it('should obey maximum redirect, reject case', function() { - url = base + '/redirect/chain'; + url = `${base}/redirect/chain`; opts = { follow: 1 } @@ -290,18 +291,18 @@ describe('node-fetch', function() { }); it('should obey redirect chain, resolve case', function() { - url = base + '/redirect/chain'; + url = `${base}/redirect/chain`; opts = { follow: 2 } - return fetch(url, opts).then(function(res) { - expect(res.url).to.equal(base + '/inspect'); + return fetch(url, opts).then(res => { + expect(res.url).to.equal(`${base}/inspect`); expect(res.status).to.equal(200); }); }); it('should allow not following redirect', function() { - url = base + '/redirect/301'; + url = `${base}/redirect/301`; opts = { follow: 0 } @@ -311,19 +312,19 @@ describe('node-fetch', function() { }); it('should support redirect mode, manual flag', function() { - url = base + '/redirect/301'; + url = `${base}/redirect/301`; opts = { redirect: 'manual' }; - return fetch(url, opts).then(function(res) { + return fetch(url, opts).then(res => { expect(res.url).to.equal(url); expect(res.status).to.equal(301); - expect(res.headers.get('location')).to.equal(base + '/inspect'); + expect(res.headers.get('location')).to.equal(`${base}/inspect`); }); }); it('should support redirect mode, error flag', function() { - url = base + '/redirect/301'; + url = `${base}/redirect/301`; opts = { redirect: 'error' }; @@ -333,11 +334,11 @@ describe('node-fetch', function() { }); it('should support redirect mode, manual flag when there is no redirect', function() { - url = base + '/hello'; + url = `${base}/hello`; opts = { redirect: 'manual' }; - return fetch(url, opts).then(function(res) { + return fetch(url, opts).then(res => { expect(res.url).to.equal(url); expect(res.status).to.equal(200); expect(res.headers.get('location')).to.be.null; @@ -345,31 +346,31 @@ describe('node-fetch', function() { }); it('should follow redirect code 301 and keep existing headers', function() { - url = base + '/redirect/301'; + url = `${base}/redirect/301`; opts = { headers: new Headers({ 'x-custom-header': 'abc' }) }; - return fetch(url, opts).then(function(res) { - expect(res.url).to.equal(base + '/inspect'); + return fetch(url, opts).then(res => { + expect(res.url).to.equal(`${base}/inspect`); return res.json(); - }).then(function(res) { + }).then(res => { expect(res.headers['x-custom-header']).to.equal('abc'); }); }); it('should reject broken redirect', function() { - url = base + '/error/redirect'; + url = `${base}/error/redirect`; return expect(fetch(url)).to.eventually.be.rejected .and.be.an.instanceOf(FetchError) .and.have.property('type', 'invalid-redirect'); }); it('should not reject broken redirect under manual redirect', function() { - url = base + '/error/redirect'; + url = `${base}/error/redirect`; opts = { redirect: 'manual' }; - return fetch(url, opts).then(function(res) { + return fetch(url, opts).then(res => { expect(res.url).to.equal(url); expect(res.status).to.equal(301); expect(res.headers.get('location')).to.be.null; @@ -377,13 +378,13 @@ describe('node-fetch', function() { }); it('should handle client-error response', function() { - url = base + '/error/400'; - return fetch(url).then(function(res) { + url = `${base}/error/400`; + return fetch(url).then(res => { expect(res.headers.get('content-type')).to.equal('text/plain'); expect(res.status).to.equal(400); expect(res.statusText).to.equal('Bad Request'); expect(res.ok).to.be.false; - return res.text().then(function(result) { + return res.text().then(result => { expect(res.bodyUsed).to.be.true; expect(result).to.be.a('string'); expect(result).to.equal('client error'); @@ -392,13 +393,13 @@ describe('node-fetch', function() { }); it('should handle server-error response', function() { - url = base + '/error/500'; - return fetch(url).then(function(res) { + url = `${base}/error/500`; + return fetch(url).then(res => { expect(res.headers.get('content-type')).to.equal('text/plain'); expect(res.status).to.equal(500); expect(res.statusText).to.equal('Internal Server Error'); expect(res.ok).to.be.false; - return res.text().then(function(result) { + return res.text().then(result => { expect(res.bodyUsed).to.be.true; expect(result).to.be.a('string'); expect(result).to.equal('server error'); @@ -407,7 +408,7 @@ describe('node-fetch', function() { }); it('should handle network-error response', function() { - url = base + '/error/reset'; + url = `${base}/error/reset`; return expect(fetch(url)).to.eventually.be.rejected .and.be.an.instanceOf(FetchError) .and.have.property('code', 'ECONNRESET'); @@ -421,20 +422,20 @@ describe('node-fetch', function() { }); it('should reject invalid json response', function() { - url = base + '/error/json'; - return fetch(url).then(function(res) { + url = `${base}/error/json`; + return fetch(url).then(res => { expect(res.headers.get('content-type')).to.equal('application/json'); return expect(res.json()).to.eventually.be.rejectedWith(Error); }); }); it('should handle no content response', function() { - url = base + '/no-content'; - return fetch(url).then(function(res) { + url = `${base}/no-content`; + return fetch(url).then(res => { expect(res.status).to.equal(204); expect(res.statusText).to.equal('No Content'); expect(res.ok).to.be.true; - return res.text().then(function(result) { + return res.text().then(result => { expect(result).to.be.a('string'); expect(result).to.be.empty; }); @@ -442,9 +443,9 @@ describe('node-fetch', function() { }); it('should return empty object on no-content response', function() { - url = base + '/no-content'; - return fetch(url).then(function(res) { - return res.json().then(function(result) { + url = `${base}/no-content`; + return fetch(url).then(res => { + return res.json().then(result => { expect(result).to.be.an('object'); expect(result).to.be.empty; }); @@ -452,13 +453,13 @@ describe('node-fetch', function() { }); it('should handle no content response with gzip encoding', function() { - url = base + '/no-content/gzip'; - return fetch(url).then(function(res) { + url = `${base}/no-content/gzip`; + return fetch(url).then(res => { expect(res.status).to.equal(204); expect(res.statusText).to.equal('No Content'); expect(res.headers.get('content-encoding')).to.equal('gzip'); expect(res.ok).to.be.true; - return res.text().then(function(result) { + return res.text().then(result => { expect(result).to.be.a('string'); expect(result).to.be.empty; }); @@ -466,12 +467,12 @@ describe('node-fetch', function() { }); it('should handle not modified response', function() { - url = base + '/not-modified'; - return fetch(url).then(function(res) { + url = `${base}/not-modified`; + return fetch(url).then(res => { expect(res.status).to.equal(304); expect(res.statusText).to.equal('Not Modified'); expect(res.ok).to.be.false; - return res.text().then(function(result) { + return res.text().then(result => { expect(result).to.be.a('string'); expect(result).to.be.empty; }); @@ -479,13 +480,13 @@ describe('node-fetch', function() { }); it('should handle not modified response with gzip encoding', function() { - url = base + '/not-modified/gzip'; - return fetch(url).then(function(res) { + url = `${base}/not-modified/gzip`; + return fetch(url).then(res => { expect(res.status).to.equal(304); expect(res.statusText).to.equal('Not Modified'); expect(res.headers.get('content-encoding')).to.equal('gzip'); expect(res.ok).to.be.false; - return res.text().then(function(result) { + return res.text().then(result => { expect(result).to.be.a('string'); expect(result).to.be.empty; }); @@ -493,10 +494,10 @@ describe('node-fetch', function() { }); it('should decompress gzip response', function() { - url = base + '/gzip'; - return fetch(url).then(function(res) { + url = `${base}/gzip`; + return fetch(url).then(res => { expect(res.headers.get('content-type')).to.equal('text/plain'); - return res.text().then(function(result) { + return res.text().then(result => { expect(result).to.be.a('string'); expect(result).to.equal('hello world'); }); @@ -504,10 +505,10 @@ describe('node-fetch', function() { }); it('should decompress deflate response', function() { - url = base + '/deflate'; - return fetch(url).then(function(res) { + url = `${base}/deflate`; + return fetch(url).then(res => { expect(res.headers.get('content-type')).to.equal('text/plain'); - return res.text().then(function(result) { + return res.text().then(result => { expect(result).to.be.a('string'); expect(result).to.equal('hello world'); }); @@ -515,10 +516,10 @@ describe('node-fetch', function() { }); it('should decompress deflate raw response from old apache server', function() { - url = base + '/deflate-raw'; - return fetch(url).then(function(res) { + url = `${base}/deflate-raw`; + return fetch(url).then(res => { expect(res.headers.get('content-type')).to.equal('text/plain'); - return res.text().then(function(result) { + return res.text().then(result => { expect(result).to.be.a('string'); expect(result).to.equal('hello world'); }); @@ -526,10 +527,10 @@ describe('node-fetch', function() { }); it('should skip decompression if unsupported', function() { - url = base + '/sdch'; - return fetch(url).then(function(res) { + url = `${base}/sdch`; + return fetch(url).then(res => { expect(res.headers.get('content-type')).to.equal('text/plain'); - return res.text().then(function(result) { + return res.text().then(result => { expect(result).to.be.a('string'); expect(result).to.equal('fake sdch string'); }); @@ -537,8 +538,8 @@ describe('node-fetch', function() { }); it('should reject if response compression is invalid', function() { - url = base + '/invalid-content-encoding'; - return fetch(url).then(function(res) { + url = `${base}/invalid-content-encoding`; + return fetch(url).then(res => { expect(res.headers.get('content-type')).to.equal('text/plain'); return expect(res.text()).to.eventually.be.rejected .and.be.an.instanceOf(FetchError) @@ -547,13 +548,13 @@ describe('node-fetch', function() { }); it('should allow disabling auto decompression', function() { - url = base + '/gzip'; + url = `${base}/gzip`; opts = { compress: false }; - return fetch(url, opts).then(function(res) { + return fetch(url, opts).then(res => { expect(res.headers.get('content-type')).to.equal('text/plain'); - return res.text().then(function(result) { + return res.text().then(result => { expect(result).to.be.a('string'); expect(result).to.not.equal('hello world'); }); @@ -562,7 +563,7 @@ describe('node-fetch', function() { it('should allow custom timeout', function() { this.timeout(500); - url = base + '/timeout'; + url = `${base}/timeout`; opts = { timeout: 100 }; @@ -573,11 +574,11 @@ describe('node-fetch', function() { it('should allow custom timeout on response body', function() { this.timeout(500); - url = base + '/slow'; + url = `${base}/slow`; opts = { timeout: 100 }; - return fetch(url, opts).then(function(res) { + return fetch(url, opts).then(res => { expect(res.ok).to.be.true; return expect(res.text()).to.eventually.be.rejected .and.be.an.instanceOf(FetchError) @@ -587,36 +588,36 @@ describe('node-fetch', function() { it('should clear internal timeout on fetch response', function (done) { this.timeout(1000); - spawn('node', ['-e', 'require("./")("' + base + '/hello", { timeout: 5000 })']) - .on('exit', function () { + spawn('node', ['-e', `require('./')('${base}/hello', { timeout: 5000 })`]) + .on('exit', () => { done(); }); }); it('should clear internal timeout on fetch redirect', function (done) { this.timeout(1000); - spawn('node', ['-e', 'require("./")("' + base + '/redirect/301", { timeout: 5000 })']) - .on('exit', function () { + spawn('node', ['-e', `require('./')('${base}/redirect/301', { timeout: 5000 })`]) + .on('exit', () => { done(); }); }); it('should clear internal timeout on fetch error', function (done) { this.timeout(1000); - spawn('node', ['-e', 'require("./")("' + base + '/error/reset", { timeout: 5000 })']) - .on('exit', function () { + spawn('node', ['-e', `require('./')('${base}/error/reset', { timeout: 5000 })`]) + .on('exit', () => { done(); }); }); it('should allow POST request', function() { - url = base + '/inspect'; + url = `${base}/inspect`; opts = { method: 'POST' }; - return fetch(url, opts).then(function(res) { + return fetch(url, opts).then(res => { return res.json(); - }).then(function(res) { + }).then(res => { expect(res.method).to.equal('POST'); expect(res.headers['transfer-encoding']).to.be.undefined; expect(res.headers['content-length']).to.equal('0'); @@ -624,14 +625,14 @@ describe('node-fetch', function() { }); it('should allow POST request with string body', function() { - url = base + '/inspect'; + url = `${base}/inspect`; opts = { method: 'POST' , body: 'a=1' }; - return fetch(url, opts).then(function(res) { + return fetch(url, opts).then(res => { return res.json(); - }).then(function(res) { + }).then(res => { expect(res.method).to.equal('POST'); expect(res.body).to.equal('a=1'); expect(res.headers['transfer-encoding']).to.be.undefined; @@ -640,14 +641,14 @@ describe('node-fetch', function() { }); it('should allow POST request with buffer body', function() { - url = base + '/inspect'; + url = `${base}/inspect`; opts = { method: 'POST' , body: new Buffer('a=1', 'utf-8') }; - return fetch(url, opts).then(function(res) { + return fetch(url, opts).then(res => { return res.json(); - }).then(function(res) { + }).then(res => { expect(res.method).to.equal('POST'); expect(res.body).to.equal('a=1'); expect(res.headers['transfer-encoding']).to.equal('chunked'); @@ -656,17 +657,17 @@ describe('node-fetch', function() { }); it('should allow POST request with readable stream as body', function() { - var body = resumer().queue('a=1').end(); + let body = resumer().queue('a=1').end(); body = body.pipe(new stream.PassThrough()); - url = base + '/inspect'; + url = `${base}/inspect`; opts = { method: 'POST' - , body: body + , body }; - return fetch(url, opts).then(function(res) { + return fetch(url, opts).then(res => { return res.json(); - }).then(function(res) { + }).then(res => { expect(res.method).to.equal('POST'); expect(res.body).to.equal('a=1'); expect(res.headers['transfer-encoding']).to.equal('chunked'); @@ -675,17 +676,17 @@ describe('node-fetch', function() { }); it('should allow POST request with form-data as body', function() { - var form = new FormData(); + const form = new FormData(); form.append('a','1'); - url = base + '/multipart'; + url = `${base}/multipart`; opts = { method: 'POST' , body: form }; - return fetch(url, opts).then(function(res) { + return fetch(url, opts).then(res => { return res.json(); - }).then(function(res) { + }).then(res => { expect(res.method).to.equal('POST'); expect(res.headers['content-type']).to.contain('multipart/form-data'); expect(res.headers['content-length']).to.be.a('string'); @@ -694,18 +695,18 @@ describe('node-fetch', function() { }); it('should allow POST request with form-data using stream as body', function() { - var form = new FormData(); + const form = new FormData(); form.append('my_field', fs.createReadStream('test/dummy.txt')); - url = base + '/multipart'; + url = `${base}/multipart`; opts = { method: 'POST' , body: form }; - return fetch(url, opts).then(function(res) { + return fetch(url, opts).then(res => { return res.json(); - }).then(function(res) { + }).then(res => { expect(res.method).to.equal('POST'); expect(res.headers['content-type']).to.contain('multipart/form-data'); expect(res.headers['content-length']).to.be.undefined; @@ -714,21 +715,21 @@ describe('node-fetch', function() { }); it('should allow POST request with form-data as body and custom headers', function() { - var form = new FormData(); + const form = new FormData(); form.append('a','1'); - var headers = form.getHeaders(); + const headers = form.getHeaders(); headers['b'] = '2'; - url = base + '/multipart'; + url = `${base}/multipart`; opts = { method: 'POST' , body: form - , headers: headers + , headers }; - return fetch(url, opts).then(function(res) { + return fetch(url, opts).then(res => { return res.json(); - }).then(function(res) { + }).then(res => { expect(res.method).to.equal('POST'); expect(res.headers['content-type']).to.contain('multipart/form-data'); expect(res.headers['content-length']).to.be.a('string'); @@ -738,55 +739,55 @@ describe('node-fetch', function() { }); it('should allow POST request with object body', function() { - url = base + '/inspect'; + url = `${base}/inspect`; // note that fetch simply calls tostring on an object opts = { method: 'POST' , body: { a:1 } }; - return fetch(url, opts).then(function(res) { + return fetch(url, opts).then(res => { return res.json(); - }).then(function(res) { + }).then(res => { expect(res.method).to.equal('POST'); expect(res.body).to.equal('[object Object]'); }); }); it('should allow PUT request', function() { - url = base + '/inspect'; + url = `${base}/inspect`; opts = { method: 'PUT' , body: 'a=1' }; - return fetch(url, opts).then(function(res) { + return fetch(url, opts).then(res => { return res.json(); - }).then(function(res) { + }).then(res => { expect(res.method).to.equal('PUT'); expect(res.body).to.equal('a=1'); }); }); it('should allow DELETE request', function() { - url = base + '/inspect'; + url = `${base}/inspect`; opts = { method: 'DELETE' }; - return fetch(url, opts).then(function(res) { + return fetch(url, opts).then(res => { return res.json(); - }).then(function(res) { + }).then(res => { expect(res.method).to.equal('DELETE'); }); }); it('should allow POST request with string body', function() { - url = base + '/inspect'; + url = `${base}/inspect`; opts = { method: 'POST' , body: 'a=1' }; - return fetch(url, opts).then(function(res) { + return fetch(url, opts).then(res => { return res.json(); - }).then(function(res) { + }).then(res => { expect(res.method).to.equal('POST'); expect(res.body).to.equal('a=1'); expect(res.headers['transfer-encoding']).to.be.undefined; @@ -795,14 +796,14 @@ describe('node-fetch', function() { }); it('should allow DELETE request with string body', function() { - url = base + '/inspect'; + url = `${base}/inspect`; opts = { method: 'DELETE' , body: 'a=1' }; - return fetch(url, opts).then(function(res) { + return fetch(url, opts).then(res => { return res.json(); - }).then(function(res) { + }).then(res => { expect(res.method).to.equal('DELETE'); expect(res.body).to.equal('a=1'); expect(res.headers['transfer-encoding']).to.be.undefined; @@ -811,55 +812,55 @@ describe('node-fetch', function() { }); it('should allow PATCH request', function() { - url = base + '/inspect'; + url = `${base}/inspect`; opts = { method: 'PATCH' , body: 'a=1' }; - return fetch(url, opts).then(function(res) { + return fetch(url, opts).then(res => { return res.json(); - }).then(function(res) { + }).then(res => { expect(res.method).to.equal('PATCH'); expect(res.body).to.equal('a=1'); }); }); it('should allow HEAD request', function() { - url = base + '/hello'; + url = `${base}/hello`; opts = { method: 'HEAD' }; - return fetch(url, opts).then(function(res) { + return fetch(url, opts).then(res => { expect(res.status).to.equal(200); expect(res.statusText).to.equal('OK'); expect(res.headers.get('content-type')).to.equal('text/plain'); expect(res.body).to.be.an.instanceof(stream.Transform); return res.text(); - }).then(function(text) { + }).then(text => { expect(text).to.equal(''); }); }); it('should allow HEAD request with content-encoding header', function() { - url = base + '/error/404'; + url = `${base}/error/404`; opts = { method: 'HEAD' }; - return fetch(url, opts).then(function(res) { + return fetch(url, opts).then(res => { expect(res.status).to.equal(404); expect(res.headers.get('content-encoding')).to.equal('gzip'); return res.text(); - }).then(function(text) { + }).then(text => { expect(text).to.equal(''); }); }); it('should allow OPTIONS request', function() { - url = base + '/options'; + url = `${base}/options`; opts = { method: 'OPTIONS' }; - return fetch(url, opts).then(function(res) { + return fetch(url, opts).then(res => { expect(res.status).to.equal(200); expect(res.statusText).to.equal('OK'); expect(res.headers.get('allow')).to.equal('GET, HEAD, OPTIONS'); @@ -868,10 +869,10 @@ describe('node-fetch', function() { }); it('should reject decoding body twice', function() { - url = base + '/plain'; - return fetch(url).then(function(res) { + url = `${base}/plain`; + return fetch(url).then(res => { expect(res.headers.get('content-type')).to.equal('text/plain'); - return res.text().then(function(result) { + return res.text().then(result => { expect(res.bodyUsed).to.be.true; return expect(res.text()).to.eventually.be.rejectedWith(Error); }); @@ -879,11 +880,11 @@ describe('node-fetch', function() { }); it('should support maximum response size, multiple chunk', function() { - url = base + '/size/chunk'; + url = `${base}/size/chunk`; opts = { size: 5 }; - return fetch(url, opts).then(function(res) { + return fetch(url, opts).then(res => { expect(res.status).to.equal(200); expect(res.headers.get('content-type')).to.equal('text/plain'); return expect(res.text()).to.eventually.be.rejected @@ -893,11 +894,11 @@ describe('node-fetch', function() { }); it('should support maximum response size, single chunk', function() { - url = base + '/size/long'; + url = `${base}/size/long`; opts = { size: 5 }; - return fetch(url, opts).then(function(res) { + return fetch(url, opts).then(res => { expect(res.status).to.equal(200); expect(res.headers.get('content-type')).to.equal('text/plain'); return expect(res.text()).to.eventually.be.rejected @@ -907,142 +908,140 @@ describe('node-fetch', function() { }); it('should support encoding decode, xml dtd detect', function() { - url = base + '/encoding/euc-jp'; - return fetch(url).then(function(res) { + url = `${base}/encoding/euc-jp`; + return fetch(url).then(res => { expect(res.status).to.equal(200); - return res.text().then(function(result) { + return res.text().then(result => { expect(result).to.equal('日本語'); }); }); }); it('should support encoding decode, content-type detect', function() { - url = base + '/encoding/shift-jis'; - return fetch(url).then(function(res) { + url = `${base}/encoding/shift-jis`; + return fetch(url).then(res => { expect(res.status).to.equal(200); - return res.text().then(function(result) { + return res.text().then(result => { expect(result).to.equal('
日本語
'); }); }); }); it('should support encoding decode, html5 detect', function() { - url = base + '/encoding/gbk'; - return fetch(url).then(function(res) { + url = `${base}/encoding/gbk`; + return fetch(url).then(res => { expect(res.status).to.equal(200); - return res.text().then(function(result) { + return res.text().then(result => { expect(result).to.equal('
中文
'); }); }); }); it('should support encoding decode, html4 detect', function() { - url = base + '/encoding/gb2312'; - return fetch(url).then(function(res) { + url = `${base}/encoding/gb2312`; + return fetch(url).then(res => { expect(res.status).to.equal(200); - return res.text().then(function(result) { + return res.text().then(result => { expect(result).to.equal('
中文
'); }); }); }); it('should default to utf8 encoding', function() { - url = base + '/encoding/utf8'; - return fetch(url).then(function(res) { + url = `${base}/encoding/utf8`; + return fetch(url).then(res => { expect(res.status).to.equal(200); expect(res.headers.get('content-type')).to.be.null; - return res.text().then(function(result) { + return res.text().then(result => { expect(result).to.equal('中文'); }); }); }); it('should support uncommon content-type order, charset in front', function() { - url = base + '/encoding/order1'; - return fetch(url).then(function(res) { + url = `${base}/encoding/order1`; + return fetch(url).then(res => { expect(res.status).to.equal(200); - return res.text().then(function(result) { + return res.text().then(result => { expect(result).to.equal('中文'); }); }); }); it('should support uncommon content-type order, end with qs', function() { - url = base + '/encoding/order2'; - return fetch(url).then(function(res) { + url = `${base}/encoding/order2`; + return fetch(url).then(res => { expect(res.status).to.equal(200); - return res.text().then(function(result) { + return res.text().then(result => { expect(result).to.equal('中文'); }); }); }); it('should support chunked encoding, html4 detect', function() { - url = base + '/encoding/chunked'; - return fetch(url).then(function(res) { + url = `${base}/encoding/chunked`; + return fetch(url).then(res => { expect(res.status).to.equal(200); - // because node v0.12 doesn't have str.repeat - var padding = new Array(10 + 1).join('a'); - return res.text().then(function(result) { - expect(result).to.equal(padding + '
日本語
'); + const padding = 'a'.repeat(10); + return res.text().then(result => { + expect(result).to.equal(`${padding}
日本語
`); }); }); }); it('should only do encoding detection up to 1024 bytes', function() { - url = base + '/encoding/invalid'; - return fetch(url).then(function(res) { + url = `${base}/encoding/invalid`; + return fetch(url).then(res => { expect(res.status).to.equal(200); - // because node v0.12 doesn't have str.repeat - var padding = new Array(1200 + 1).join('a'); - return res.text().then(function(result) { - expect(result).to.not.equal(padding + '中文'); + const padding = 'a'.repeat(1200); + return res.text().then(result => { + expect(result).to.not.equal(`${padding}中文`); }); }); }); it('should allow piping response body as stream', function(done) { - url = base + '/hello'; - fetch(url).then(function(res) { + url = `${base}/hello`; + fetch(url).then(res => { expect(res.body).to.be.an.instanceof(stream.Transform); - res.body.on('data', function(chunk) { + res.body.on('data', chunk => { if (chunk === null) { return; } expect(chunk.toString()).to.equal('world'); }); - res.body.on('end', function() { + res.body.on('end', () => { done(); }); }); }); it('should allow cloning a response, and use both as stream', function(done) { - url = base + '/hello'; - return fetch(url).then(function(res) { - var counter = 0; - var r1 = res.clone(); + url = `${base}/hello`; + return fetch(url).then(res => { + let counter = 0; + const r1 = res.clone(); expect(res.body).to.be.an.instanceof(stream.Transform); expect(r1.body).to.be.an.instanceof(stream.Transform); - res.body.on('data', function(chunk) { + res.body.on('data', chunk => { if (chunk === null) { return; } expect(chunk.toString()).to.equal('world'); }); - res.body.on('end', function() { + res.body.on('end', () => { counter++; if (counter == 2) { done(); } }); - r1.body.on('data', function(chunk) { + r1.body.on('data', chunk => { if (chunk === null) { return; } expect(chunk.toString()).to.equal('world'); }); - r1.body.on('end', function() { + r1.body.on('end', () => { counter++; if (counter == 2) { done(); @@ -1052,10 +1051,10 @@ describe('node-fetch', function() { }); it('should allow cloning a json response and log it as text response', function() { - url = base + '/json'; - return fetch(url).then(function(res) { - var r1 = res.clone(); - return fetch.Promise.all([res.json(), r1.text()]).then(function(results) { + url = `${base}/json`; + return fetch(url).then(res => { + const r1 = res.clone(); + return fetch.Promise.all([res.json(), r1.text()]).then(results => { expect(results[0]).to.deep.equal({name: 'value'}); expect(results[1]).to.equal('{"name":"value"}'); }); @@ -1063,12 +1062,12 @@ describe('node-fetch', function() { }); it('should allow cloning a json response, and then log it as text response', function() { - url = base + '/json'; - return fetch(url).then(function(res) { - var r1 = res.clone(); - return res.json().then(function(result) { + url = `${base}/json`; + return fetch(url).then(res => { + const r1 = res.clone(); + return res.json().then(result => { expect(result).to.deep.equal({name: 'value'}); - return r1.text().then(function(result) { + return r1.text().then(result => { expect(result).to.equal('{"name":"value"}'); }); }); @@ -1076,12 +1075,12 @@ describe('node-fetch', function() { }); it('should allow cloning a json response, first log as text response, then return json object', function() { - url = base + '/json'; - return fetch(url).then(function(res) { - var r1 = res.clone(); - return r1.text().then(function(result) { + url = `${base}/json`; + return fetch(url).then(res => { + const r1 = res.clone(); + return r1.text().then(result => { expect(result).to.equal('{"name":"value"}'); - return res.json().then(function(result) { + return res.json().then(result => { expect(result).to.deep.equal({name: 'value'}); }); }); @@ -1089,19 +1088,19 @@ describe('node-fetch', function() { }); it('should not allow cloning a response after its been used', function() { - url = base + '/hello'; - return fetch(url).then(function(res) { - return res.text().then(function(result) { - expect(function() { - var r1 = res.clone(); + url = `${base}/hello`; + return fetch(url).then(res => + res.text().then(result => { + expect(() => { + res.clone(); }).to.throw(Error); - }); - }) + }) + ); }); it('should allow get all responses of a header', function() { - url = base + '/cookie'; - return fetch(url).then(function(res) { + url = `${base}/cookie`; + return fetch(url).then(res => { expect(res.headers.get('set-cookie')).to.equal('a=1'); expect(res.headers.get('Set-Cookie')).to.equal('a=1'); expect(res.headers.getAll('set-cookie')).to.deep.equal(['a=1', 'b=1']); @@ -1110,19 +1109,19 @@ describe('node-fetch', function() { }); it('should allow iterating through all headers', function() { - var headers = new Headers({ + const headers = new Headers({ a: 1 , b: [2, 3] , c: [4] }); expect(headers).to.have.property('forEach'); - var result = []; - headers.forEach(function(val, key) { + const result = []; + headers.forEach((val, key) => { result.push([key, val]); }); - var expected = [ + const expected = [ ["a", "1"] , ["b", "2"] , ["b", "3"] @@ -1132,7 +1131,7 @@ describe('node-fetch', function() { }); it('should allow iterating through all headers', function() { - var headers = new Headers({ + const headers = new Headers({ a: 1 , b: [2, 3] , c: [4] @@ -1142,7 +1141,7 @@ describe('node-fetch', function() { expect(headers).to.have.property('values'); expect(headers).to.have.property('entries'); - var result, expected; + let result, expected; result = []; for (let [key, val] of headers) { @@ -1191,8 +1190,8 @@ describe('node-fetch', function() { }); it('should allow deleting header', function() { - url = base + '/cookie'; - return fetch(url).then(function(res) { + url = `${base}/cookie`; + return fetch(url).then(res => { res.headers.delete('set-cookie'); expect(res.headers.get('set-cookie')).to.be.null; expect(res.headers.getAll('set-cookie')).to.be.empty; @@ -1200,25 +1199,25 @@ describe('node-fetch', function() { }); it('should send request with connection keep-alive if agent is provided', function() { - url = base + '/inspect'; + url = `${base}/inspect`; opts = { agent: new http.Agent({ keepAlive: true }) }; - return fetch(url, opts).then(function(res) { + return fetch(url, opts).then(res => { return res.json(); - }).then(function(res) { + }).then(res => { expect(res.headers['connection']).to.equal('keep-alive'); }); }); it('should ignore unsupported attributes while reading headers', function() { - var FakeHeader = function() {}; + const FakeHeader = () => {}; // prototypes are ignored FakeHeader.prototype.z = 'fake'; - var res = new FakeHeader; + const res = new FakeHeader; // valid res.a = 'string'; res.b = ['1','2']; @@ -1236,8 +1235,8 @@ describe('node-fetch', function() { res.l = false; res.m = new Buffer('test'); - var h1 = new Headers(res); - var h1Raw = h1.raw(); + const h1 = new Headers(res); + const h1Raw = h1.raw(); expect(h1Raw['a']).to.include('string'); expect(h1Raw['b']).to.include('1'); @@ -1261,18 +1260,18 @@ describe('node-fetch', function() { }); it('should wrap headers', function() { - var h1 = new Headers({ + const h1 = new Headers({ a: '1' }); - var h1Raw = h1.raw(); + const h1Raw = h1.raw(); - var h2 = new Headers(h1); + const h2 = new Headers(h1); h2.set('b', '1'); - var h2Raw = h2.raw(); + const h2Raw = h2.raw(); - var h3 = new Headers(h2); + const h3 = new Headers(h2); h3.append('a', '2'); - var h3Raw = h3.raw(); + const h3Raw = h3.raw(); expect(h1Raw['a']).to.include('1'); expect(h1Raw['a']).to.not.include('2'); @@ -1287,9 +1286,9 @@ describe('node-fetch', function() { }); it('should support fetch with Request instance', function() { - url = base + '/hello'; - var req = new Request(url); - return fetch(req).then(function(res) { + url = `${base}/hello`; + const req = new Request(url); + return fetch(req).then(res => { expect(res.url).to.equal(url); expect(res.ok).to.be.true; expect(res.status).to.equal(200); @@ -1297,17 +1296,17 @@ describe('node-fetch', function() { }); it('should support wrapping Request instance', function() { - url = base + '/hello'; + url = `${base}/hello`; - var form = new FormData(); + const form = new FormData(); form.append('a', '1'); - var r1 = new Request(url, { + const r1 = new Request(url, { method: 'POST' , follow: 1 , body: form }); - var r2 = new Request(r1, { + const r2 = new Request(r1, { follow: 2 }); @@ -1322,8 +1321,8 @@ describe('node-fetch', function() { }); it('should support overwrite Request instance', function() { - url = base + '/inspect'; - var req = new Request(url, { + url = `${base}/inspect`; + const req = new Request(url, { method: 'POST' , headers: { a: '1' @@ -1334,25 +1333,25 @@ describe('node-fetch', function() { , headers: { a: '2' } - }).then(function(res) { + }).then(res => { return res.json(); - }).then(function(body) { + }).then(body => { expect(body.method).to.equal('GET'); expect(body.headers.a).to.equal('2'); }); }); it('should support empty options in Response constructor', function() { - var body = resumer().queue('a=1').end(); + let body = resumer().queue('a=1').end(); body = body.pipe(new stream.PassThrough()); - var res = new Response(body); - return res.text().then(function(result) { + const res = new Response(body); + return res.text().then(result => { expect(result).to.equal('a=1'); }); }); it('should support parsing headers in Response constructor', function() { - var res = new Response(null, { + const res = new Response(null, { headers: { a: '1' } @@ -1361,30 +1360,30 @@ describe('node-fetch', function() { }); it('should support text() method in Response constructor', function() { - var res = new Response('a=1'); - return res.text().then(function(result) { + const res = new Response('a=1'); + return res.text().then(result => { expect(result).to.equal('a=1'); }); }); it('should support json() method in Response constructor', function() { - var res = new Response('{"a":1}'); - return res.json().then(function(result) { + const res = new Response('{"a":1}'); + return res.json().then(result => { expect(result.a).to.equal(1); }); }); it('should support buffer() method in Response constructor', function() { - var res = new Response('a=1'); - return res.buffer().then(function(result) { + const res = new Response('a=1'); + return res.buffer().then(result => { expect(result.toString()).to.equal('a=1'); }); }); it('should support clone() method in Response constructor', function() { - var body = resumer().queue('a=1').end(); + let body = resumer().queue('a=1').end(); body = body.pipe(new stream.PassThrough()); - var res = new Response(body, { + const res = new Response(body, { headers: { a: '1' } @@ -1392,7 +1391,7 @@ describe('node-fetch', function() { , status: 346 , statusText: 'production' }); - var cl = res.clone(); + const cl = res.clone(); expect(cl.headers.get('a')).to.equal('1'); expect(cl.url).to.equal(base); expect(cl.status).to.equal(346); @@ -1400,42 +1399,42 @@ describe('node-fetch', function() { expect(cl.ok).to.be.false; // clone body shouldn't be the same body expect(cl.body).to.not.equal(body); - return cl.text().then(function(result) { + return cl.text().then(result => { expect(result).to.equal('a=1'); }); }); it('should support stream as body in Response constructor', function() { - var body = resumer().queue('a=1').end(); + let body = resumer().queue('a=1').end(); body = body.pipe(new stream.PassThrough()); - var res = new Response(body); - return res.text().then(function(result) { + const res = new Response(body); + return res.text().then(result => { expect(result).to.equal('a=1'); }); }); it('should support string as body in Response constructor', function() { - var res = new Response('a=1'); - return res.text().then(function(result) { + const res = new Response('a=1'); + return res.text().then(result => { expect(result).to.equal('a=1'); }); }); it('should support buffer as body in Response constructor', function() { - var res = new Response(new Buffer('a=1')); - return res.text().then(function(result) { + const res = new Response(new Buffer('a=1')); + return res.text().then(result => { expect(result).to.equal('a=1'); }); }); it('should default to 200 as status code', function() { - var res = new Response(null); + const res = new Response(null); expect(res.status).to.equal(200); }); it('should support parsing headers in Request constructor', function() { url = base; - var req = new Request(url, { + const req = new Request(url, { headers: { a: '1' } @@ -1446,50 +1445,50 @@ describe('node-fetch', function() { it('should support text() method in Request constructor', function() { url = base; - var req = new Request(url, { + const req = new Request(url, { body: 'a=1' }); expect(req.url).to.equal(url); - return req.text().then(function(result) { + return req.text().then(result => { expect(result).to.equal('a=1'); }); }); it('should support json() method in Request constructor', function() { url = base; - var req = new Request(url, { + const req = new Request(url, { body: '{"a":1}' }); expect(req.url).to.equal(url); - return req.json().then(function(result) { + return req.json().then(result => { expect(result.a).to.equal(1); }); }); it('should support buffer() method in Request constructor', function() { url = base; - var req = new Request(url, { + const req = new Request(url, { body: 'a=1' }); expect(req.url).to.equal(url); - return req.buffer().then(function(result) { + return req.buffer().then(result => { expect(result.toString()).to.equal('a=1'); }); }); it('should support arbitrary url in Request constructor', function() { url = 'anything'; - var req = new Request(url); + const req = new Request(url); expect(req.url).to.equal('anything'); }); it('should support clone() method in Request constructor', function() { url = base; - var body = resumer().queue('a=1').end(); + let body = resumer().queue('a=1').end(); body = body.pipe(new stream.PassThrough()); - var agent = new http.Agent(); - var req = new Request(url, { - body: body + const agent = new http.Agent(); + const req = new Request(url, { + body , method: 'POST' , redirect: 'manual' , headers: { @@ -1497,9 +1496,9 @@ describe('node-fetch', function() { } , follow: 3 , compress: false - , agent: agent + , agent }); - var cl = req.clone(); + const cl = req.clone(); expect(cl.url).to.equal(url); expect(cl.method).to.equal('POST'); expect(cl.redirect).to.equal('manual'); @@ -1511,24 +1510,24 @@ describe('node-fetch', function() { expect(cl.agent).to.equal(agent); // clone body shouldn't be the same body expect(cl.body).to.not.equal(body); - return fetch.Promise.all([cl.text(), req.text()]).then(function(results) { + return fetch.Promise.all([cl.text(), req.text()]).then(results => { expect(results[0]).to.equal('a=1'); expect(results[1]).to.equal('a=1'); }); }); it('should support text(), json() and buffer() method in Body constructor', function() { - var body = new Body('a=1'); + const body = new Body('a=1'); expect(body).to.have.property('text'); expect(body).to.have.property('json'); expect(body).to.have.property('buffer'); }); it('should create custom FetchError', function() { - var systemError = new Error('system'); + const systemError = new Error('system'); systemError.code = 'ESOMEERROR'; - var err = new FetchError('test message', 'test-error', systemError); + const 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'); @@ -1544,7 +1543,7 @@ describe('node-fetch', function() { opts = { method: 'HEAD' }; - return fetch(url, opts).then(function(res) { + return fetch(url, opts).then(res => { expect(res.status).to.equal(200); expect(res.ok).to.be.true; }); From a0be6aa34aa4c98325011987e48074db0b933879 Mon Sep 17 00:00:00 2001 From: Timothy Gu Date: Sat, 8 Oct 2016 18:31:42 -0700 Subject: [PATCH 05/61] Simplify Request constructor Also make Request more standard compliant: > The `url` attribute's getter must return request's url, **serialized.** --- src/request.js | 21 +++--- test/test.js | 196 ++++++++++++++++++++++++------------------------- 2 files changed, 107 insertions(+), 110 deletions(-) diff --git a/src/request.js b/src/request.js index 1508d67..978e5ec 100644 --- a/src/request.js +++ b/src/request.js @@ -5,7 +5,7 @@ * Request class contains server only options */ -import { parse as parse_url } from 'url'; +import { format as format_url, parse as parse_url } from 'url'; import Headers from './headers.js'; import Body, { clone } from './body'; @@ -18,16 +18,14 @@ import Body, { clone } from './body'; */ export default class Request extends Body { constructor(input, init = {}) { - let url, url_parsed; + let parsedURL; // normalize input if (!(input instanceof Request)) { - url = input; - url_parsed = parse_url(url); + parsedURL = parse_url(input); input = {}; } else { - url = input.url; - url_parsed = parse_url(url); + parsedURL = parse_url(input.url); } super(init.body || clone(input), { @@ -39,7 +37,6 @@ export default class Request extends Body { 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; // server only options this.follow = init.follow !== undefined ? @@ -52,11 +49,11 @@ export default class Request extends Body { this.agent = init.agent || input.agent; // server request options - this.protocol = url_parsed.protocol; - this.hostname = url_parsed.hostname; - this.port = url_parsed.port; - this.path = url_parsed.path; - this.auth = url_parsed.auth; + Object.assign(this, parsedURL); + } + + get url() { + return format_url(this); } /** diff --git a/test/test.js b/test/test.js index a1ea39a..180953d 100644 --- a/test/test.js +++ b/test/test.js @@ -32,7 +32,7 @@ describe('node-fetch', () => { before(done => { local = new TestServer(); - base = 'http://' + local.hostname + ':' + local.port; + base = `http://${local.hostname}:${local.port}/`; local.start(done); }); @@ -95,7 +95,7 @@ describe('node-fetch', () => { }); it('should resolve into response', function() { - url = `${base}/hello`; + url = `${base}hello`; return fetch(url).then(res => { expect(res).to.be.an.instanceof(Response); expect(res.headers).to.be.an.instanceof(Headers); @@ -110,7 +110,7 @@ describe('node-fetch', () => { }); it('should accept plain text response', function() { - url = `${base}/plain`; + url = `${base}plain`; return fetch(url).then(res => { expect(res.headers.get('content-type')).to.equal('text/plain'); return res.text().then(result => { @@ -122,7 +122,7 @@ describe('node-fetch', () => { }); it('should accept html response (like plain text)', function() { - url = `${base}/html`; + url = `${base}html`; return fetch(url).then(res => { expect(res.headers.get('content-type')).to.equal('text/html'); return res.text().then(result => { @@ -134,7 +134,7 @@ describe('node-fetch', () => { }); it('should accept json response', function() { - url = `${base}/json`; + url = `${base}json`; return fetch(url).then(res => { expect(res.headers.get('content-type')).to.equal('application/json'); return res.json().then(result => { @@ -146,7 +146,7 @@ describe('node-fetch', () => { }); it('should send request with custom headers', function() { - url = `${base}/inspect`; + url = `${base}inspect`; opts = { headers: { 'x-custom-header': 'abc' } }; @@ -158,7 +158,7 @@ describe('node-fetch', () => { }); it('should accept headers instance', function() { - url = `${base}/inspect`; + url = `${base}inspect`; opts = { headers: new Headers({ 'x-custom-header': 'abc' }) }; @@ -170,7 +170,7 @@ describe('node-fetch', () => { }); it('should accept custom host header', function() { - url = `${base}/inspect`; + url = `${base}inspect`; opts = { headers: { host: 'example.com' @@ -184,62 +184,62 @@ describe('node-fetch', () => { }); it('should follow redirect code 301', function() { - url = `${base}/redirect/301`; + url = `${base}redirect/301`; return fetch(url).then(res => { - expect(res.url).to.equal(`${base}/inspect`); + expect(res.url).to.equal(`${base}inspect`); expect(res.status).to.equal(200); expect(res.ok).to.be.true; }); }); it('should follow redirect code 302', function() { - url = `${base}/redirect/302`; + url = `${base}redirect/302`; return fetch(url).then(res => { - expect(res.url).to.equal(`${base}/inspect`); + expect(res.url).to.equal(`${base}inspect`); expect(res.status).to.equal(200); }); }); it('should follow redirect code 303', function() { - url = `${base}/redirect/303`; + url = `${base}redirect/303`; return fetch(url).then(res => { - expect(res.url).to.equal(`${base}/inspect`); + expect(res.url).to.equal(`${base}inspect`); expect(res.status).to.equal(200); }); }); it('should follow redirect code 307', function() { - url = `${base}/redirect/307`; + url = `${base}redirect/307`; return fetch(url).then(res => { - expect(res.url).to.equal(`${base}/inspect`); + expect(res.url).to.equal(`${base}inspect`); expect(res.status).to.equal(200); }); }); it('should follow redirect code 308', function() { - url = `${base}/redirect/308`; + url = `${base}redirect/308`; return fetch(url).then(res => { - expect(res.url).to.equal(`${base}/inspect`); + expect(res.url).to.equal(`${base}inspect`); expect(res.status).to.equal(200); }); }); it('should follow redirect chain', function() { - url = `${base}/redirect/chain`; + url = `${base}redirect/chain`; return fetch(url).then(res => { - expect(res.url).to.equal(`${base}/inspect`); + expect(res.url).to.equal(`${base}inspect`); expect(res.status).to.equal(200); }); }); it('should follow POST request redirect code 301 with GET', function() { - url = `${base}/redirect/301`; + url = `${base}redirect/301`; opts = { method: 'POST' , body: 'a=1' }; return fetch(url, opts).then(res => { - expect(res.url).to.equal(`${base}/inspect`); + expect(res.url).to.equal(`${base}inspect`); expect(res.status).to.equal(200); return res.json().then(result => { expect(result.method).to.equal('GET'); @@ -249,13 +249,13 @@ describe('node-fetch', () => { }); it('should follow POST request redirect code 302 with GET', function() { - url = `${base}/redirect/302`; + url = `${base}redirect/302`; opts = { method: 'POST' , body: 'a=1' }; return fetch(url, opts).then(res => { - expect(res.url).to.equal(`${base}/inspect`); + expect(res.url).to.equal(`${base}inspect`); expect(res.status).to.equal(200); return res.json().then(result => { expect(result.method).to.equal('GET'); @@ -265,13 +265,13 @@ describe('node-fetch', () => { }); it('should follow redirect code 303 with GET', function() { - url = `${base}/redirect/303`; + url = `${base}redirect/303`; opts = { method: 'PUT' , body: 'a=1' }; return fetch(url, opts).then(res => { - expect(res.url).to.equal(`${base}/inspect`); + expect(res.url).to.equal(`${base}inspect`); expect(res.status).to.equal(200); return res.json().then(result => { expect(result.method).to.equal('GET'); @@ -281,7 +281,7 @@ describe('node-fetch', () => { }); it('should obey maximum redirect, reject case', function() { - url = `${base}/redirect/chain`; + url = `${base}redirect/chain`; opts = { follow: 1 } @@ -291,18 +291,18 @@ describe('node-fetch', () => { }); it('should obey redirect chain, resolve case', function() { - url = `${base}/redirect/chain`; + url = `${base}redirect/chain`; opts = { follow: 2 } return fetch(url, opts).then(res => { - expect(res.url).to.equal(`${base}/inspect`); + expect(res.url).to.equal(`${base}inspect`); expect(res.status).to.equal(200); }); }); it('should allow not following redirect', function() { - url = `${base}/redirect/301`; + url = `${base}redirect/301`; opts = { follow: 0 } @@ -312,19 +312,19 @@ describe('node-fetch', () => { }); it('should support redirect mode, manual flag', function() { - url = `${base}/redirect/301`; + url = `${base}redirect/301`; opts = { redirect: 'manual' }; return fetch(url, opts).then(res => { expect(res.url).to.equal(url); expect(res.status).to.equal(301); - expect(res.headers.get('location')).to.equal(`${base}/inspect`); + expect(res.headers.get('location')).to.equal(`${base}inspect`); }); }); it('should support redirect mode, error flag', function() { - url = `${base}/redirect/301`; + url = `${base}redirect/301`; opts = { redirect: 'error' }; @@ -334,7 +334,7 @@ describe('node-fetch', () => { }); it('should support redirect mode, manual flag when there is no redirect', function() { - url = `${base}/hello`; + url = `${base}hello`; opts = { redirect: 'manual' }; @@ -346,12 +346,12 @@ describe('node-fetch', () => { }); it('should follow redirect code 301 and keep existing headers', function() { - url = `${base}/redirect/301`; + url = `${base}redirect/301`; opts = { headers: new Headers({ 'x-custom-header': 'abc' }) }; return fetch(url, opts).then(res => { - expect(res.url).to.equal(`${base}/inspect`); + expect(res.url).to.equal(`${base}inspect`); return res.json(); }).then(res => { expect(res.headers['x-custom-header']).to.equal('abc'); @@ -359,14 +359,14 @@ describe('node-fetch', () => { }); it('should reject broken redirect', function() { - url = `${base}/error/redirect`; + url = `${base}error/redirect`; return expect(fetch(url)).to.eventually.be.rejected .and.be.an.instanceOf(FetchError) .and.have.property('type', 'invalid-redirect'); }); it('should not reject broken redirect under manual redirect', function() { - url = `${base}/error/redirect`; + url = `${base}error/redirect`; opts = { redirect: 'manual' }; @@ -378,7 +378,7 @@ describe('node-fetch', () => { }); it('should handle client-error response', function() { - url = `${base}/error/400`; + url = `${base}error/400`; return fetch(url).then(res => { expect(res.headers.get('content-type')).to.equal('text/plain'); expect(res.status).to.equal(400); @@ -393,7 +393,7 @@ describe('node-fetch', () => { }); it('should handle server-error response', function() { - url = `${base}/error/500`; + url = `${base}error/500`; return fetch(url).then(res => { expect(res.headers.get('content-type')).to.equal('text/plain'); expect(res.status).to.equal(500); @@ -408,7 +408,7 @@ describe('node-fetch', () => { }); it('should handle network-error response', function() { - url = `${base}/error/reset`; + url = `${base}error/reset`; return expect(fetch(url)).to.eventually.be.rejected .and.be.an.instanceOf(FetchError) .and.have.property('code', 'ECONNRESET'); @@ -422,7 +422,7 @@ describe('node-fetch', () => { }); it('should reject invalid json response', function() { - url = `${base}/error/json`; + url = `${base}error/json`; return fetch(url).then(res => { expect(res.headers.get('content-type')).to.equal('application/json'); return expect(res.json()).to.eventually.be.rejectedWith(Error); @@ -430,7 +430,7 @@ describe('node-fetch', () => { }); it('should handle no content response', function() { - url = `${base}/no-content`; + url = `${base}no-content`; return fetch(url).then(res => { expect(res.status).to.equal(204); expect(res.statusText).to.equal('No Content'); @@ -443,7 +443,7 @@ describe('node-fetch', () => { }); it('should return empty object on no-content response', function() { - url = `${base}/no-content`; + url = `${base}no-content`; return fetch(url).then(res => { return res.json().then(result => { expect(result).to.be.an('object'); @@ -453,7 +453,7 @@ describe('node-fetch', () => { }); it('should handle no content response with gzip encoding', function() { - url = `${base}/no-content/gzip`; + url = `${base}no-content/gzip`; return fetch(url).then(res => { expect(res.status).to.equal(204); expect(res.statusText).to.equal('No Content'); @@ -467,7 +467,7 @@ describe('node-fetch', () => { }); it('should handle not modified response', function() { - url = `${base}/not-modified`; + url = `${base}not-modified`; return fetch(url).then(res => { expect(res.status).to.equal(304); expect(res.statusText).to.equal('Not Modified'); @@ -480,7 +480,7 @@ describe('node-fetch', () => { }); it('should handle not modified response with gzip encoding', function() { - url = `${base}/not-modified/gzip`; + url = `${base}not-modified/gzip`; return fetch(url).then(res => { expect(res.status).to.equal(304); expect(res.statusText).to.equal('Not Modified'); @@ -494,7 +494,7 @@ describe('node-fetch', () => { }); it('should decompress gzip response', function() { - url = `${base}/gzip`; + url = `${base}gzip`; return fetch(url).then(res => { expect(res.headers.get('content-type')).to.equal('text/plain'); return res.text().then(result => { @@ -505,7 +505,7 @@ describe('node-fetch', () => { }); it('should decompress deflate response', function() { - url = `${base}/deflate`; + url = `${base}deflate`; return fetch(url).then(res => { expect(res.headers.get('content-type')).to.equal('text/plain'); return res.text().then(result => { @@ -516,7 +516,7 @@ describe('node-fetch', () => { }); it('should decompress deflate raw response from old apache server', function() { - url = `${base}/deflate-raw`; + url = `${base}deflate-raw`; return fetch(url).then(res => { expect(res.headers.get('content-type')).to.equal('text/plain'); return res.text().then(result => { @@ -527,7 +527,7 @@ describe('node-fetch', () => { }); it('should skip decompression if unsupported', function() { - url = `${base}/sdch`; + url = `${base}sdch`; return fetch(url).then(res => { expect(res.headers.get('content-type')).to.equal('text/plain'); return res.text().then(result => { @@ -538,7 +538,7 @@ describe('node-fetch', () => { }); it('should reject if response compression is invalid', function() { - url = `${base}/invalid-content-encoding`; + url = `${base}invalid-content-encoding`; return fetch(url).then(res => { expect(res.headers.get('content-type')).to.equal('text/plain'); return expect(res.text()).to.eventually.be.rejected @@ -548,7 +548,7 @@ describe('node-fetch', () => { }); it('should allow disabling auto decompression', function() { - url = `${base}/gzip`; + url = `${base}gzip`; opts = { compress: false }; @@ -563,7 +563,7 @@ describe('node-fetch', () => { it('should allow custom timeout', function() { this.timeout(500); - url = `${base}/timeout`; + url = `${base}timeout`; opts = { timeout: 100 }; @@ -574,7 +574,7 @@ describe('node-fetch', () => { it('should allow custom timeout on response body', function() { this.timeout(500); - url = `${base}/slow`; + url = `${base}slow`; opts = { timeout: 100 }; @@ -588,7 +588,7 @@ describe('node-fetch', () => { it('should clear internal timeout on fetch response', function (done) { this.timeout(1000); - spawn('node', ['-e', `require('./')('${base}/hello', { timeout: 5000 })`]) + spawn('node', ['-e', `require('./')('${base}hello', { timeout: 5000 })`]) .on('exit', () => { done(); }); @@ -596,7 +596,7 @@ describe('node-fetch', () => { it('should clear internal timeout on fetch redirect', function (done) { this.timeout(1000); - spawn('node', ['-e', `require('./')('${base}/redirect/301', { timeout: 5000 })`]) + spawn('node', ['-e', `require('./')('${base}redirect/301', { timeout: 5000 })`]) .on('exit', () => { done(); }); @@ -604,14 +604,14 @@ describe('node-fetch', () => { it('should clear internal timeout on fetch error', function (done) { this.timeout(1000); - spawn('node', ['-e', `require('./')('${base}/error/reset', { timeout: 5000 })`]) + spawn('node', ['-e', `require('./')('${base}error/reset', { timeout: 5000 })`]) .on('exit', () => { done(); }); }); it('should allow POST request', function() { - url = `${base}/inspect`; + url = `${base}inspect`; opts = { method: 'POST' }; @@ -625,7 +625,7 @@ describe('node-fetch', () => { }); it('should allow POST request with string body', function() { - url = `${base}/inspect`; + url = `${base}inspect`; opts = { method: 'POST' , body: 'a=1' @@ -641,7 +641,7 @@ describe('node-fetch', () => { }); it('should allow POST request with buffer body', function() { - url = `${base}/inspect`; + url = `${base}inspect`; opts = { method: 'POST' , body: new Buffer('a=1', 'utf-8') @@ -660,7 +660,7 @@ describe('node-fetch', () => { let body = resumer().queue('a=1').end(); body = body.pipe(new stream.PassThrough()); - url = `${base}/inspect`; + url = `${base}inspect`; opts = { method: 'POST' , body @@ -679,7 +679,7 @@ describe('node-fetch', () => { const form = new FormData(); form.append('a','1'); - url = `${base}/multipart`; + url = `${base}multipart`; opts = { method: 'POST' , body: form @@ -698,7 +698,7 @@ describe('node-fetch', () => { const form = new FormData(); form.append('my_field', fs.createReadStream('test/dummy.txt')); - url = `${base}/multipart`; + url = `${base}multipart`; opts = { method: 'POST' , body: form @@ -721,7 +721,7 @@ describe('node-fetch', () => { const headers = form.getHeaders(); headers['b'] = '2'; - url = `${base}/multipart`; + url = `${base}multipart`; opts = { method: 'POST' , body: form @@ -739,7 +739,7 @@ describe('node-fetch', () => { }); it('should allow POST request with object body', function() { - url = `${base}/inspect`; + url = `${base}inspect`; // note that fetch simply calls tostring on an object opts = { method: 'POST' @@ -754,7 +754,7 @@ describe('node-fetch', () => { }); it('should allow PUT request', function() { - url = `${base}/inspect`; + url = `${base}inspect`; opts = { method: 'PUT' , body: 'a=1' @@ -768,7 +768,7 @@ describe('node-fetch', () => { }); it('should allow DELETE request', function() { - url = `${base}/inspect`; + url = `${base}inspect`; opts = { method: 'DELETE' }; @@ -780,7 +780,7 @@ describe('node-fetch', () => { }); it('should allow POST request with string body', function() { - url = `${base}/inspect`; + url = `${base}inspect`; opts = { method: 'POST' , body: 'a=1' @@ -796,7 +796,7 @@ describe('node-fetch', () => { }); it('should allow DELETE request with string body', function() { - url = `${base}/inspect`; + url = `${base}inspect`; opts = { method: 'DELETE' , body: 'a=1' @@ -812,7 +812,7 @@ describe('node-fetch', () => { }); it('should allow PATCH request', function() { - url = `${base}/inspect`; + url = `${base}inspect`; opts = { method: 'PATCH' , body: 'a=1' @@ -826,7 +826,7 @@ describe('node-fetch', () => { }); it('should allow HEAD request', function() { - url = `${base}/hello`; + url = `${base}hello`; opts = { method: 'HEAD' }; @@ -842,7 +842,7 @@ describe('node-fetch', () => { }); it('should allow HEAD request with content-encoding header', function() { - url = `${base}/error/404`; + url = `${base}error/404`; opts = { method: 'HEAD' }; @@ -856,7 +856,7 @@ describe('node-fetch', () => { }); it('should allow OPTIONS request', function() { - url = `${base}/options`; + url = `${base}options`; opts = { method: 'OPTIONS' }; @@ -869,7 +869,7 @@ describe('node-fetch', () => { }); it('should reject decoding body twice', function() { - url = `${base}/plain`; + url = `${base}plain`; return fetch(url).then(res => { expect(res.headers.get('content-type')).to.equal('text/plain'); return res.text().then(result => { @@ -880,7 +880,7 @@ describe('node-fetch', () => { }); it('should support maximum response size, multiple chunk', function() { - url = `${base}/size/chunk`; + url = `${base}size/chunk`; opts = { size: 5 }; @@ -894,7 +894,7 @@ describe('node-fetch', () => { }); it('should support maximum response size, single chunk', function() { - url = `${base}/size/long`; + url = `${base}size/long`; opts = { size: 5 }; @@ -908,7 +908,7 @@ describe('node-fetch', () => { }); it('should support encoding decode, xml dtd detect', function() { - url = `${base}/encoding/euc-jp`; + url = `${base}encoding/euc-jp`; return fetch(url).then(res => { expect(res.status).to.equal(200); return res.text().then(result => { @@ -918,7 +918,7 @@ describe('node-fetch', () => { }); it('should support encoding decode, content-type detect', function() { - url = `${base}/encoding/shift-jis`; + url = `${base}encoding/shift-jis`; return fetch(url).then(res => { expect(res.status).to.equal(200); return res.text().then(result => { @@ -928,7 +928,7 @@ describe('node-fetch', () => { }); it('should support encoding decode, html5 detect', function() { - url = `${base}/encoding/gbk`; + url = `${base}encoding/gbk`; return fetch(url).then(res => { expect(res.status).to.equal(200); return res.text().then(result => { @@ -938,7 +938,7 @@ describe('node-fetch', () => { }); it('should support encoding decode, html4 detect', function() { - url = `${base}/encoding/gb2312`; + url = `${base}encoding/gb2312`; return fetch(url).then(res => { expect(res.status).to.equal(200); return res.text().then(result => { @@ -948,7 +948,7 @@ describe('node-fetch', () => { }); it('should default to utf8 encoding', function() { - url = `${base}/encoding/utf8`; + url = `${base}encoding/utf8`; return fetch(url).then(res => { expect(res.status).to.equal(200); expect(res.headers.get('content-type')).to.be.null; @@ -959,7 +959,7 @@ describe('node-fetch', () => { }); it('should support uncommon content-type order, charset in front', function() { - url = `${base}/encoding/order1`; + url = `${base}encoding/order1`; return fetch(url).then(res => { expect(res.status).to.equal(200); return res.text().then(result => { @@ -969,7 +969,7 @@ describe('node-fetch', () => { }); it('should support uncommon content-type order, end with qs', function() { - url = `${base}/encoding/order2`; + url = `${base}encoding/order2`; return fetch(url).then(res => { expect(res.status).to.equal(200); return res.text().then(result => { @@ -979,7 +979,7 @@ describe('node-fetch', () => { }); it('should support chunked encoding, html4 detect', function() { - url = `${base}/encoding/chunked`; + url = `${base}encoding/chunked`; return fetch(url).then(res => { expect(res.status).to.equal(200); const padding = 'a'.repeat(10); @@ -990,7 +990,7 @@ describe('node-fetch', () => { }); it('should only do encoding detection up to 1024 bytes', function() { - url = `${base}/encoding/invalid`; + url = `${base}encoding/invalid`; return fetch(url).then(res => { expect(res.status).to.equal(200); const padding = 'a'.repeat(1200); @@ -1001,7 +1001,7 @@ describe('node-fetch', () => { }); it('should allow piping response body as stream', function(done) { - url = `${base}/hello`; + url = `${base}hello`; fetch(url).then(res => { expect(res.body).to.be.an.instanceof(stream.Transform); res.body.on('data', chunk => { @@ -1017,7 +1017,7 @@ describe('node-fetch', () => { }); it('should allow cloning a response, and use both as stream', function(done) { - url = `${base}/hello`; + url = `${base}hello`; return fetch(url).then(res => { let counter = 0; const r1 = res.clone(); @@ -1051,7 +1051,7 @@ describe('node-fetch', () => { }); it('should allow cloning a json response and log it as text response', function() { - url = `${base}/json`; + url = `${base}json`; return fetch(url).then(res => { const r1 = res.clone(); return fetch.Promise.all([res.json(), r1.text()]).then(results => { @@ -1062,7 +1062,7 @@ describe('node-fetch', () => { }); it('should allow cloning a json response, and then log it as text response', function() { - url = `${base}/json`; + url = `${base}json`; return fetch(url).then(res => { const r1 = res.clone(); return res.json().then(result => { @@ -1075,7 +1075,7 @@ describe('node-fetch', () => { }); it('should allow cloning a json response, first log as text response, then return json object', function() { - url = `${base}/json`; + url = `${base}json`; return fetch(url).then(res => { const r1 = res.clone(); return r1.text().then(result => { @@ -1088,7 +1088,7 @@ describe('node-fetch', () => { }); it('should not allow cloning a response after its been used', function() { - url = `${base}/hello`; + url = `${base}hello`; return fetch(url).then(res => res.text().then(result => { expect(() => { @@ -1099,7 +1099,7 @@ describe('node-fetch', () => { }); it('should allow get all responses of a header', function() { - url = `${base}/cookie`; + url = `${base}cookie`; return fetch(url).then(res => { expect(res.headers.get('set-cookie')).to.equal('a=1'); expect(res.headers.get('Set-Cookie')).to.equal('a=1'); @@ -1190,7 +1190,7 @@ describe('node-fetch', () => { }); it('should allow deleting header', function() { - url = `${base}/cookie`; + url = `${base}cookie`; return fetch(url).then(res => { res.headers.delete('set-cookie'); expect(res.headers.get('set-cookie')).to.be.null; @@ -1199,7 +1199,7 @@ describe('node-fetch', () => { }); it('should send request with connection keep-alive if agent is provided', function() { - url = `${base}/inspect`; + url = `${base}inspect`; opts = { agent: new http.Agent({ keepAlive: true @@ -1286,7 +1286,7 @@ describe('node-fetch', () => { }); it('should support fetch with Request instance', function() { - url = `${base}/hello`; + url = `${base}hello`; const req = new Request(url); return fetch(req).then(res => { expect(res.url).to.equal(url); @@ -1296,7 +1296,7 @@ describe('node-fetch', () => { }); it('should support wrapping Request instance', function() { - url = `${base}/hello`; + url = `${base}hello`; const form = new FormData(); form.append('a', '1'); @@ -1321,7 +1321,7 @@ describe('node-fetch', () => { }); it('should support overwrite Request instance', function() { - url = `${base}/inspect`; + url = `${base}inspect`; const req = new Request(url, { method: 'POST' , headers: { From 9d3cc52601d170aaedc86d9ad7a5c2b7f5bcbd97 Mon Sep 17 00:00:00 2001 From: Timothy Gu Date: Sat, 8 Oct 2016 18:51:34 -0700 Subject: [PATCH 06/61] Body: store fewer things in the class Incorporates some changes from #140, by Gabriel Wicke . --- src/body.js | 202 +++++++++++++++++++++++++--------------------------- 1 file changed, 97 insertions(+), 105 deletions(-) diff --git a/src/body.js b/src/body.js index ab76421..962232f 100644 --- a/src/body.js +++ b/src/body.js @@ -11,11 +11,7 @@ import {PassThrough} from 'stream'; import FetchError from './fetch-error.js'; const DISTURBED = Symbol('disturbed'); -const BYTES = Symbol('bytes'); -const RAW = Symbol('raw'); -const ABORT = Symbol('abort'); -const CONVERT = Symbol('convert'); -const DECODE = Symbol('decode'); +const CONSUME_BODY = Symbol('consumeBody'); /** * Body class @@ -32,10 +28,7 @@ export default class Body { this.body = body; this[DISTURBED] = false; this.size = size; - this[BYTES] = 0; this.timeout = timeout; - this[RAW] = []; - this[ABORT] = false; } get bodyUsed() { @@ -53,7 +46,7 @@ export default class Body { return Body.Promise.resolve({}); } - return this[DECODE]().then(buffer => JSON.parse(buffer.toString())); + return this[CONSUME_BODY]().then(buffer => JSON.parse(buffer.toString())); } /** @@ -62,7 +55,7 @@ export default class Body { * @return Promise */ text() { - return this[DECODE]().then(buffer => buffer.toString()); + return this[CONSUME_BODY]().then(buffer => buffer.toString()); } /** @@ -71,7 +64,7 @@ export default class Body { * @return Promise */ buffer() { - return this[DECODE](); + return this[CONSUME_BODY](); } /** @@ -79,144 +72,143 @@ export default class Body { * * @return Promise */ - [DECODE]() { + [CONSUME_BODY]() { if (this[DISTURBED]) { return Body.Promise.reject(new Error(`body used already for: ${this.url}`)); } this[DISTURBED] = true; - this[BYTES] = 0; - this[ABORT] = false; - this[RAW] = []; + + // body is string + if (typeof this.body === 'string') { + return Body.Promise.resolve(convertBody([new Buffer(this.body)], this.headers)); + } + + // body is buffer + if (Buffer.isBuffer(this.body)) { + return Body.Promise.resolve(convertBody([this.body], this.headers)); + } + + // body is stream + // get ready to actually consume the body + let accum = []; + let accumBytes = 0; + let abort = false; return new Body.Promise((resolve, reject) => { let resTimeout; - // body is string - if (typeof this.body === 'string') { - this[BYTES] = this.body.length; - this[RAW] = [new Buffer(this.body)]; - return resolve(this[CONVERT]()); - } - - // body is buffer - if (this.body instanceof Buffer) { - this[BYTES] = this.body.length; - this[RAW] = [this.body]; - return resolve(this[CONVERT]()); - } - // allow timeout on slow response body if (this.timeout) { resTimeout = setTimeout(() => { - this[ABORT] = true; - reject(new FetchError('response timeout at ' + this.url + ' over limit: ' + this.timeout, 'body-timeout')); + abort = true; + reject(new FetchError(`Response timeout while trying to fetch ${this.url} (over ${this.timeout}ms)`, 'body-timeout')); }, this.timeout); } // handle stream error, such as incorrect content-encoding this.body.on('error', err => { - reject(new FetchError('invalid response body at: ' + this.url + ' reason: ' + err.message, 'system', err)); + reject(new FetchError(`Invalid response body while trying to fetch ${this.url}: ${err.message}`, 'system', err)); }); - // body is stream this.body.on('data', chunk => { - if (this[ABORT] || chunk === null) { + if (abort || chunk === null) { return; } - if (this.size && this[BYTES] + chunk.length > this.size) { - this[ABORT] = true; + if (this.size && accumBytes + chunk.length > this.size) { + abort = true; reject(new FetchError(`content size at ${this.url} over limit: ${this.size}`, 'max-size')); return; } - this[BYTES] += chunk.length; - this[RAW].push(chunk); + accumBytes += chunk.length; + accum.push(chunk); }); this.body.on('end', () => { - if (this[ABORT]) { + if (abort) { return; } clearTimeout(resTimeout); - resolve(this[CONVERT]()); + resolve(convertBody(accum, this.headers)); }); }); } - /** - * Detect buffer encoding and convert to target encoding - * ref: http://www.w3.org/TR/2011/WD-html5-20110113/parsing.html#determining-the-character-encoding - * - * @param String encoding Target encoding - * @return String - */ - [CONVERT](encoding = 'utf-8') { - const ct = this.headers.get('content-type'); - let charset = 'utf-8'; - let res, str; +} - // header - if (ct) { - // skip encoding detection altogether if not html/xml/plain text - if (!/text\/html|text\/plain|\+xml|\/xml/i.test(ct)) { - return Buffer.concat(this[RAW]); - } +/** + * Detect buffer encoding and convert to target encoding + * ref: http://www.w3.org/TR/2011/WD-html5-20110113/parsing.html#determining-the-character-encoding + * + * @param Array arrayOfBuffers Array of buffers + * @param String encoding Target encoding + * @return String + */ +function convertBody(arrayOfBuffers, headers) { + const ct = headers.get('content-type'); + let charset = 'utf-8'; + let res, str; - res = /charset=([^;]*)/i.exec(ct); + // header + if (ct) { + // skip encoding detection altogether if not html/xml/plain text + if (!/text\/html|text\/plain|\+xml|\/xml/i.test(ct)) { + return Buffer.concat(arrayOfBuffers); } - // no charset in content type, peek at response body for at most 1024 bytes - if (!res && this[RAW].length > 0) { - for (let i = 0; i < this[RAW].length; i++) { - str += this[RAW][i].toString() - if (str.length > 1024) { - break; - } - } - str = str.substr(0, 1024); - } - - // html5 - if (!res && str) { - res = / 0) { + for (let i = 0; i < arrayOfBuffers.length; i++) { + str += arrayOfBuffers[i].toString() + if (str.length > 1024) { + break; + } + } + str = str.substr(0, 1024); + } + + // html5 + if (!res && str) { + res = / Date: Sat, 8 Oct 2016 19:40:56 -0700 Subject: [PATCH 07/61] Improve Body spec compliance when body is null --- src/body.js | 5 +++++ src/response.js | 2 +- test/test.js | 8 ++++++++ 3 files changed, 14 insertions(+), 1 deletion(-) diff --git a/src/body.js b/src/body.js index 962232f..349657e 100644 --- a/src/body.js +++ b/src/body.js @@ -79,6 +79,11 @@ export default class Body { this[DISTURBED] = true; + // body is null + if (!this.body) { + return Body.Promise.resolve(new Buffer(0)); + } + // body is string if (typeof this.body === 'string') { return Body.Promise.resolve(convertBody([new Buffer(this.body)], this.headers)); diff --git a/src/response.js b/src/response.js index fb53a78..bc3175b 100644 --- a/src/response.js +++ b/src/response.js @@ -17,7 +17,7 @@ import Body, { clone } from './body'; * @return Void */ export default class Response extends Body { - constructor(body, opts = {}) { + constructor(body = null, opts = {}) { super(body, opts); this.url = opts.url; diff --git a/test/test.js b/test/test.js index 180953d..34880c2 100644 --- a/test/test.js +++ b/test/test.js @@ -1427,6 +1427,14 @@ describe('node-fetch', () => { }); }); + it('should default to null as body', function() { + const res = new Response(); + expect(res.body).to.equal(null); + return res.text().then(result => { + expect(result).to.equal(''); + }); + }); + it('should default to 200 as status code', function() { const res = new Response(null); expect(res.status).to.equal(200); From c3a121a36030a6c23732cb4f9897248b0f318172 Mon Sep 17 00:00:00 2001 From: Timothy Gu Date: Sat, 8 Oct 2016 19:41:45 -0700 Subject: [PATCH 08/61] Add support for Body#arrayBuffer --- package.json | 1 + src/body.js | 10 ++++++++++ test/test.js | 16 +++++++++++++++- 3 files changed, 26 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index 1552704..af51757 100644 --- a/package.json +++ b/package.json @@ -46,6 +46,7 @@ }, "dependencies": { "babel-runtime": "^6.11.6", + "buffer-to-arraybuffer": "0.0.4", "encoding": "^0.1.11", "is-stream": "^1.0.1" }, diff --git a/src/body.js b/src/body.js index 349657e..bd30eb6 100644 --- a/src/body.js +++ b/src/body.js @@ -7,6 +7,7 @@ import {convert} from 'encoding'; import bodyStream from 'is-stream'; +import toArrayBuffer from 'buffer-to-arraybuffer'; import {PassThrough} from 'stream'; import FetchError from './fetch-error.js'; @@ -35,6 +36,15 @@ export default class Body { return this[DISTURBED]; } + /** + * Decode response as ArrayBuffer + * + * @return Promise + */ + arrayBuffer() { + return this[CONSUME_BODY]().then(buf => toArrayBuffer(buf)); + } + /** * Decode response as json * diff --git a/test/test.js b/test/test.js index 34880c2..679a414 100644 --- a/test/test.js +++ b/test/test.js @@ -1451,6 +1451,19 @@ describe('node-fetch', () => { expect(req.headers.get('a')).to.equal('1'); }); + it('should support arrayBuffer() method in Request constructor', function() { + url = base; + var req = new Request(url, { + body: 'a=1' + }); + expect(req.url).to.equal(url); + return req.arrayBuffer().then(function(result) { + expect(result).to.be.an.instanceOf(ArrayBuffer); + const str = String.fromCharCode.apply(null, new Uint8Array(result)); + expect(str).to.equal('a=1'); + }); + }); + it('should support text() method in Request constructor', function() { url = base; const req = new Request(url, { @@ -1524,8 +1537,9 @@ describe('node-fetch', () => { }); }); - it('should support text(), json() and buffer() method in Body constructor', function() { + it('should support arrayBuffer(), text(), json() and buffer() method in Body constructor', function() { const body = new Body('a=1'); + expect(body).to.have.property('arrayBuffer'); expect(body).to.have.property('text'); expect(body).to.have.property('json'); expect(body).to.have.property('buffer'); From 0f65af3fd86a9015360bafec2cb7a928e0bf19c0 Mon Sep 17 00:00:00 2001 From: Timothy Gu Date: Mon, 10 Oct 2016 12:05:02 -0700 Subject: [PATCH 09/61] Split Headers iterable test into four --- package.json | 1 + test/test.js | 99 +++++++++++++++++++++++++++------------------------- 2 files changed, 53 insertions(+), 47 deletions(-) diff --git a/package.json b/package.json index af51757..18109f8 100644 --- a/package.json +++ b/package.json @@ -35,6 +35,7 @@ "bluebird": "^3.3.4", "chai": "^3.5.0", "chai-as-promised": "^5.2.0", + "chai-iterator": "^1.1.1", "coveralls": "^2.11.2", "cross-env": "2.0.1", "form-data": ">=1.0.0", diff --git a/test/test.js b/test/test.js index 679a414..753290c 100644 --- a/test/test.js +++ b/test/test.js @@ -1,7 +1,8 @@ // test tools import chai from 'chai'; -import cap from 'chai-as-promised'; +import chaiPromised from 'chai-as-promised'; +import chaiIterator from 'chai-iterator'; import bluebird from 'bluebird'; import then from 'promise'; import {spawn} from 'child_process'; @@ -11,7 +12,8 @@ import FormData from 'form-data'; import * as http from 'http'; import * as fs from 'fs'; -chai.use(cap); +chai.use(chaiPromised); +chai.use(chaiIterator); const expect = chai.expect; import TestServer from './server'; @@ -1108,7 +1110,7 @@ describe('node-fetch', () => { }); }); - it('should allow iterating through all headers', function() { + it('should allow iterating through all headers with forEach', function() { const headers = new Headers({ a: 1 , b: [2, 3] @@ -1130,63 +1132,66 @@ describe('node-fetch', () => { expect(result).to.deep.equal(expected); }); - it('should allow iterating through all headers', function() { + it('should allow iterating through all headers with for-of loop', function() { const headers = new Headers({ - a: 1 - , b: [2, 3] - , c: [4] + a: '1' + , b: '2' + , c: '4' }); - expect(headers).to.have.property(Symbol.iterator); - expect(headers).to.have.property('keys'); - expect(headers).to.have.property('values'); - expect(headers).to.have.property('entries'); + headers.append('b', '3'); + expect(headers).to.be.iterable; - let result, expected; - - result = []; - for (let [key, val] of headers) { - result.push([key, val]); + const result = []; + for (let pair of headers) { + result.push(pair); } - - expected = [ + expect(result).to.deep.equal([ ["a", "1"] , ["b", "2"] , ["b", "3"] , ["c", "4"] - ]; - expect(result).to.deep.equal(expected); + ]); + }); - result = []; - for (let [key, val] of headers.entries()) { - result.push([key, val]); - } - expect(result).to.deep.equal(expected); + it('should allow iterating through all headers with entries()', function() { + const headers = new Headers({ + a: '1' + , b: '2' + , c: '4' + }); + headers.append('b', '3'); - result = []; - for (let key of headers.keys()) { - result.push(key); - } + expect(headers.entries()).to.be.iterable + .and.to.deep.iterate.over([ + ["a", "1"] + , ["b", "2"] + , ["b", "3"] + , ["c", "4"] + ]); + }); - expected = [ - "a" - , "b" - , "b" - , "c" - ]; - expect(result).to.deep.equal(expected); + it('should allow iterating through all headers with keys()', function() { + const headers = new Headers({ + a: '1' + , b: '2' + , c: '4' + }); + headers.append('b', '3'); - result = []; - for (let key of headers.values()) { - result.push(key); - } + expect(headers.keys()).to.be.iterable + .and.to.iterate.over(['a', 'b', 'b', 'c']); + }); - expected = [ - "1" - , "2" - , "3" - , "4" - ]; - expect(result).to.deep.equal(expected); + it('should allow iterating through all headers with values()', function() { + const headers = new Headers({ + a: '1' + , b: '2' + , c: '4' + }); + headers.append('b', '3'); + + expect(headers.values()).to.be.iterable + .and.to.iterate.over(['1', '2', '3', '4']); }); it('should allow deleting header', function() { From 4d81cb4877cb047c46abd2fdbd3086d9ea16b8f9 Mon Sep 17 00:00:00 2001 From: Timothy Gu Date: Mon, 10 Oct 2016 12:41:13 -0700 Subject: [PATCH 10/61] Test @@toStringTag getter of all classes --- test/test.js | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/test/test.js b/test/test.js index 753290c..444ca1f 100644 --- a/test/test.js +++ b/test/test.js @@ -171,6 +171,11 @@ describe('node-fetch', () => { }); }); + it('should support proper toString output for Headers instance', function() { + const headers = new Headers(); + expect(headers.toString()).to.equal('[object Headers]'); + }); + it('should accept custom host header', function() { url = `${base}inspect`; opts = { @@ -1300,6 +1305,11 @@ describe('node-fetch', () => { }); }); + it('should support proper toString output for Request instance', function() { + const req = new Request(base); + expect(req.toString()).to.equal('[object Request]'); + }); + it('should support wrapping Request instance', function() { url = `${base}hello`; @@ -1355,6 +1365,11 @@ describe('node-fetch', () => { }); }); + it('should support proper toString output for Response instance', function() { + const res = new Response(); + expect(res.toString()).to.equal('[object Response]'); + }); + it('should support parsing headers in Response constructor', function() { const res = new Response(null, { headers: { From c2c6550e549e34ea0ce65a9fac9b7d1c80a61498 Mon Sep 17 00:00:00 2001 From: Timothy Gu Date: Wed, 12 Oct 2016 18:16:46 -0700 Subject: [PATCH 11/61] Use loose mode when compiling Babel --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 18109f8..c408a6e 100644 --- a/package.json +++ b/package.json @@ -53,7 +53,7 @@ }, "babel": { "presets": [ - "es2015" + ["es2015", {"loose": true}] ], "plugins": [ "transform-runtime" From 67326e3873ba1ee3dff79871f256fdf77dff0ab2 Mon Sep 17 00:00:00 2001 From: Timothy Gu Date: Wed, 12 Oct 2016 20:51:19 -0700 Subject: [PATCH 12/61] Condense class toString tests --- test/test.js | 21 ++++++--------------- 1 file changed, 6 insertions(+), 15 deletions(-) diff --git a/test/test.js b/test/test.js index 444ca1f..d781160 100644 --- a/test/test.js +++ b/test/test.js @@ -74,6 +74,12 @@ describe('node-fetch', () => { expect(fetch.Request).to.equal(Request); }); + it('should support proper toString output for Headers, Response and Request objects', function() { + expect(new Headers().toString()).to.equal('[object Headers]'); + expect(new Response().toString()).to.equal('[object Response]'); + expect(new Request(base).toString()).to.equal('[object Request]'); + }); + it('should reject with error if url is protocol relative', function() { url = '//example.com/'; return expect(fetch(url)).to.eventually.be.rejectedWith(Error); @@ -171,11 +177,6 @@ describe('node-fetch', () => { }); }); - it('should support proper toString output for Headers instance', function() { - const headers = new Headers(); - expect(headers.toString()).to.equal('[object Headers]'); - }); - it('should accept custom host header', function() { url = `${base}inspect`; opts = { @@ -1305,11 +1306,6 @@ describe('node-fetch', () => { }); }); - it('should support proper toString output for Request instance', function() { - const req = new Request(base); - expect(req.toString()).to.equal('[object Request]'); - }); - it('should support wrapping Request instance', function() { url = `${base}hello`; @@ -1365,11 +1361,6 @@ describe('node-fetch', () => { }); }); - it('should support proper toString output for Response instance', function() { - const res = new Response(); - expect(res.toString()).to.equal('[object Response]'); - }); - it('should support parsing headers in Response constructor', function() { const res = new Response(null, { headers: { From ea111626e9ae3ea112bf7bc8fec4b28a905ddf37 Mon Sep 17 00:00:00 2001 From: Timothy Gu Date: Thu, 13 Oct 2016 00:32:52 -0700 Subject: [PATCH 13/61] Switch to Codecov Fixes #186. --- package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index c408a6e..bdc513f 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,7 @@ "prepublish": "npm run build", "test": "mocha --compilers js:babel-polyfill --compilers js:babel-register test/test.js", "report": "cross-env BABEL_ENV=test nyc --reporter lcov --reporter text mocha -R spec test/test.js", - "coverage": "cross-env BABEL_ENV=test nyc --reporter lcovonly mocha -R spec test/test.js && cat ./coverage/lcov.info | coveralls" + "coverage": "cross-env BABEL_ENV=test nyc --reporter json --reporter text mocha -R spec test/test.js && codecov -f coverage/coverage-final.json" }, "repository": { "type": "git", @@ -36,7 +36,7 @@ "chai": "^3.5.0", "chai-as-promised": "^5.2.0", "chai-iterator": "^1.1.1", - "coveralls": "^2.11.2", + "codecov": "^1.0.1", "cross-env": "2.0.1", "form-data": ">=1.0.0", "mocha": "^2.1.0", From b9b0341db7375f07114fb93283676d94647c0932 Mon Sep 17 00:00:00 2001 From: Timothy Gu Date: Thu, 13 Oct 2016 00:40:36 -0700 Subject: [PATCH 14/61] Add Codecov badges and settings --- .codecov.yml | 5 +++++ README.md | 6 +++--- 2 files changed, 8 insertions(+), 3 deletions(-) create mode 100644 .codecov.yml diff --git a/.codecov.yml b/.codecov.yml new file mode 100644 index 0000000..dcdc85a --- /dev/null +++ b/.codecov.yml @@ -0,0 +1,5 @@ +parsers: + javascript: + enable_partials: yes +codecov: + branch: v2 diff --git a/README.md b/README.md index 96d69f6..499324f 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ node-fetch [![npm version][npm-image]][npm-url] [![build status][travis-image]][travis-url] -[![coverage status][coveralls-image]][coveralls-url] +[![coverage status][codecov-image]][codecov-url] A light-weight module that brings `window.fetch` to Node.js @@ -206,5 +206,5 @@ Thanks to [github/fetch](https://github.com/github/fetch) for providing a solid [npm-url]: https://www.npmjs.com/package/node-fetch [travis-image]: https://img.shields.io/travis/bitinn/node-fetch.svg?style=flat-square [travis-url]: https://travis-ci.org/bitinn/node-fetch -[coveralls-image]: https://img.shields.io/coveralls/bitinn/node-fetch.svg?style=flat-square -[coveralls-url]: https://coveralls.io/r/bitinn/node-fetch +[coveralls-image]: https://img.shields.io/codecov/c/github/bitinn/node-fetch.svg?style=flat-square +[coveralls-url]: https://codecov.io/gh/bitinn/node-fetch From ba8c392965d718e16fffb3f7a2777b64e352f2d1 Mon Sep 17 00:00:00 2001 From: Timothy Gu Date: Thu, 13 Oct 2016 00:42:26 -0700 Subject: [PATCH 15/61] Cache node_modules in Travis [ci skip] --- .travis.yml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index a1358b0..b6c2082 100644 --- a/.travis.yml +++ b/.travis.yml @@ -9,4 +9,7 @@ env: before_script: - 'if [ "$FORMDATA_VERSION" ]; then npm install form-data@^$FORMDATA_VERSION; fi' before_install: npm install -g npm -script: npm run coverage \ No newline at end of file +script: npm run coverage +cache: + directories: + - node_modules From a914cca57713914d3d76357883d2bd0f957e473e Mon Sep 17 00:00:00 2001 From: Timothy Gu Date: Thu, 13 Oct 2016 00:44:31 -0700 Subject: [PATCH 16/61] Remove all information related to Coveralls --- .gitignore | 3 --- README.md | 4 ++-- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/.gitignore b/.gitignore index 99c8c2d..f212ad9 100644 --- a/.gitignore +++ b/.gitignore @@ -31,8 +31,5 @@ node_modules # OS files .DS_Store -# Coveralls token files -.coveralls.yml - # Babel-compiled files lib diff --git a/README.md b/README.md index 499324f..0bfb387 100644 --- a/README.md +++ b/README.md @@ -206,5 +206,5 @@ Thanks to [github/fetch](https://github.com/github/fetch) for providing a solid [npm-url]: https://www.npmjs.com/package/node-fetch [travis-image]: https://img.shields.io/travis/bitinn/node-fetch.svg?style=flat-square [travis-url]: https://travis-ci.org/bitinn/node-fetch -[coveralls-image]: https://img.shields.io/codecov/c/github/bitinn/node-fetch.svg?style=flat-square -[coveralls-url]: https://codecov.io/gh/bitinn/node-fetch +[codecov-image]: https://img.shields.io/codecov/c/github/bitinn/node-fetch.svg?style=flat-square +[codecov-url]: https://codecov.io/gh/bitinn/node-fetch From 82c1e781847789d14ea67ad989c167c6eaa52b9d Mon Sep 17 00:00:00 2001 From: Timothy Gu Date: Mon, 10 Oct 2016 13:49:12 -0700 Subject: [PATCH 17/61] Allow constructing Headers with an Array --- src/headers.js | 13 ++++++++++++- test/test.js | 15 +++++++++++++++ 2 files changed, 27 insertions(+), 1 deletion(-) diff --git a/src/headers.js b/src/headers.js index ad2e9e4..88f25f6 100644 --- a/src/headers.js +++ b/src/headers.js @@ -17,7 +17,18 @@ export default class Headers { constructor(headers) { this[MAP] = {}; - // Headers + if (Array.isArray(headers)) { + // array of tuples + for (let el of headers) { + if (!Array.isArray(el) || el.length !== 2) { + throw new TypeError('Header pairs must contain exactly two items'); + } + this.append(el[0], el[1]); + } + return; + } + + // Headers if (headers instanceof Headers) { headers = headers.raw(); } diff --git a/test/test.js b/test/test.js index d781160..bbf71cf 100644 --- a/test/test.js +++ b/test/test.js @@ -1296,6 +1296,21 @@ describe('node-fetch', () => { expect(h3Raw['b']).to.include('1'); }); + it('should accept headers as an array of tuples', function() { + const headers = new Headers([ + ['a', '1'], + ['b', '2'], + ['a', '3'] + ]); + expect(headers.getAll('a')).to.deep.equal(['1', '3']); + expect(headers.getAll('b')).to.deep.equal(['2']); + }); + + it('should throw a TypeError if non-tuple exists in a headers initializer', function() { + expect(() => new Headers([ ['b', '2', 'huh?'] ])).to.throw(TypeError); + expect(() => new Headers([ 'b2' ])).to.throw(TypeError); + }); + it('should support fetch with Request instance', function() { url = `${base}hello`; const req = new Request(url); From ba226399d4b201230397ddb25b3f42725e7484b9 Mon Sep 17 00:00:00 2001 From: Timothy Gu Date: Mon, 10 Oct 2016 14:12:57 -0700 Subject: [PATCH 18/61] Construct Headers object in a spec-compliant fashion --- src/index.js | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/index.js b/src/index.js index 07963a5..f27b5a3 100644 --- a/src/index.js +++ b/src/index.js @@ -152,7 +152,16 @@ function fetch(url, opts) { } // normalize location header for manual redirect mode - const headers = new Headers(res.headers); + const headers = new Headers(); + for (const name of Object.keys(res.headers)) { + if (Array.isArray(res.headers[name])) { + for (const val of res.headers[name]) { + headers.append(name, val); + } + } else { + headers.append(name, res.headers[name]); + } + } if (options.redirect === 'manual' && headers.has('location')) { headers.set('location', resolve_url(options.url, headers.get('location'))); } From fba873d3feea4d1422885ac1c8a1d2d9e83882a3 Mon Sep 17 00:00:00 2001 From: Timothy Gu Date: Mon, 10 Oct 2016 15:04:16 -0700 Subject: [PATCH 19/61] Make sure to coerce header values to string --- src/headers.js | 44 +++++++++++++++++++++----------------------- test/test.js | 40 +++++++++++++++++++--------------------- 2 files changed, 40 insertions(+), 44 deletions(-) diff --git a/src/headers.js b/src/headers.js index 88f25f6..2790772 100644 --- a/src/headers.js +++ b/src/headers.js @@ -17,7 +17,15 @@ export default class Headers { constructor(headers) { this[MAP] = {}; - if (Array.isArray(headers)) { + // Headers + if (headers instanceof Headers) { + let init = headers.raw(); + for (let name of Object.keys(init)) { + for (let value of init[name]) { + this.append(name, value); + } + } + } else if (Array.isArray(headers)) { // array of tuples for (let el of headers) { if (!Array.isArray(el) || el.length !== 2) { @@ -25,28 +33,12 @@ export default class Headers { } this.append(el[0], el[1]); } - return; - } - - // Headers - if (headers instanceof Headers) { - headers = headers.raw(); - } - - // plain object - for (const prop in headers) { - if (!headers.hasOwnProperty(prop)) { - continue; - } - - if (typeof headers[prop] === 'string') { - this.set(prop, headers[prop]); - } else if (typeof headers[prop] === 'number' && !isNaN(headers[prop])) { - this.set(prop, headers[prop].toString()); - } else if (headers[prop] instanceof Array) { - headers[prop].forEach(item => { - this.append(prop, item.toString()); - }); + } else if (typeof headers === 'object') { + // plain object + for (const prop of Object.keys(headers)) { + // We don't worry about converting prop to ByteString here as append() + // will handle it. + this.append(prop, headers[prop]); } } } @@ -99,6 +91,9 @@ export default class Headers { * @return Void */ set(name, value) { + name += ''; + value += ''; + this[MAP][name.toLowerCase()] = [value]; } @@ -110,6 +105,9 @@ export default class Headers { * @return Void */ append(name, value) { + name += ''; + value += ''; + if (!this.has(name)) { this.set(name, value); return; diff --git a/test/test.js b/test/test.js index bbf71cf..3995857 100644 --- a/test/test.js +++ b/test/test.js @@ -1131,8 +1131,7 @@ describe('node-fetch', () => { const expected = [ ["a", "1"] - , ["b", "2"] - , ["b", "3"] + , ["b", "2,3"] , ["c", "4"] ]; expect(result).to.deep.equal(expected); @@ -1224,20 +1223,18 @@ describe('node-fetch', () => { }); it('should ignore unsupported attributes while reading headers', function() { - const FakeHeader = () => {}; - // prototypes are ignored + const FakeHeader = function () {}; + // prototypes are currently ignored + // This might change in the future: #181 FakeHeader.prototype.z = 'fake'; const res = new FakeHeader; - // valid res.a = 'string'; res.b = ['1','2']; res.c = ''; res.d = []; - // common mistakes, normalized res.e = 1; res.f = [1, 2]; - // invalid, ignored res.g = { a:1 }; res.h = undefined; res.i = null; @@ -1247,25 +1244,26 @@ describe('node-fetch', () => { res.m = new Buffer('test'); const h1 = new Headers(res); + h1.set('n', [1, 2]); + h1.append('n', ['3', 4]) + const h1Raw = h1.raw(); expect(h1Raw['a']).to.include('string'); - expect(h1Raw['b']).to.include('1'); - expect(h1Raw['b']).to.include('2'); + expect(h1Raw['b']).to.include('1,2'); expect(h1Raw['c']).to.include(''); - expect(h1Raw['d']).to.be.undefined; - + expect(h1Raw['d']).to.include(''); expect(h1Raw['e']).to.include('1'); - expect(h1Raw['f']).to.include('1'); - expect(h1Raw['f']).to.include('2'); - - expect(h1Raw['g']).to.be.undefined; - expect(h1Raw['h']).to.be.undefined; - expect(h1Raw['i']).to.be.undefined; - expect(h1Raw['j']).to.be.undefined; - expect(h1Raw['k']).to.be.undefined; - expect(h1Raw['l']).to.be.undefined; - expect(h1Raw['m']).to.be.undefined; + expect(h1Raw['f']).to.include('1,2'); + expect(h1Raw['g']).to.include('[object Object]'); + expect(h1Raw['h']).to.include('undefined'); + expect(h1Raw['i']).to.include('null'); + expect(h1Raw['j']).to.include('NaN'); + expect(h1Raw['k']).to.include('true'); + expect(h1Raw['l']).to.include('false'); + expect(h1Raw['m']).to.include('test'); + expect(h1Raw['n']).to.include('1,2'); + expect(h1Raw['n']).to.include('3,4'); expect(h1Raw['z']).to.be.undefined; }); From 2cafdcb5e449f02980ea54b74342ab41d77a1f76 Mon Sep 17 00:00:00 2001 From: Timothy Gu Date: Mon, 10 Oct 2016 15:32:56 -0700 Subject: [PATCH 20/61] Validate headers --- src/common.js | 111 +++++++++++++++++++++++++++++++++++++++++++++++++ src/headers.js | 37 +++++++++++------ test/test.js | 16 +++++++ 3 files changed, 151 insertions(+), 13 deletions(-) create mode 100644 src/common.js diff --git a/src/common.js b/src/common.js new file mode 100644 index 0000000..f4908a3 --- /dev/null +++ b/src/common.js @@ -0,0 +1,111 @@ +/** + * A set of utilities borrowed from Node.js' _http_common.js + */ + +/** + * Verifies that the given val is a valid HTTP token + * per the rules defined in RFC 7230 + * See https://tools.ietf.org/html/rfc7230#section-3.2.6 + * + * Allowed characters in an HTTP token: + * ^_`a-z 94-122 + * A-Z 65-90 + * - 45 + * 0-9 48-57 + * ! 33 + * #$%&' 35-39 + * *+ 42-43 + * . 46 + * | 124 + * ~ 126 + * + * This implementation of checkIsHttpToken() loops over the string instead of + * using a regular expression since the former is up to 180% faster with v8 4.9 + * depending on the string length (the shorter the string, the larger the + * performance difference) + * + * Additionally, checkIsHttpToken() is currently designed to be inlinable by v8, + * so take care when making changes to the implementation so that the source + * code size does not exceed v8's default max_inlined_source_size setting. + **/ +/* istanbul ignore next */ +function isValidTokenChar(ch) { + if (ch >= 94 && ch <= 122) + return true; + if (ch >= 65 && ch <= 90) + return true; + if (ch === 45) + return true; + if (ch >= 48 && ch <= 57) + return true; + if (ch === 34 || ch === 40 || ch === 41 || ch === 44) + return false; + if (ch >= 33 && ch <= 46) + return true; + if (ch === 124 || ch === 126) + return true; + return false; +} +/* istanbul ignore next */ +function checkIsHttpToken(val) { + if (typeof val !== 'string' || val.length === 0) + return false; + if (!isValidTokenChar(val.charCodeAt(0))) + return false; + const len = val.length; + if (len > 1) { + if (!isValidTokenChar(val.charCodeAt(1))) + return false; + if (len > 2) { + if (!isValidTokenChar(val.charCodeAt(2))) + return false; + if (len > 3) { + if (!isValidTokenChar(val.charCodeAt(3))) + return false; + for (var i = 4; i < len; i++) { + if (!isValidTokenChar(val.charCodeAt(i))) + return false; + } + } + } + } + return true; +} +exports._checkIsHttpToken = checkIsHttpToken; + +/** + * True if val contains an invalid field-vchar + * field-value = *( field-content / obs-fold ) + * field-content = field-vchar [ 1*( SP / HTAB ) field-vchar ] + * field-vchar = VCHAR / obs-text + * + * checkInvalidHeaderChar() is currently designed to be inlinable by v8, + * so take care when making changes to the implementation so that the source + * code size does not exceed v8's default max_inlined_source_size setting. + **/ +/* istanbul ignore next */ +function checkInvalidHeaderChar(val) { + val += ''; + if (val.length < 1) + return false; + var c = val.charCodeAt(0); + if ((c <= 31 && c !== 9) || c > 255 || c === 127) + return true; + if (val.length < 2) + return false; + c = val.charCodeAt(1); + if ((c <= 31 && c !== 9) || c > 255 || c === 127) + return true; + if (val.length < 3) + return false; + c = val.charCodeAt(2); + if ((c <= 31 && c !== 9) || c > 255 || c === 127) + return true; + for (var i = 3; i < val.length; ++i) { + c = val.charCodeAt(i); + if ((c <= 31 && c !== 9) || c > 255 || c === 127) + return true; + } + return false; +} +exports._checkInvalidHeaderChar = checkInvalidHeaderChar; diff --git a/src/headers.js b/src/headers.js index 2790772..a7eff55 100644 --- a/src/headers.js +++ b/src/headers.js @@ -5,8 +5,25 @@ * Headers class offers convenient helpers */ -export const MAP = Symbol('map'); +import { _checkIsHttpToken, _checkInvalidHeaderChar } from './common.js'; +function sanitizeName(name) { + name += ''; + if (!_checkIsHttpToken(name)) { + throw new TypeError(`${name} is not a legal HTTP header name`); + } + return name.toLowerCase(); +} + +function sanitizeValue(value) { + value += ''; + if (_checkInvalidHeaderChar(value)) { + throw new TypeError(`${value} is not a legal HTTP header value`); + } + return value; +} + +export const MAP = Symbol('map'); export default class Headers { /** * Headers class @@ -50,7 +67,7 @@ export default class Headers { * @return Mixed */ get(name) { - const list = this[MAP][name.toLowerCase()]; + const list = this[MAP][sanitizeName(name)]; return list ? list[0] : null; } @@ -65,7 +82,7 @@ export default class Headers { return []; } - return this[MAP][name.toLowerCase()]; + return this[MAP][sanitizeName(name)]; } /** @@ -91,10 +108,7 @@ export default class Headers { * @return Void */ set(name, value) { - name += ''; - value += ''; - - this[MAP][name.toLowerCase()] = [value]; + this[MAP][sanitizeName(name)] = [sanitizeValue(value)]; } /** @@ -105,15 +119,12 @@ export default class Headers { * @return Void */ append(name, value) { - name += ''; - value += ''; - if (!this.has(name)) { this.set(name, value); return; } - this[MAP][name.toLowerCase()].push(value); + this[MAP][sanitizeName(name)].push(sanitizeValue(value)); } /** @@ -123,7 +134,7 @@ export default class Headers { * @return Boolean */ has(name) { - return this[MAP].hasOwnProperty(name.toLowerCase()); + return this[MAP].hasOwnProperty(sanitizeName(name)); } /** @@ -133,7 +144,7 @@ export default class Headers { * @return Void */ delete(name) { - delete this[MAP][name.toLowerCase()]; + delete this[MAP][sanitizeName(name)]; }; /** diff --git a/test/test.js b/test/test.js index 3995857..1cad476 100644 --- a/test/test.js +++ b/test/test.js @@ -1208,6 +1208,22 @@ describe('node-fetch', () => { }); }); + it('should reject illegal header', function() { + const headers = new Headers(); + expect(() => new Headers({ 'He y': 'ok' })).to.throw(TypeError); + expect(() => new Headers({ 'Hé-y': 'ok' })).to.throw(TypeError); + expect(() => new Headers({ 'He-y': 'ăk' })).to.throw(TypeError); + expect(() => headers.append('Hé-y', 'ok')) .to.throw(TypeError); + expect(() => headers.delete('Hé-y')) .to.throw(TypeError); + expect(() => headers.get('Hé-y')) .to.throw(TypeError); + expect(() => headers.getAll('Hé-y')) .to.throw(TypeError); + expect(() => headers.has('Hé-y')) .to.throw(TypeError); + expect(() => headers.set('Hé-y', 'ok')) .to.throw(TypeError); + + // 'o k' is valid value but invalid name + new Headers({ 'He-y': 'o k' }); + }); + it('should send request with connection keep-alive if agent is provided', function() { url = `${base}inspect`; opts = { From 2a7ef63bc410fea58776bce1f9a11729f435c351 Mon Sep 17 00:00:00 2001 From: Timothy Gu Date: Mon, 10 Oct 2016 18:31:53 -0700 Subject: [PATCH 21/61] Add FOLLOW_SPEC mode --- src/headers.js | 46 +++++++++++++----- src/index.js | 10 ++++ test/test.js | 125 ++++++++++++++++++++++++++++++++----------------- 3 files changed, 127 insertions(+), 54 deletions(-) diff --git a/src/headers.js b/src/headers.js index a7eff55..9ad129e 100644 --- a/src/headers.js +++ b/src/headers.js @@ -24,6 +24,7 @@ function sanitizeValue(value) { } export const MAP = Symbol('map'); +const FOLLOW_SPEC = Symbol('followSpec'); export default class Headers { /** * Headers class @@ -33,6 +34,7 @@ export default class Headers { */ constructor(headers) { this[MAP] = {}; + this[FOLLOW_SPEC] = Headers.FOLLOW_SPEC; // Headers if (headers instanceof Headers) { @@ -68,7 +70,11 @@ export default class Headers { */ get(name) { const list = this[MAP][sanitizeName(name)]; - return list ? list[0] : null; + if (!list) { + return null; + } + + return this[FOLLOW_SPEC] ? list.join(',') : list[0]; } /** @@ -162,8 +168,12 @@ export default class Headers { * @return Iterator */ keys() { - const keys = []; - this.forEach((_, name) => keys.push(name)); + let keys = []; + if (this[FOLLOW_SPEC]) { + keys = Object.keys(this[MAP]).sort(); + } else { + this.forEach((_, name) => keys.push(name)); + }; return new Iterator(keys); } @@ -172,10 +182,16 @@ export default class Headers { * * @return Iterator */ - values() { - const values = []; - this.forEach(value => values.push(value)); - return new Iterator(values); + *values() { + if (this[FOLLOW_SPEC]) { + for (const name of this.keys()) { + yield this.get(name); + } + } else { + const values = []; + this.forEach(value => values.push(value)); + yield* new Iterator(values); + } } /** @@ -183,10 +199,16 @@ export default class Headers { * * @return Iterator */ - entries() { - const entries = []; - this.forEach((value, name) => entries.push([name, value])); - return new Iterator(entries); + *entries() { + if (this[FOLLOW_SPEC]) { + for (const name of this.keys()) { + yield [name, this.get(name)]; + } + } else { + const entries = []; + this.forEach((value, name) => entries.push([name, value])); + yield* new Iterator(entries); + } } /** @@ -208,6 +230,8 @@ export default class Headers { } } +Headers.FOLLOW_SPEC = false; + const ITEMS = Symbol('items'); class Iterator { constructor(items) { diff --git a/src/index.js b/src/index.js index f27b5a3..6a889fd 100644 --- a/src/index.js +++ b/src/index.js @@ -32,6 +32,7 @@ function fetch(url, opts) { } Body.Promise = fetch.Promise; + Headers.FOLLOW_SPEC = fetch.FOLLOW_SPEC; // wrap http.request into fetch return new fetch.Promise((resolve, reject) => { @@ -258,6 +259,15 @@ fetch.isRedirect = code => code === 301 || code === 302 || code === 303 || code // expose Promise fetch.Promise = global.Promise; +/** + * Option to make newly constructed Headers objects conformant to the + * **latest** version of the Fetch Standard. Note, that most other + * implementations of fetch() have not yet been updated to the latest + * version, so enabling this option almost certainly breaks any isomorphic + * attempt. Also, changing this variable will only affect new Headers + * objects; existing objects are not affected. + */ +fetch.FOLLOW_SPEC = false; fetch.Response = Response; fetch.Headers = Headers; fetch.Request = Request; diff --git a/test/test.js b/test/test.js index 1cad476..5f61eb8 100644 --- a/test/test.js +++ b/test/test.js @@ -28,18 +28,26 @@ import FetchError from '../src/fetch-error.js'; // test with native promise on node 0.11, and bluebird for node 0.10 fetch.Promise = fetch.Promise || bluebird; -let url, opts, local, base; +const local = new TestServer(); +const base = `http://${local.hostname}:${local.port}/`; +let url, opts; -describe('node-fetch', () => { +before(done => { + local.start(done); +}); - before(done => { - local = new TestServer(); - base = `http://${local.hostname}:${local.port}/`; - local.start(done); - }); +after(done => { + local.stop(done); +}); - after(done => { - local.stop(done); +(runner => { + runner(false); + runner(true); +})(defaultFollowSpec => { + +describe(`node-fetch with FOLLOW_SPEC = ${defaultFollowSpec}`, () => { + before(() => { + fetch.FOLLOW_SPEC = Headers.FOLLOW_SPEC = defaultFollowSpec; }); it('should return a promise', function() { @@ -1109,8 +1117,9 @@ describe('node-fetch', () => { it('should allow get all responses of a header', function() { url = `${base}cookie`; return fetch(url).then(res => { - expect(res.headers.get('set-cookie')).to.equal('a=1'); - expect(res.headers.get('Set-Cookie')).to.equal('a=1'); + const expected = fetch.FOLLOW_SPEC ? 'a=1,b=1' : 'a=1'; + expect(res.headers.get('set-cookie')).to.equal(expected); + expect(res.headers.get('Set-Cookie')).to.equal(expected); expect(res.headers.getAll('set-cookie')).to.deep.equal(['a=1', 'b=1']); expect(res.headers.getAll('Set-Cookie')).to.deep.equal(['a=1', 'b=1']); }); @@ -1138,11 +1147,11 @@ describe('node-fetch', () => { }); it('should allow iterating through all headers with for-of loop', function() { - const headers = new Headers({ - a: '1' - , b: '2' - , c: '4' - }); + const headers = new Headers([ + ['b', '2'], + ['c', '4'], + ['a', '1'] + ]); headers.append('b', '3'); expect(headers).to.be.iterable; @@ -1150,53 +1159,81 @@ describe('node-fetch', () => { for (let pair of headers) { result.push(pair); } - expect(result).to.deep.equal([ - ["a", "1"] - , ["b", "2"] - , ["b", "3"] - , ["c", "4"] + expect(result).to.deep.equal(Headers.FOLLOW_SPEC ? [ + ['a', '1'], + ['b', '2,3'], + ['c', '4'] + ] : [ + ['b', '2'], + ['b', '3'], + ['c', '4'], + ['a', '1'], ]); }); it('should allow iterating through all headers with entries()', function() { - const headers = new Headers({ - a: '1' - , b: '2' - , c: '4' - }); + const headers = new Headers([ + ['b', '2'], + ['c', '4'], + ['a', '1'] + ]); headers.append('b', '3'); expect(headers.entries()).to.be.iterable - .and.to.deep.iterate.over([ - ["a", "1"] - , ["b", "2"] - , ["b", "3"] - , ["c", "4"] + .and.to.deep.iterate.over(Headers.FOLLOW_SPEC ? [ + ['a', '1'], + ['b', '2,3'], + ['c', '4'] + ] : [ + ['b', '2'], + ['b', '3'], + ['c', '4'], + ['a', '1'], ]); }); it('should allow iterating through all headers with keys()', function() { - const headers = new Headers({ - a: '1' - , b: '2' - , c: '4' - }); + const headers = new Headers([ + ['b', '2'], + ['c', '4'], + ['a', '1'] + ]); headers.append('b', '3'); expect(headers.keys()).to.be.iterable - .and.to.iterate.over(['a', 'b', 'b', 'c']); + .and.to.iterate.over(Headers.FOLLOW_SPEC ? ['a', 'b', 'c'] : ['b', 'b', 'c', 'a']); }); it('should allow iterating through all headers with values()', function() { - const headers = new Headers({ - a: '1' - , b: '2' - , c: '4' - }); + const headers = new Headers([ + ['b', '2'], + ['c', '4'], + ['a', '1'] + ]); headers.append('b', '3'); expect(headers.values()).to.be.iterable - .and.to.iterate.over(['1', '2', '3', '4']); + .and.to.iterate.over(Headers.FOLLOW_SPEC ? ['1', '2,3', '4'] : ['2', '3', '4', '1']); + }); + + it('should only apply FOLLOW_SPEC when it is requested', function () { + Headers.FOLLOW_SPEC = true; + + const src = [ + ['b', '2'], + ['b', '3'] + ]; + + let headers = new Headers(src); + expect(headers.get('b')).to.equal('2,3'); + + Headers.FOLLOW_SPEC = false; + expect(headers.get('b')).to.equal('2,3'); + + headers = new Headers(src); + expect(headers.get('b')).to.equal('2'); + + Headers.FOLLOW_SPEC = defaultFollowSpec; }); it('should allow deleting header', function() { @@ -1612,3 +1649,5 @@ describe('node-fetch', () => { }); }); + +}); From f829b71ddb1ec433e463585fdcb69e232bd6fffb Mon Sep 17 00:00:00 2001 From: Timothy Gu Date: Wed, 12 Oct 2016 17:48:19 -0700 Subject: [PATCH 22/61] Use babel-runtime's Iterator implementation --- src/headers.js | 33 ++++----------------------------- 1 file changed, 4 insertions(+), 29 deletions(-) diff --git a/src/headers.js b/src/headers.js index 9ad129e..ece6b0b 100644 --- a/src/headers.js +++ b/src/headers.js @@ -5,6 +5,7 @@ * Headers class offers convenient helpers */ +import getIterator from 'babel-runtime/core-js/get-iterator'; import { _checkIsHttpToken, _checkInvalidHeaderChar } from './common.js'; function sanitizeName(name) { @@ -174,7 +175,7 @@ export default class Headers { } else { this.forEach((_, name) => keys.push(name)); }; - return new Iterator(keys); + return getIterator(keys); } /** @@ -190,7 +191,7 @@ export default class Headers { } else { const values = []; this.forEach(value => values.push(value)); - yield* new Iterator(values); + yield* getIterator(values); } } @@ -207,7 +208,7 @@ export default class Headers { } else { const entries = []; this.forEach((value, name) => entries.push([name, value])); - yield* new Iterator(entries); + yield* getIterator(entries); } } @@ -231,29 +232,3 @@ export default class Headers { } Headers.FOLLOW_SPEC = false; - -const ITEMS = Symbol('items'); -class Iterator { - constructor(items) { - this[ITEMS] = items; - } - - next() { - if (!this[ITEMS].length) { - return { - value: undefined, - done: true - }; - } - - return { - value: this[ITEMS].shift(), - done: false - }; - - } - - [Symbol.iterator]() { - return this; - } -} From d3b4161d7c26210aa115047844d2504ca58a87a8 Mon Sep 17 00:00:00 2001 From: Timothy Gu Date: Wed, 12 Oct 2016 21:47:36 -0700 Subject: [PATCH 23/61] Add a new res.textConverted() and always use UTF-8 for res.text() Also uses iconv-lite directly instead of using the "encoding" package. Fixes #184. --- src/body.js | 38 ++++++++++++++++++++------------------ test/test.js | 28 +++++++++++++++++++--------- 2 files changed, 39 insertions(+), 27 deletions(-) diff --git a/src/body.js b/src/body.js index bd30eb6..48a7c89 100644 --- a/src/body.js +++ b/src/body.js @@ -77,6 +77,16 @@ export default class Body { return this[CONSUME_BODY](); } + /** + * Decode response as text, while automatically detecting the encoding and + * trying to decode to UTF-8 (non-spec api) + * + * @return Promise + */ + textConverted() { + return this[CONSUME_BODY]().then(buffer => convertBody(buffer, this.headers)); + } + /** * Decode buffers into utf-8 string * @@ -96,12 +106,12 @@ export default class Body { // body is string if (typeof this.body === 'string') { - return Body.Promise.resolve(convertBody([new Buffer(this.body)], this.headers)); + return Body.Promise.resolve(new Buffer(this.body)); } // body is buffer if (Buffer.isBuffer(this.body)) { - return Body.Promise.resolve(convertBody([this.body], this.headers)); + return Body.Promise.resolve(this.body); } // body is stream @@ -147,7 +157,7 @@ export default class Body { } clearTimeout(resTimeout); - resolve(convertBody(accum, this.headers)); + resolve(Buffer.concat(accum)); }); }); } @@ -158,11 +168,11 @@ export default class Body { * Detect buffer encoding and convert to target encoding * ref: http://www.w3.org/TR/2011/WD-html5-20110113/parsing.html#determining-the-character-encoding * - * @param Array arrayOfBuffers Array of buffers + * @param Buffer buffer Incoming buffer * @param String encoding Target encoding * @return String */ -function convertBody(arrayOfBuffers, headers) { +function convertBody(buffer, headers) { const ct = headers.get('content-type'); let charset = 'utf-8'; let res, str; @@ -171,22 +181,14 @@ function convertBody(arrayOfBuffers, headers) { if (ct) { // skip encoding detection altogether if not html/xml/plain text if (!/text\/html|text\/plain|\+xml|\/xml/i.test(ct)) { - return Buffer.concat(arrayOfBuffers); + return buffer; } res = /charset=([^;]*)/i.exec(ct); } // no charset in content type, peek at response body for at most 1024 bytes - if (!res && arrayOfBuffers.length > 0) { - for (let i = 0; i < arrayOfBuffers.length; i++) { - str += arrayOfBuffers[i].toString() - if (str.length > 1024) { - break; - } - } - str = str.substr(0, 1024); - } + str = buffer.slice(0, 1024).toString(); // html5 if (!res && str) { @@ -220,10 +222,10 @@ function convertBody(arrayOfBuffers, headers) { // turn raw buffers into a single utf-8 buffer return convert( - Buffer.concat(arrayOfBuffers) - , 'utf-8' + buffer + , 'UTF-8' , charset - ); + ).toString(); } /** diff --git a/test/test.js b/test/test.js index 5f61eb8..51d10eb 100644 --- a/test/test.js +++ b/test/test.js @@ -923,11 +923,21 @@ describe(`node-fetch with FOLLOW_SPEC = ${defaultFollowSpec}`, () => { }); }); - it('should support encoding decode, xml dtd detect', function() { + it('should only use UTF-8 decoding with text()', function() { url = `${base}encoding/euc-jp`; return fetch(url).then(res => { expect(res.status).to.equal(200); return res.text().then(result => { + expect(result).to.equal('\ufffd\ufffd\ufffd\u0738\ufffd'); + }); + }); + }); + + it('should support encoding decode, xml dtd detect', function() { + url = `${base}encoding/euc-jp`; + return fetch(url).then(res => { + expect(res.status).to.equal(200); + return res.textConverted().then(result => { expect(result).to.equal('日本語'); }); }); @@ -937,7 +947,7 @@ describe(`node-fetch with FOLLOW_SPEC = ${defaultFollowSpec}`, () => { url = `${base}encoding/shift-jis`; return fetch(url).then(res => { expect(res.status).to.equal(200); - return res.text().then(result => { + return res.textConverted().then(result => { expect(result).to.equal('
日本語
'); }); }); @@ -947,7 +957,7 @@ describe(`node-fetch with FOLLOW_SPEC = ${defaultFollowSpec}`, () => { url = `${base}encoding/gbk`; return fetch(url).then(res => { expect(res.status).to.equal(200); - return res.text().then(result => { + return res.textConverted().then(result => { expect(result).to.equal('
中文
'); }); }); @@ -957,7 +967,7 @@ describe(`node-fetch with FOLLOW_SPEC = ${defaultFollowSpec}`, () => { url = `${base}encoding/gb2312`; return fetch(url).then(res => { expect(res.status).to.equal(200); - return res.text().then(result => { + return res.textConverted().then(result => { expect(result).to.equal('
中文
'); }); }); @@ -968,7 +978,7 @@ describe(`node-fetch with FOLLOW_SPEC = ${defaultFollowSpec}`, () => { return fetch(url).then(res => { expect(res.status).to.equal(200); expect(res.headers.get('content-type')).to.be.null; - return res.text().then(result => { + return res.textConverted().then(result => { expect(result).to.equal('中文'); }); }); @@ -978,7 +988,7 @@ describe(`node-fetch with FOLLOW_SPEC = ${defaultFollowSpec}`, () => { url = `${base}encoding/order1`; return fetch(url).then(res => { expect(res.status).to.equal(200); - return res.text().then(result => { + return res.textConverted().then(result => { expect(result).to.equal('中文'); }); }); @@ -988,7 +998,7 @@ describe(`node-fetch with FOLLOW_SPEC = ${defaultFollowSpec}`, () => { url = `${base}encoding/order2`; return fetch(url).then(res => { expect(res.status).to.equal(200); - return res.text().then(result => { + return res.textConverted().then(result => { expect(result).to.equal('中文'); }); }); @@ -999,7 +1009,7 @@ describe(`node-fetch with FOLLOW_SPEC = ${defaultFollowSpec}`, () => { return fetch(url).then(res => { expect(res.status).to.equal(200); const padding = 'a'.repeat(10); - return res.text().then(result => { + return res.textConverted().then(result => { expect(result).to.equal(`${padding}
日本語
`); }); }); @@ -1010,7 +1020,7 @@ describe(`node-fetch with FOLLOW_SPEC = ${defaultFollowSpec}`, () => { return fetch(url).then(res => { expect(res.status).to.equal(200); const padding = 'a'.repeat(1200); - return res.text().then(result => { + return res.textConverted().then(result => { expect(result).to.not.equal(`${padding}中文`); }); }); From 1b951701ece70345976925cc0b0dfffdd27b19b8 Mon Sep 17 00:00:00 2001 From: Timothy Gu Date: Thu, 13 Oct 2016 00:26:55 -0700 Subject: [PATCH 24/61] Remove logic to skip if not HTML in textConverted As this function is now separated from the general purpose text(), we should be more specific in our purpose. --- src/body.js | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/body.js b/src/body.js index 48a7c89..dee16de 100644 --- a/src/body.js +++ b/src/body.js @@ -179,11 +179,6 @@ function convertBody(buffer, headers) { // header if (ct) { - // skip encoding detection altogether if not html/xml/plain text - if (!/text\/html|text\/plain|\+xml|\/xml/i.test(ct)) { - return buffer; - } - res = /charset=([^;]*)/i.exec(ct); } From 53e1055845459d77dedf29451bc2b1b784ad96f1 Mon Sep 17 00:00:00 2001 From: Timothy Gu Date: Sat, 15 Oct 2016 10:02:52 -0700 Subject: [PATCH 25/61] Use Object.create(null) for Headers' internal map Suggested by @jimmywarting. --- src/headers.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/headers.js b/src/headers.js index ece6b0b..ce320b5 100644 --- a/src/headers.js +++ b/src/headers.js @@ -34,7 +34,7 @@ export default class Headers { * @return Void */ constructor(headers) { - this[MAP] = {}; + this[MAP] = Object.create(null); this[FOLLOW_SPEC] = Headers.FOLLOW_SPEC; // Headers @@ -100,11 +100,11 @@ export default class Headers { * @return Void */ forEach(callback, thisArg) { - Object.getOwnPropertyNames(this[MAP]).forEach(name => { + for (let name in this[MAP]) { this[MAP][name].forEach(value => { callback.call(thisArg, value, name, this); }); - }); + } } /** @@ -141,7 +141,7 @@ export default class Headers { * @return Boolean */ has(name) { - return this[MAP].hasOwnProperty(sanitizeName(name)); + return !!this[MAP][sanitizeName(name)]; } /** From b092a8ed12f07ec2838d3b05572a3d3920c1b350 Mon Sep 17 00:00:00 2001 From: Timothy Gu Date: Wed, 12 Oct 2016 19:25:00 -0700 Subject: [PATCH 26/61] Add test for constructing Request with parsed URL object --- test/test.js | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/test/test.js b/test/test.js index 51d10eb..0871a07 100644 --- a/test/test.js +++ b/test/test.js @@ -9,6 +9,7 @@ import {spawn} from 'child_process'; import * as stream from 'stream'; import resumer from 'resumer'; import FormData from 'form-data'; +import {parse as parseURL} from 'url'; import * as http from 'http'; import * as fs from 'fs'; @@ -1382,6 +1383,17 @@ describe(`node-fetch with FOLLOW_SPEC = ${defaultFollowSpec}`, () => { }); }); + it('should support fetch with Node.js URL object', function() { + url = `${base}hello`; + const urlObj = parseURL(url); + const req = new Request(urlObj); + return fetch(req).then(res => { + expect(res.url).to.equal(url); + expect(res.ok).to.be.true; + expect(res.status).to.equal(200); + }); + }); + it('should support wrapping Request instance', function() { url = `${base}hello`; From 76cb57cace246fa6d5764d761247fdbe8e08519a Mon Sep 17 00:00:00 2001 From: Timothy Gu Date: Wed, 12 Oct 2016 19:56:47 -0700 Subject: [PATCH 27/61] Support WHATWG URL objects Fixes #175. --- package.json | 10 ++++++++-- src/request.js | 10 +++++++++- test/test.js | 17 +++++++++++++++++ 3 files changed, 34 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index bdc513f..1b0b39a 100644 --- a/package.json +++ b/package.json @@ -43,7 +43,8 @@ "nyc": "^8.3.0", "parted": "^0.1.1", "promise": "^7.1.1", - "resumer": "0.0.0" + "resumer": "0.0.0", + "whatwg-url": "^3.0.0" }, "dependencies": { "babel-runtime": "^6.11.6", @@ -53,7 +54,12 @@ }, "babel": { "presets": [ - ["es2015", {"loose": true}] + [ + "es2015", + { + "loose": true + } + ] ], "plugins": [ "transform-runtime" diff --git a/src/request.js b/src/request.js index 978e5ec..0ba13e7 100644 --- a/src/request.js +++ b/src/request.js @@ -22,7 +22,15 @@ export default class Request extends Body { // normalize input if (!(input instanceof Request)) { - parsedURL = parse_url(input); + if (input && input.href) { + // in order to support Node.js' Url objects; though WHATWG's URL objects + // will fall into this branch also (since their `toString()` will return + // `href` property anyway) + parsedURL = parse_url(input.href); + } else { + // coerce input to a string before attempting to parse + parsedURL = parse_url(input + ''); + } input = {}; } else { parsedURL = parse_url(input.url); diff --git a/test/test.js b/test/test.js index 0871a07..0fa797b 100644 --- a/test/test.js +++ b/test/test.js @@ -29,6 +29,12 @@ import FetchError from '../src/fetch-error.js'; // test with native promise on node 0.11, and bluebird for node 0.10 fetch.Promise = fetch.Promise || bluebird; +let URL; +// whatwg-url doesn't support old Node.js, so make it optional +try { + URL = require('whatwg-url').URL; +} catch (err) {} + const local = new TestServer(); const base = `http://${local.hostname}:${local.port}/`; let url, opts; @@ -1394,6 +1400,17 @@ describe(`node-fetch with FOLLOW_SPEC = ${defaultFollowSpec}`, () => { }); }); + (URL ? it : it.skip)('should support fetch with WHATWG URL object', function() { + url = `${base}hello`; + const urlObj = new URL(url); + const req = new Request(urlObj); + return fetch(req).then(res => { + expect(res.url).to.equal(url); + expect(res.ok).to.be.true; + expect(res.status).to.equal(200); + }); + }); + it('should support wrapping Request instance', function() { url = `${base}hello`; From 4a9a3246f633c3ddb358d3e79a38b39bb2a61bb1 Mon Sep 17 00:00:00 2001 From: Timothy Gu Date: Fri, 4 Nov 2016 21:50:58 -0700 Subject: [PATCH 28/61] Remove dependency on Node.js' util module Closes #194. --- src/fetch-error.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/fetch-error.js b/src/fetch-error.js index cc67925..a417919 100644 --- a/src/fetch-error.js +++ b/src/fetch-error.js @@ -14,11 +14,11 @@ * @return FetchError */ export default function FetchError(message, type, systemError) { + Error.call(this, message); // hide custom error implementation details from end-users Error.captureStackTrace(this, this.constructor); - this.name = this.constructor.name; this.message = message; this.type = type; @@ -29,4 +29,5 @@ export default function FetchError(message, type, systemError) { } -require('util').inherits(FetchError, Error); +FetchError.prototype = Object.create(Error.prototype); +FetchError.prototype.name = 'FetchError'; From 72d34af6e257503d79e77cdd3b520999f2d0fd94 Mon Sep 17 00:00:00 2001 From: Timothy Gu Date: Sat, 5 Nov 2016 09:39:01 -0700 Subject: [PATCH 29/61] Start test server when executed directly --- test/server.js | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/test/server.js b/test/server.js index 5a75b29..403f9ed 100644 --- a/test/server.js +++ b/test/server.js @@ -329,3 +329,10 @@ export default class TestServer { } } } + +if (require.main === module) { + const server = new TestServer; + server.start(() => { + console.log(`Server started listening at port ${server.port}`); + }); +} From 5fe80dba06fbf384cc712a299c698b884a857cfc Mon Sep 17 00:00:00 2001 From: Timothy Gu Date: Sat, 5 Nov 2016 10:26:30 -0700 Subject: [PATCH 30/61] Remove dependency on babel-polyfill This way the tests can better emulate the real Node.js environment. --- package.json | 4 +--- test/server.js | 8 +++----- test/test.js | 24 ++++++++++++++++-------- 3 files changed, 20 insertions(+), 16 deletions(-) diff --git a/package.json b/package.json index 1b0b39a..2e27124 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,7 @@ "scripts": { "build": "babel -d lib src", "prepublish": "npm run build", - "test": "mocha --compilers js:babel-polyfill --compilers js:babel-register test/test.js", + "test": "mocha --compilers js:babel-register test/test.js", "report": "cross-env BABEL_ENV=test nyc --reporter lcov --reporter text mocha -R spec test/test.js", "coverage": "cross-env BABEL_ENV=test nyc --reporter json --reporter text mocha -R spec test/test.js && codecov -f coverage/coverage-final.json" }, @@ -29,7 +29,6 @@ "babel-cli": "^6.16.0", "babel-plugin-istanbul": "^2.0.1", "babel-plugin-transform-runtime": "^6.15.0", - "babel-polyfill": "^6.16.0", "babel-preset-es2015": "^6.16.0", "babel-register": "^6.16.3", "bluebird": "^3.3.4", @@ -77,7 +76,6 @@ "src/*.js" ], "require": [ - "babel-polyfill", "babel-register" ], "sourceMap": false, diff --git a/test/server.js b/test/server.js index 403f9ed..804603a 100644 --- a/test/server.js +++ b/test/server.js @@ -1,4 +1,4 @@ -import 'babel-polyfill'; +import repeat from 'babel-runtime/core-js/string/repeat'; import * as http from 'http'; import { parse } from 'url'; import * as zlib from 'zlib'; @@ -188,8 +188,7 @@ export default class TestServer { res.statusCode = 200; res.setHeader('Content-Type', 'text/html'); res.setHeader('Transfer-Encoding', 'chunked'); - const padding = 'a'; - res.write(padding.repeat(10)); + res.write(repeat('a', 10)); res.end(convert('
日本語
', 'Shift_JIS')); } @@ -197,8 +196,7 @@ export default class TestServer { res.statusCode = 200; res.setHeader('Content-Type', 'text/html'); res.setHeader('Transfer-Encoding', 'chunked'); - const padding = 'a'.repeat(120); - res.write(padding.repeat(10)); + res.write(repeat('a', 1200)); res.end(convert('中文', 'gbk')); } diff --git a/test/test.js b/test/test.js index 0fa797b..5dd5d5b 100644 --- a/test/test.js +++ b/test/test.js @@ -1,5 +1,6 @@ // test tools +import repeat from 'babel-runtime/core-js/string/repeat'; import chai from 'chai'; import chaiPromised from 'chai-as-promised'; import chaiIterator from 'chai-iterator'; @@ -35,6 +36,11 @@ try { URL = require('whatwg-url').URL; } catch (err) {} +const supportToString = ({ + [Symbol.toStringTag]: 'z' +}).toString() === '[object z]'; +const supportIterator = !!(global.Symbol && global.Symbol.iterator); + const local = new TestServer(); const base = `http://${local.hostname}:${local.port}/`; let url, opts; @@ -89,7 +95,7 @@ describe(`node-fetch with FOLLOW_SPEC = ${defaultFollowSpec}`, () => { expect(fetch.Request).to.equal(Request); }); - it('should support proper toString output for Headers, Response and Request objects', function() { + (supportToString ? it : it.skip)('should support proper toString output for Headers, Response and Request objects', function() { expect(new Headers().toString()).to.equal('[object Headers]'); expect(new Response().toString()).to.equal('[object Response]'); expect(new Request(base).toString()).to.equal('[object Request]'); @@ -1015,7 +1021,7 @@ describe(`node-fetch with FOLLOW_SPEC = ${defaultFollowSpec}`, () => { url = `${base}encoding/chunked`; return fetch(url).then(res => { expect(res.status).to.equal(200); - const padding = 'a'.repeat(10); + const padding = repeat('a', 10); return res.textConverted().then(result => { expect(result).to.equal(`${padding}
日本語
`); }); @@ -1026,7 +1032,7 @@ describe(`node-fetch with FOLLOW_SPEC = ${defaultFollowSpec}`, () => { url = `${base}encoding/invalid`; return fetch(url).then(res => { expect(res.status).to.equal(200); - const padding = 'a'.repeat(1200); + const padding = repeat('a', 1200); return res.textConverted().then(result => { expect(result).to.not.equal(`${padding}中文`); }); @@ -1170,7 +1176,9 @@ describe(`node-fetch with FOLLOW_SPEC = ${defaultFollowSpec}`, () => { ['a', '1'] ]); headers.append('b', '3'); - expect(headers).to.be.iterable; + if (supportIterator) { + expect(headers).to.be.iterable; + } const result = []; for (let pair of headers) { @@ -1181,14 +1189,14 @@ describe(`node-fetch with FOLLOW_SPEC = ${defaultFollowSpec}`, () => { ['b', '2,3'], ['c', '4'] ] : [ - ['b', '2'], + ['b', '2'], ['b', '3'], ['c', '4'], ['a', '1'], ]); }); - it('should allow iterating through all headers with entries()', function() { + (supportIterator ? it : it.skip)('should allow iterating through all headers with entries()', function() { const headers = new Headers([ ['b', '2'], ['c', '4'], @@ -1209,7 +1217,7 @@ describe(`node-fetch with FOLLOW_SPEC = ${defaultFollowSpec}`, () => { ]); }); - it('should allow iterating through all headers with keys()', function() { + (supportIterator ? it : it.skip)('should allow iterating through all headers with keys()', function() { const headers = new Headers([ ['b', '2'], ['c', '4'], @@ -1221,7 +1229,7 @@ describe(`node-fetch with FOLLOW_SPEC = ${defaultFollowSpec}`, () => { .and.to.iterate.over(Headers.FOLLOW_SPEC ? ['a', 'b', 'c'] : ['b', 'b', 'c', 'a']); }); - it('should allow iterating through all headers with values()', function() { + (supportIterator ? it : it.skip)('should allow iterating through all headers with values()', function() { const headers = new Headers([ ['b', '2'], ['c', '4'], From 31bc2835dd45927af2697073ca4b294ec7244742 Mon Sep 17 00:00:00 2001 From: Timothy Gu Date: Wed, 23 Nov 2016 11:17:42 -0800 Subject: [PATCH 31/61] Start using Rollup Smaller distributed tarball. --- .babelrc | 25 +++++++++++++++++++++++++ .nycrc | 10 ++++++++++ package.json | 47 ++++++++++++----------------------------------- rollup.config.js | 17 +++++++++++++++++ src/common.js | 6 ++---- src/headers.js | 6 +++--- 6 files changed, 69 insertions(+), 42 deletions(-) create mode 100644 .babelrc create mode 100644 .nycrc create mode 100644 rollup.config.js diff --git a/.babelrc b/.babelrc new file mode 100644 index 0000000..50318ff --- /dev/null +++ b/.babelrc @@ -0,0 +1,25 @@ +{ + "plugins": [ + "transform-runtime" + ], + "env": { + "test": { + "presets": [ + [ "es2015", { "loose": true } ] + ] + }, + "coverage": { + "presets": [ + [ "es2015", { "loose": true } ] + ], + "plugins": [ + "istanbul" + ] + }, + "rollup": { + "presets": [ + [ "es2015", { "loose": true, "modules": false } ] + ] + } + } +} diff --git a/.nycrc b/.nycrc new file mode 100644 index 0000000..ad1e790 --- /dev/null +++ b/.nycrc @@ -0,0 +1,10 @@ +{ + "include": [ + "src/*.js" + ], + "require": [ + "babel-register" + ], + "sourceMap": false, + "instrument": false +} diff --git a/package.json b/package.json index 2e27124..2b514ac 100644 --- a/package.json +++ b/package.json @@ -3,12 +3,17 @@ "version": "1.6.3", "description": "A light-weight module that brings window.fetch to node.js and io.js", "main": "lib/index.js", + "jsnext:main": "lib/index.es.js", + "files": [ + "lib/index.js", + "lib/index.es.js" + ], "scripts": { - "build": "babel -d lib src", + "build": "rollup -c", "prepublish": "npm run build", - "test": "mocha --compilers js:babel-register test/test.js", - "report": "cross-env BABEL_ENV=test nyc --reporter lcov --reporter text mocha -R spec test/test.js", - "coverage": "cross-env BABEL_ENV=test nyc --reporter json --reporter text mocha -R spec test/test.js && codecov -f coverage/coverage-final.json" + "test": "cross-env BABEL_ENV=test mocha --compilers js:babel-register test/test.js", + "report": "cross-env BABEL_ENV=coverage nyc --reporter lcov --reporter text mocha -R spec test/test.js", + "coverage": "cross-env BABEL_ENV=coverage nyc --reporter json --reporter text mocha -R spec test/test.js && codecov -f coverage/coverage-final.json" }, "repository": { "type": "git", @@ -26,7 +31,6 @@ }, "homepage": "https://github.com/bitinn/node-fetch", "devDependencies": { - "babel-cli": "^6.16.0", "babel-plugin-istanbul": "^2.0.1", "babel-plugin-transform-runtime": "^6.15.0", "babel-preset-es2015": "^6.16.0", @@ -43,6 +47,9 @@ "parted": "^0.1.1", "promise": "^7.1.1", "resumer": "0.0.0", + "rollup": "^0.36.4", + "rollup-plugin-babel": "^2.6.1", + "rollup-plugin-node-resolve": "^2.0.0", "whatwg-url": "^3.0.0" }, "dependencies": { @@ -50,35 +57,5 @@ "buffer-to-arraybuffer": "0.0.4", "encoding": "^0.1.11", "is-stream": "^1.0.1" - }, - "babel": { - "presets": [ - [ - "es2015", - { - "loose": true - } - ] - ], - "plugins": [ - "transform-runtime" - ], - "env": { - "test": { - "plugins": [ - "istanbul" - ] - } - } - }, - "nyc": { - "include": [ - "src/*.js" - ], - "require": [ - "babel-register" - ], - "sourceMap": false, - "instrument": false } } diff --git a/rollup.config.js b/rollup.config.js new file mode 100644 index 0000000..4a57423 --- /dev/null +++ b/rollup.config.js @@ -0,0 +1,17 @@ +import babel from 'rollup-plugin-babel'; +import resolve from 'rollup-plugin-node-resolve'; + +process.env.BABEL_ENV = 'rollup'; + +export default { + entry: 'src/index.js', + plugins: [ + babel({ + runtimeHelpers: true + }) + ], + targets: [ + { dest: 'lib/index.js', format: 'cjs' }, + { dest: 'lib/index.es.js', format: 'es' } + ] +}; diff --git a/src/common.js b/src/common.js index f4908a3..a53e051 100644 --- a/src/common.js +++ b/src/common.js @@ -47,7 +47,7 @@ function isValidTokenChar(ch) { return false; } /* istanbul ignore next */ -function checkIsHttpToken(val) { +export function checkIsHttpToken(val) { if (typeof val !== 'string' || val.length === 0) return false; if (!isValidTokenChar(val.charCodeAt(0))) @@ -71,7 +71,6 @@ function checkIsHttpToken(val) { } return true; } -exports._checkIsHttpToken = checkIsHttpToken; /** * True if val contains an invalid field-vchar @@ -84,7 +83,7 @@ exports._checkIsHttpToken = checkIsHttpToken; * code size does not exceed v8's default max_inlined_source_size setting. **/ /* istanbul ignore next */ -function checkInvalidHeaderChar(val) { +export function checkInvalidHeaderChar(val) { val += ''; if (val.length < 1) return false; @@ -108,4 +107,3 @@ function checkInvalidHeaderChar(val) { } return false; } -exports._checkInvalidHeaderChar = checkInvalidHeaderChar; diff --git a/src/headers.js b/src/headers.js index ce320b5..07f1ff3 100644 --- a/src/headers.js +++ b/src/headers.js @@ -6,11 +6,11 @@ */ import getIterator from 'babel-runtime/core-js/get-iterator'; -import { _checkIsHttpToken, _checkInvalidHeaderChar } from './common.js'; +import { checkIsHttpToken, checkInvalidHeaderChar } from './common.js'; function sanitizeName(name) { name += ''; - if (!_checkIsHttpToken(name)) { + if (!checkIsHttpToken(name)) { throw new TypeError(`${name} is not a legal HTTP header name`); } return name.toLowerCase(); @@ -18,7 +18,7 @@ function sanitizeName(name) { function sanitizeValue(value) { value += ''; - if (_checkInvalidHeaderChar(value)) { + if (checkInvalidHeaderChar(value)) { throw new TypeError(`${value} is not a legal HTTP header value`); } return value; From a355664e642cade7d92db56a1fcc28639a11229a Mon Sep 17 00:00:00 2001 From: Timothy Gu Date: Wed, 23 Nov 2016 11:30:01 -0800 Subject: [PATCH 32/61] Update packages --- .nycrc | 3 --- package.json | 12 ++++++------ test/test.js | 51 +++++++++++++++++++++++---------------------------- 3 files changed, 29 insertions(+), 37 deletions(-) diff --git a/.nycrc b/.nycrc index ad1e790..d8d9c14 100644 --- a/.nycrc +++ b/.nycrc @@ -1,7 +1,4 @@ { - "include": [ - "src/*.js" - ], "require": [ "babel-register" ], diff --git a/package.json b/package.json index 2b514ac..3c693ec 100644 --- a/package.json +++ b/package.json @@ -31,26 +31,26 @@ }, "homepage": "https://github.com/bitinn/node-fetch", "devDependencies": { - "babel-plugin-istanbul": "^2.0.1", + "babel-plugin-istanbul": "^3.0.0", "babel-plugin-transform-runtime": "^6.15.0", "babel-preset-es2015": "^6.16.0", "babel-register": "^6.16.3", "bluebird": "^3.3.4", "chai": "^3.5.0", - "chai-as-promised": "^5.2.0", + "chai-as-promised": "^6.0.0", "chai-iterator": "^1.1.1", "codecov": "^1.0.1", - "cross-env": "2.0.1", + "cross-env": "^3.1.3", "form-data": ">=1.0.0", - "mocha": "^2.1.0", - "nyc": "^8.3.0", + "mocha": "^3.1.2", + "nyc": "^10.0.0", "parted": "^0.1.1", "promise": "^7.1.1", "resumer": "0.0.0", "rollup": "^0.36.4", "rollup-plugin-babel": "^2.6.1", "rollup-plugin-node-resolve": "^2.0.0", - "whatwg-url": "^3.0.0" + "whatwg-url": "^4.0.0" }, "dependencies": { "babel-runtime": "^6.11.6", diff --git a/test/test.js b/test/test.js index 5dd5d5b..6ad17df 100644 --- a/test/test.js +++ b/test/test.js @@ -1039,53 +1039,36 @@ describe(`node-fetch with FOLLOW_SPEC = ${defaultFollowSpec}`, () => { }); }); - it('should allow piping response body as stream', function(done) { + it('should allow piping response body as stream', function() { url = `${base}hello`; - fetch(url).then(res => { + return fetch(url).then(res => { expect(res.body).to.be.an.instanceof(stream.Transform); - res.body.on('data', chunk => { + return streamToPromise(res.body, chunk => { if (chunk === null) { return; } expect(chunk.toString()).to.equal('world'); }); - res.body.on('end', () => { - done(); - }); }); }); - it('should allow cloning a response, and use both as stream', function(done) { + it('should allow cloning a response, and use both as stream', function() { url = `${base}hello`; return fetch(url).then(res => { - let counter = 0; const r1 = res.clone(); expect(res.body).to.be.an.instanceof(stream.Transform); expect(r1.body).to.be.an.instanceof(stream.Transform); - res.body.on('data', chunk => { + const dataHandler = chunk => { if (chunk === null) { return; } expect(chunk.toString()).to.equal('world'); - }); - res.body.on('end', () => { - counter++; - if (counter == 2) { - done(); - } - }); - r1.body.on('data', chunk => { - if (chunk === null) { - return; - } - expect(chunk.toString()).to.equal('world'); - }); - r1.body.on('end', () => { - counter++; - if (counter == 2) { - done(); - } - }); + }; + + return Promise.all([ + streamToPromise(res.body, dataHandler), + streamToPromise(r1.body, dataHandler) + ]); }); }); @@ -1698,3 +1681,15 @@ describe(`node-fetch with FOLLOW_SPEC = ${defaultFollowSpec}`, () => { }); }); + +function streamToPromise(stream, dataHandler) { + return new Promise((resolve, reject) => { + stream.on('data', (...args) => { + Promise.resolve() + .then(() => dataHandler(...args)) + .catch(reject); + }); + stream.on('end', resolve); + stream.on('error', reject); + }); +} From 908bcaac4cacf5b027e3a82e883cabe09f22a0ed Mon Sep 17 00:00:00 2001 From: Timothy Gu Date: Wed, 23 Nov 2016 11:32:43 -0800 Subject: [PATCH 33/61] Work around istanbul's bug See istanbuljs/istanbul-lib-instrument#30. --- src/common.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/common.js b/src/common.js index a53e051..b0f9c99 100644 --- a/src/common.js +++ b/src/common.js @@ -47,7 +47,7 @@ function isValidTokenChar(ch) { return false; } /* istanbul ignore next */ -export function checkIsHttpToken(val) { +function checkIsHttpToken(val) { if (typeof val !== 'string' || val.length === 0) return false; if (!isValidTokenChar(val.charCodeAt(0))) @@ -71,6 +71,7 @@ export function checkIsHttpToken(val) { } return true; } +export { checkIsHttpToken }; /** * True if val contains an invalid field-vchar @@ -83,7 +84,7 @@ export function checkIsHttpToken(val) { * code size does not exceed v8's default max_inlined_source_size setting. **/ /* istanbul ignore next */ -export function checkInvalidHeaderChar(val) { +function checkInvalidHeaderChar(val) { val += ''; if (val.length < 1) return false; @@ -107,3 +108,4 @@ export function checkInvalidHeaderChar(val) { } return false; } +export { checkInvalidHeaderChar }; From 049585be113af88c2315db186717929903954205 Mon Sep 17 00:00:00 2001 From: Timothy Gu Date: Wed, 23 Nov 2016 11:43:51 -0800 Subject: [PATCH 34/61] Revert to cross-env 2.0.1 The Object.assign used in that module is not polyfilled. --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 3c693ec..d1ff669 100644 --- a/package.json +++ b/package.json @@ -40,7 +40,7 @@ "chai-as-promised": "^6.0.0", "chai-iterator": "^1.1.1", "codecov": "^1.0.1", - "cross-env": "^3.1.3", + "cross-env": "2.0.1", "form-data": ">=1.0.0", "mocha": "^3.1.2", "nyc": "^10.0.0", From 25d139a4d0a4ea1d703abbacec8fb31589a9086e Mon Sep 17 00:00:00 2001 From: Timothy Gu Date: Wed, 23 Nov 2016 11:50:05 -0800 Subject: [PATCH 35/61] Officially drop support for 0.10 Current LTS releases are added. --- .travis.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index b6c2082..23a2e4a 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,7 +1,8 @@ language: node_js node_js: - - "0.10" - "0.12" + - "4" + - "6" - "node" env: - FORMDATA_VERSION=1.0.0 From 4ae42ea5eec9260c9372b30f47e052dee9569914 Mon Sep 17 00:00:00 2001 From: Timothy Gu Date: Wed, 23 Nov 2016 14:36:08 -0800 Subject: [PATCH 36/61] Fix class names for prototypes Per Web IDL specification. --- src/headers.js | 21 ++++++++++++++------- src/request.js | 21 ++++++++++++++------- src/response.js | 21 ++++++++++++++------- 3 files changed, 42 insertions(+), 21 deletions(-) diff --git a/src/headers.js b/src/headers.js index 07f1ff3..703ed67 100644 --- a/src/headers.js +++ b/src/headers.js @@ -61,6 +61,13 @@ export default class Headers { this.append(prop, headers[prop]); } } + + Object.defineProperty(this, Symbol.toStringTag, { + value: 'Headers', + writable: false, + enumerable: false, + configurable: true + }); } /** @@ -222,13 +229,13 @@ export default class Headers { [Symbol.iterator]() { return this.entries(); } - - /** - * Tag used by `Object.prototype.toString()`. - */ - get [Symbol.toStringTag]() { - return 'Headers'; - } } +Object.defineProperty(Headers.prototype, Symbol.toStringTag, { + value: 'HeadersPrototype', + writable: false, + enumerable: false, + configurable: true +}); + Headers.FOLLOW_SPEC = false; diff --git a/src/request.js b/src/request.js index 0ba13e7..136570a 100644 --- a/src/request.js +++ b/src/request.js @@ -58,6 +58,13 @@ export default class Request extends Body { // server request options Object.assign(this, parsedURL); + + Object.defineProperty(this, Symbol.toStringTag, { + value: 'Request', + writable: false, + enumerable: false, + configurable: true + }); } get url() { @@ -72,11 +79,11 @@ export default class Request extends Body { clone() { return new Request(this); } - - /** - * Tag used by `Object.prototype.toString()`. - */ - get [Symbol.toStringTag]() { - return 'Request'; - } } + +Object.defineProperty(Request.prototype, Symbol.toStringTag, { + value: 'RequestPrototype', + writable: false, + enumerable: false, + configurable: true +}); diff --git a/src/response.js b/src/response.js index bc3175b..85b1820 100644 --- a/src/response.js +++ b/src/response.js @@ -24,6 +24,13 @@ export default class Response extends Body { this.status = opts.status || 200; this.statusText = opts.statusText || STATUS_CODES[this.status]; this.headers = new Headers(opts.headers); + + Object.defineProperty(this, Symbol.toStringTag, { + value: 'Response', + writable: false, + enumerable: false, + configurable: true + }); } /** @@ -49,11 +56,11 @@ export default class Response extends Body { }); } - - /** - * Tag used by `Object.prototype.toString()`. - */ - get [Symbol.toStringTag]() { - return 'Response'; - } } + +Object.defineProperty(Response.prototype, Symbol.toStringTag, { + value: 'ResponsePrototype', + writable: false, + enumerable: false, + configurable: true +}); From 25ff99677dbf61ece16a7b8f39776cd2f30c6ff5 Mon Sep 17 00:00:00 2001 From: Timothy Gu Date: Wed, 23 Nov 2016 15:06:30 -0800 Subject: [PATCH 37/61] Improve Headers' iteration support Class strings, spec-compliant forEach, etc. --- src/headers.js | 136 ++++++++++++++++++++++++++++++++++--------------- test/test.js | 18 ++++--- 2 files changed, 107 insertions(+), 47 deletions(-) diff --git a/src/headers.js b/src/headers.js index 703ed67..ce4c32a 100644 --- a/src/headers.js +++ b/src/headers.js @@ -5,7 +5,6 @@ * Headers class offers convenient helpers */ -import getIterator from 'babel-runtime/core-js/get-iterator'; import { checkIsHttpToken, checkInvalidHeaderChar } from './common.js'; function sanitizeName(name) { @@ -106,11 +105,14 @@ export default class Headers { * @param Boolean thisArg `this` context for callback function * @return Void */ - forEach(callback, thisArg) { - for (let name in this[MAP]) { - this[MAP][name].forEach(value => { - callback.call(thisArg, value, name, this); - }); + forEach(callback, thisArg = undefined) { + let pairs = getHeaderPairs(this); + let i = 0; + while (i < pairs.length) { + const [name, value] = pairs[i]; + callback.call(thisArg, value, name, this); + pairs = getHeaderPairs(this); + i++; } } @@ -176,13 +178,7 @@ export default class Headers { * @return Iterator */ keys() { - let keys = []; - if (this[FOLLOW_SPEC]) { - keys = Object.keys(this[MAP]).sort(); - } else { - this.forEach((_, name) => keys.push(name)); - }; - return getIterator(keys); + return createHeadersIterator(this, 'key'); } /** @@ -190,33 +186,8 @@ export default class Headers { * * @return Iterator */ - *values() { - if (this[FOLLOW_SPEC]) { - for (const name of this.keys()) { - yield this.get(name); - } - } else { - const values = []; - this.forEach(value => values.push(value)); - yield* getIterator(values); - } - } - - /** - * Get an iterator on entries. - * - * @return Iterator - */ - *entries() { - if (this[FOLLOW_SPEC]) { - for (const name of this.keys()) { - yield [name, this.get(name)]; - } - } else { - const entries = []; - this.forEach((value, name) => entries.push([name, value])); - yield* getIterator(entries); - } + values() { + return createHeadersIterator(this, 'value'); } /** @@ -227,9 +198,10 @@ export default class Headers { * @return Iterator */ [Symbol.iterator]() { - return this.entries(); + return createHeadersIterator(this, 'key+value'); } } +Headers.prototype.entries = Headers.prototype[Symbol.iterator]; Object.defineProperty(Headers.prototype, Symbol.toStringTag, { value: 'HeadersPrototype', @@ -238,4 +210,86 @@ Object.defineProperty(Headers.prototype, Symbol.toStringTag, { configurable: true }); +function getHeaderPairs(headers, kind) { + if (headers[FOLLOW_SPEC]) { + const keys = Object.keys(headers[MAP]).sort(); + return keys.map( + kind === 'key' ? + k => [k] : + k => [k, headers.get(k)] + ); + } + + const values = []; + + for (let name in headers[MAP]) { + for (let value of headers[MAP][name]) { + values.push([name, value]); + } + } + + return values; +} + +const INTERNAL = Symbol('internal'); + +function createHeadersIterator(target, kind) { + const iterator = Object.create(HeadersIteratorPrototype); + iterator[INTERNAL] = { + target, + kind, + index: 0 + }; + return iterator; +} + +const HeadersIteratorPrototype = Object.setPrototypeOf({ + next() { + if (!this || + Object.getPrototypeOf(this) !== HeadersIteratorPrototype) { + throw new TypeError('Value of `this` is not a HeadersIterator'); + } + + const { + target, + kind, + index + } = this[INTERNAL]; + const values = getHeaderPairs(target, kind); + const len = values.length; + if (index >= len) { + return { + value: undefined, + done: true + }; + } + + const pair = values[index]; + this[INTERNAL].index = index + 1; + + let result; + if (kind === 'key') { + result = pair[0]; + } else if (kind === 'value') { + result = pair[1]; + } else { + result = pair; + } + + return { + value: result, + done: false + }; + } +}, Object.getPrototypeOf( + Object.getPrototypeOf([][Symbol.iterator]()) +)); + +Object.defineProperty(HeadersIteratorPrototype, Symbol.toStringTag, { + value: 'HeadersIterator', + writable: false, + enumerable: false, + configurable: true +}); + Headers.FOLLOW_SPEC = false; diff --git a/test/test.js b/test/test.js index 6ad17df..114869c 100644 --- a/test/test.js +++ b/test/test.js @@ -1132,11 +1132,12 @@ describe(`node-fetch with FOLLOW_SPEC = ${defaultFollowSpec}`, () => { }); it('should allow iterating through all headers with forEach', function() { - const headers = new Headers({ - a: 1 - , b: [2, 3] - , c: [4] - }); + const headers = new Headers([ + ['b', '2'], + ['c', '4'], + ['b', '3'], + ['a', '1'] + ]); expect(headers).to.have.property('forEach'); const result = []; @@ -1144,10 +1145,15 @@ describe(`node-fetch with FOLLOW_SPEC = ${defaultFollowSpec}`, () => { result.push([key, val]); }); - const expected = [ + const expected = Headers.FOLLOW_SPEC ? [ ["a", "1"] , ["b", "2,3"] , ["c", "4"] + ] : [ + ["b", "2"] + , ["b", "3"] + , ["c", "4"] + , ["a", "1"] ]; expect(result).to.deep.equal(expected); }); From 7f0e50260ea0f9a7662a33ea1bd209f6cb4c5407 Mon Sep 17 00:00:00 2001 From: Timothy Gu Date: Wed, 23 Nov 2016 15:24:51 -0800 Subject: [PATCH 38/61] Add a polyfill for Node.js v0.12's broken %IteratorPrototype% --- src/headers.js | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/headers.js b/src/headers.js index ce4c32a..1edf190 100644 --- a/src/headers.js +++ b/src/headers.js @@ -285,6 +285,13 @@ const HeadersIteratorPrototype = Object.setPrototypeOf({ Object.getPrototypeOf([][Symbol.iterator]()) )); +// On Node.js v0.12 the %IteratorPrototype% object is broken +if (typeof HeadersIteratorPrototype[Symbol.iterator] !== 'function') { + HeadersIteratorPrototype[Symbol.iterator] = function () { + return this; + }; +} + Object.defineProperty(HeadersIteratorPrototype, Symbol.toStringTag, { value: 'HeadersIterator', writable: false, From d3071fa46a27abc744b803db890d9323636eabec Mon Sep 17 00:00:00 2001 From: Timothy Gu Date: Sat, 26 Nov 2016 09:07:12 -0800 Subject: [PATCH 39/61] Revert "Return empty .json() object on 204. Fix #165. (#166)" (#201) This reverts commit 95b58936b8673f7fdc21050c3bfeb460e9b00bf1. Fixes #165. --- src/body.js | 5 ----- test/test.js | 10 ---------- 2 files changed, 15 deletions(-) diff --git a/src/body.js b/src/body.js index dee16de..cf69ec3 100644 --- a/src/body.js +++ b/src/body.js @@ -51,11 +51,6 @@ export default class Body { * @return Promise */ json() { - // for 204 No Content response, buffer will be empty, parsing it will throw error - if (this.status === 204) { - return Body.Promise.resolve({}); - } - return this[CONSUME_BODY]().then(buffer => JSON.parse(buffer.toString())); } diff --git a/test/test.js b/test/test.js index 114869c..27d74d2 100644 --- a/test/test.js +++ b/test/test.js @@ -471,16 +471,6 @@ describe(`node-fetch with FOLLOW_SPEC = ${defaultFollowSpec}`, () => { }); }); - it('should return empty object on no-content response', function() { - url = `${base}no-content`; - return fetch(url).then(res => { - return res.json().then(result => { - expect(result).to.be.an('object'); - expect(result).to.be.empty; - }); - }); - }); - it('should handle no content response with gzip encoding', function() { url = `${base}no-content/gzip`; return fetch(url).then(res => { From 0285828fb8ca41a04ff7eb613d49fbaafd74f781 Mon Sep 17 00:00:00 2001 From: Timothy Gu Date: Wed, 23 Nov 2016 12:42:24 -0800 Subject: [PATCH 40/61] Split Content-Type extraction to Request and Body It is done in this way in the spec. --- src/body.js | 20 ++++++++++++++++++++ src/index.js | 5 ----- src/request.js | 9 ++++++++- 3 files changed, 28 insertions(+), 6 deletions(-) diff --git a/src/body.js b/src/body.js index cf69ec3..a198c6c 100644 --- a/src/body.js +++ b/src/body.js @@ -249,5 +249,25 @@ export function clone(instance) { return body; } +/** + * Performs the operation "extract a `Content-Type` value from |object|" as + * specified in the specification: + * https://fetch.spec.whatwg.org/#concept-bodyinit-extract + * + * This function assumes that instance.body is present and non-null. + * + * @param Mixed instance Response or Request instance + */ +export function extractContentType(instance) { + // detect form data input from form-data module + if (typeof instance.body.getBoundary === 'function') { + return `multipart/form-data;boundary=${instance.body.getBoundary()}`; + } + + if (typeof instance.body === 'string') { + return 'text/plain;charset=UTF-8'; + } +} + // expose Promise Body.Promise = global.Promise; diff --git a/src/index.js b/src/index.js index 6a889fd..fc47993 100644 --- a/src/index.js +++ b/src/index.js @@ -68,11 +68,6 @@ function fetch(url, opts) { headers.set('accept', '*/*'); } - // detect form data input from form-data module, this hack avoid the need to pass multipart header manually - if (!headers.has('content-type') && options.body && typeof options.body.getBoundary === 'function') { - headers.set('content-type', `multipart/form-data; boundary=${options.body.getBoundary()}`); - } - // bring node-fetch closer to browser behavior by setting content-length automatically if (!headers.has('content-length') && /post|put|patch|delete/i.test(options.method)) { if (typeof options.body === 'string') { diff --git a/src/request.js b/src/request.js index 136570a..0223a45 100644 --- a/src/request.js +++ b/src/request.js @@ -7,7 +7,7 @@ import { format as format_url, parse as parse_url } from 'url'; import Headers from './headers.js'; -import Body, { clone } from './body'; +import Body, { clone, extractContentType } from './body'; /** * Request class @@ -46,6 +46,13 @@ export default class Request extends Body { this.redirect = init.redirect || input.redirect || 'follow'; this.headers = new Headers(init.headers || input.headers || {}); + if (init.body) { + const contentType = extractContentType(this); + if (contentType && !this.headers.has('Content-Type')) { + this.headers.append('Content-Type', contentType); + } + } + // server only options this.follow = init.follow !== undefined ? init.follow : input.follow !== undefined ? From 70f61e0c7dfb2d7fcfdc741a7d02403e9ddcf86e Mon Sep 17 00:00:00 2001 From: Timothy Gu Date: Wed, 23 Nov 2016 13:39:35 -0800 Subject: [PATCH 41/61] Split http.request options generation --- src/body.js | 17 ++++++++ src/index.js | 106 ++++++++++++++++--------------------------------- src/request.js | 46 +++++++++++++++++++-- 3 files changed, 93 insertions(+), 76 deletions(-) diff --git a/src/body.js b/src/body.js index a198c6c..9f3e1f1 100644 --- a/src/body.js +++ b/src/body.js @@ -269,5 +269,22 @@ export function extractContentType(instance) { } } +export function getTotalBytes(instance) { + const {body} = instance; + + if (typeof body === 'string') { + return Buffer.byteLength(body); + } else if (body && typeof body.getLengthSync === 'function') { + // detect form data input from form-data module + if (body._lengthRetrievers && body._lengthRetrievers.length == 0 || // 1.x + body.hasKnownLength && body.hasKnownLength()) { // 2.x + return body.getLengthSync(); + } + } else if (body === undefined || body === null) { + // this is only necessary for older nodejs releases (before iojs merge) + return 0; + } +} + // expose Promise Body.Promise = global.Promise; diff --git a/src/index.js b/src/index.js index fc47993..cbb966a 100644 --- a/src/index.js +++ b/src/index.js @@ -14,7 +14,7 @@ import {PassThrough} from 'stream'; import Body from './body'; import Response from './response'; import Headers from './headers'; -import Request from './request'; +import Request, { getNodeRequestOptions } from './request'; import FetchError from './fetch-error'; /** @@ -37,7 +37,9 @@ function fetch(url, opts) { // wrap http.request into fetch return new fetch.Promise((resolve, reject) => { // build request object - const options = new Request(url, opts); + const request = new Request(url, opts); + + const options = getNodeRequestOptions(request); if (!options.protocol || !options.hostname) { throw new Error('only absolute urls are supported'); @@ -49,46 +51,6 @@ function fetch(url, opts) { const send = (options.protocol === 'https:' ? https : http).request; - // normalize headers - const headers = new Headers(options.headers); - - if (options.compress) { - headers.set('accept-encoding', 'gzip,deflate'); - } - - if (!headers.has('user-agent')) { - headers.set('user-agent', 'node-fetch/1.0 (+https://github.com/bitinn/node-fetch)'); - } - - if (!headers.has('connection') && !options.agent) { - headers.set('connection', 'close'); - } - - if (!headers.has('accept')) { - headers.set('accept', '*/*'); - } - - // bring node-fetch closer to browser behavior by setting content-length automatically - if (!headers.has('content-length') && /post|put|patch|delete/i.test(options.method)) { - if (typeof options.body === 'string') { - headers.set('content-length', Buffer.byteLength(options.body)); - // detect form data input from form-data module, this hack avoid the need to add content-length header manually - } else if (options.body && typeof options.body.getLengthSync === 'function') { - // for form-data 1.x - if (options.body._lengthRetrievers && options.body._lengthRetrievers.length == 0) { - headers.set('content-length', options.body.getLengthSync().toString()); - // for form-data 2.x - } else if (options.body.hasKnownLength && options.body.hasKnownLength()) { - headers.set('content-length', options.body.getLengthSync().toString()); - } - // this is only necessary for older nodejs releases (before iojs merge) - } else if (options.body === undefined || options.body === null) { - headers.set('content-length', '0'); - } - } - - options.headers = headers.raw(); - // http.request only support string as host header, this hack make custom host header possible if (options.headers.host) { options.headers.host = options.headers.host[0]; @@ -98,52 +60,52 @@ function fetch(url, opts) { const req = send(options); let reqTimeout; - if (options.timeout) { + if (request.timeout) { req.once('socket', socket => { reqTimeout = setTimeout(() => { req.abort(); - reject(new FetchError(`network timeout at: ${options.url}`, 'request-timeout')); - }, options.timeout); + reject(new FetchError(`network timeout at: ${request.url}`, 'request-timeout')); + }, request.timeout); }); } req.on('error', err => { clearTimeout(reqTimeout); - reject(new FetchError(`request to ${options.url} failed, reason: ${err.message}`, 'system', err)); + reject(new FetchError(`request to ${request.url} failed, reason: ${err.message}`, 'system', err)); }); req.on('response', res => { clearTimeout(reqTimeout); // handle redirect - if (fetch.isRedirect(res.statusCode) && options.redirect !== 'manual') { - if (options.redirect === 'error') { - reject(new FetchError(`redirect mode is set to error: ${options.url}`, 'no-redirect')); + if (fetch.isRedirect(res.statusCode) && request.redirect !== 'manual') { + if (request.redirect === 'error') { + reject(new FetchError(`redirect mode is set to error: ${request.url}`, 'no-redirect')); return; } - if (options.counter >= options.follow) { - reject(new FetchError(`maximum redirect reached at: ${options.url}`, 'max-redirect')); + if (request.counter >= request.follow) { + reject(new FetchError(`maximum redirect reached at: ${request.url}`, 'max-redirect')); return; } if (!res.headers.location) { - reject(new FetchError(`redirect location header missing at: ${options.url}`, 'invalid-redirect')); + reject(new FetchError(`redirect location header missing at: ${request.url}`, 'invalid-redirect')); return; } // per fetch spec, for POST request with 301/302 response, or any request with 303 response, use GET when following redirect if (res.statusCode === 303 - || ((res.statusCode === 301 || res.statusCode === 302) && options.method === 'POST')) + || ((res.statusCode === 301 || res.statusCode === 302) && request.method === 'POST')) { - options.method = 'GET'; - delete options.body; - delete options.headers['content-length']; + request.method = 'GET'; + request.body = null; + request.headers.delete('content-length'); } - options.counter++; + request.counter++; - resolve(fetch(resolve_url(options.url, res.headers.location), options)); + resolve(fetch(resolve_url(request.url, res.headers.location), request)); return; } @@ -158,19 +120,19 @@ function fetch(url, opts) { headers.append(name, res.headers[name]); } } - if (options.redirect === 'manual' && headers.has('location')) { - headers.set('location', resolve_url(options.url, headers.get('location'))); + if (request.redirect === 'manual' && headers.has('location')) { + headers.set('location', resolve_url(request.url, headers.get('location'))); } // prepare response let body = res.pipe(new PassThrough()); const response_options = { - url: options.url + url: request.url , status: res.statusCode , statusText: res.statusMessage , headers: headers - , size: options.size - , timeout: options.timeout + , size: request.size + , timeout: request.timeout }; // response object @@ -182,7 +144,7 @@ function fetch(url, opts) { // 3. no content-encoding header // 4. no content response (204) // 5. content not modified response (304) - if (!options.compress || options.method === 'HEAD' || !headers.has('content-encoding') || res.statusCode === 204 || res.statusCode === 304) { + if (!request.compress || request.method === 'HEAD' || !headers.has('content-encoding') || res.statusCode === 204 || res.statusCode === 304) { output = new Response(body, response_options); resolve(output); return; @@ -224,16 +186,16 @@ function fetch(url, opts) { // accept string, buffer or readable stream as body // per spec we will call tostring on non-stream objects - if (typeof options.body === 'string') { - req.write(options.body); + if (typeof request.body === 'string') { + req.write(request.body); req.end(); - } else if (options.body instanceof Buffer) { - req.write(options.body); + } else if (request.body instanceof Buffer) { + req.write(request.body); req.end() - } else if (typeof options.body === 'object' && options.body.pipe) { - options.body.pipe(req); - } else if (typeof options.body === 'object') { - req.write(options.body.toString()); + } else if (typeof request.body === 'object' && request.body.pipe) { + request.body.pipe(req); + } else if (typeof request.body === 'object') { + req.write(request.body.toString()); req.end(); } else { req.end(); diff --git a/src/request.js b/src/request.js index 0223a45..774dca7 100644 --- a/src/request.js +++ b/src/request.js @@ -7,7 +7,9 @@ import { format as format_url, parse as parse_url } from 'url'; import Headers from './headers.js'; -import Body, { clone, extractContentType } from './body'; +import Body, { clone, extractContentType, getTotalBytes } from './body'; + +const PARSED_URL = Symbol('url'); /** * Request class @@ -63,8 +65,7 @@ export default class Request extends Body { this.counter = init.counter || input.counter || 0; this.agent = init.agent || input.agent; - // server request options - Object.assign(this, parsedURL); + this[PARSED_URL] = parsedURL; Object.defineProperty(this, Symbol.toStringTag, { value: 'Request', @@ -75,7 +76,7 @@ export default class Request extends Body { } get url() { - return format_url(this); + return format_url(this[PARSED_URL]); } /** @@ -94,3 +95,40 @@ Object.defineProperty(Request.prototype, Symbol.toStringTag, { enumerable: false, configurable: true }); + +function normalizeHeaders(request) { + const headers = new Headers(request.headers); + + if (request.compress) { + headers.set('accept-encoding', 'gzip,deflate'); + } + + if (!headers.has('user-agent')) { + headers.set('user-agent', 'node-fetch/1.0 (+https://github.com/bitinn/node-fetch)'); + } + + if (!headers.has('connection') && !request.agent) { + headers.set('connection', 'close'); + } + + if (!headers.has('accept')) { + headers.set('accept', '*/*'); + } + + if (!headers.has('content-length') && /post|put|patch|delete/i.test(request.method)) { + const totalBytes = getTotalBytes(request); + if (typeof totalBytes === 'number') { + headers.set('content-length', totalBytes); + } + } + + return headers; +} + +export function getNodeRequestOptions(request) { + return Object.assign({}, request[PARSED_URL], { + method: request.method, + headers: normalizeHeaders(request).raw(), + agent: request.agent + }); +} From 3d676235a8045b406ce6e8af9d2bb600b627ed5b Mon Sep 17 00:00:00 2001 From: Timothy Gu Date: Sun, 4 Dec 2016 13:13:51 -0800 Subject: [PATCH 42/61] Throw when a GET/HEAD Request is created with body As mandated by the spec --- src/request.js | 11 +++++++++-- test/test.js | 15 +++++++++++++++ 2 files changed, 24 insertions(+), 2 deletions(-) diff --git a/src/request.js b/src/request.js index 774dca7..d36edcd 100644 --- a/src/request.js +++ b/src/request.js @@ -38,17 +38,24 @@ export default class Request extends Body { parsedURL = parse_url(input.url); } + let method = init.method || input.method || 'GET'; + + if ((init.body != null || input instanceof Request && input.body != null) && + (method === 'GET' || method === 'HEAD')) { + throw new TypeError('Request with GET/HEAD method cannot have body'); + } + super(init.body || clone(input), { timeout: init.timeout || input.timeout || 0, size: init.size || input.size || 0 }); // fetch spec options - this.method = init.method || input.method || 'GET'; + this.method = method; this.redirect = init.redirect || input.redirect || 'follow'; this.headers = new Headers(init.headers || input.headers || {}); - if (init.body) { + if (init.body != null) { const contentType = extractContentType(this); if (contentType && !this.headers.has('Content-Type')) { this.headers.append('Content-Type', contentType); diff --git a/test/test.js b/test/test.js index 27d74d2..1bfbf0a 100644 --- a/test/test.js +++ b/test/test.js @@ -1444,6 +1444,17 @@ describe(`node-fetch with FOLLOW_SPEC = ${defaultFollowSpec}`, () => { }); }); + it('should throw error with GET/HEAD requests with body', function() { + expect(() => new Request('.', { body: '' })) + .to.throw(TypeError); + expect(() => new Request('.', { body: 'a' })) + .to.throw(TypeError); + expect(() => new Request('.', { body: '', method: 'HEAD' })) + .to.throw(TypeError); + expect(() => new Request('.', { body: 'a', method: 'HEAD' })) + .to.throw(TypeError); + }); + it('should support empty options in Response constructor', function() { let body = resumer().queue('a=1').end(); body = body.pipe(new stream.PassThrough()); @@ -1557,6 +1568,7 @@ describe(`node-fetch with FOLLOW_SPEC = ${defaultFollowSpec}`, () => { it('should support arrayBuffer() method in Request constructor', function() { url = base; var req = new Request(url, { + method: 'POST', body: 'a=1' }); expect(req.url).to.equal(url); @@ -1570,6 +1582,7 @@ describe(`node-fetch with FOLLOW_SPEC = ${defaultFollowSpec}`, () => { it('should support text() method in Request constructor', function() { url = base; const req = new Request(url, { + method: 'POST', body: 'a=1' }); expect(req.url).to.equal(url); @@ -1581,6 +1594,7 @@ describe(`node-fetch with FOLLOW_SPEC = ${defaultFollowSpec}`, () => { it('should support json() method in Request constructor', function() { url = base; const req = new Request(url, { + method: 'POST', body: '{"a":1}' }); expect(req.url).to.equal(url); @@ -1592,6 +1606,7 @@ describe(`node-fetch with FOLLOW_SPEC = ${defaultFollowSpec}`, () => { it('should support buffer() method in Request constructor', function() { url = base; const req = new Request(url, { + method: 'POST', body: 'a=1' }); expect(req.url).to.equal(url); From cc4ace1778a9235de38c3d5666668b79f25d69a9 Mon Sep 17 00:00:00 2001 From: Timothy Gu Date: Sun, 4 Dec 2016 13:16:03 -0800 Subject: [PATCH 43/61] Make body default to null in Request Fixes #208. --- src/index.js | 6 +++--- src/request.js | 10 ++++++++-- test/test.js | 11 ++++++++--- 3 files changed, 19 insertions(+), 8 deletions(-) diff --git a/src/index.js b/src/index.js index cbb966a..9fb2440 100644 --- a/src/index.js +++ b/src/index.js @@ -184,7 +184,7 @@ function fetch(url, opts) { return; }); - // accept string, buffer or readable stream as body + // accept string, buffer, readable stream or null as body // per spec we will call tostring on non-stream objects if (typeof request.body === 'string') { req.write(request.body); @@ -192,9 +192,9 @@ function fetch(url, opts) { } else if (request.body instanceof Buffer) { req.write(request.body); req.end() - } else if (typeof request.body === 'object' && request.body.pipe) { + } else if (request.body && typeof request.body === 'object' && request.body.pipe) { request.body.pipe(req); - } else if (typeof request.body === 'object') { + } else if (request.body && typeof request.body === 'object') { req.write(request.body.toString()); req.end(); } else { diff --git a/src/request.js b/src/request.js index d36edcd..0241c70 100644 --- a/src/request.js +++ b/src/request.js @@ -40,12 +40,18 @@ export default class Request extends Body { let method = init.method || input.method || 'GET'; - if ((init.body != null || input instanceof Request && input.body != null) && + if ((init.body != null || input instanceof Request && input.body !== null) && (method === 'GET' || method === 'HEAD')) { throw new TypeError('Request with GET/HEAD method cannot have body'); } - super(init.body || clone(input), { + let inputBody = init.body != null ? + init.body : + input instanceof Request && input.body !== null ? + clone(input) : + null; + + super(inputBody, { timeout: init.timeout || input.timeout || 0, size: init.size || input.size || 0 }); diff --git a/test/test.js b/test/test.js index 1bfbf0a..892f1b4 100644 --- a/test/test.js +++ b/test/test.js @@ -1544,9 +1544,14 @@ describe(`node-fetch with FOLLOW_SPEC = ${defaultFollowSpec}`, () => { it('should default to null as body', function() { const res = new Response(); expect(res.body).to.equal(null); - return res.text().then(result => { - expect(result).to.equal(''); - }); + const req = new Request('.'); + expect(req.body).to.equal(null); + + const cb = result => expect(result).to.equal(''); + return Promise.all([ + res.text().then(cb), + req.text().then(cb) + ]); }); it('should default to 200 as status code', function() { From e7a13a5314e8bd3000e5fed8e42eebcab2d15512 Mon Sep 17 00:00:00 2001 From: Timothy Gu Date: Sat, 15 Oct 2016 14:21:33 -0700 Subject: [PATCH 44/61] Add support for blobs --- src/blob.js | 104 +++++++++++++++++++++++++++++++++++++++++++++++++++ src/body.js | 22 +++++++++++ test/test.js | 59 ++++++++++++++++++++++++++++- 3 files changed, 184 insertions(+), 1 deletion(-) create mode 100644 src/blob.js diff --git a/src/blob.js b/src/blob.js new file mode 100644 index 0000000..89e6ef2 --- /dev/null +++ b/src/blob.js @@ -0,0 +1,104 @@ +// Based on https://github.com/tmpvar/jsdom/blob/aa85b2abf07766ff7bf5c1f6daafb3726f2f2db5/lib/jsdom/living/blob.js +// (MIT licensed) + +export const BUFFER = Symbol('buffer'); +const TYPE = Symbol('type'); +const CLOSED = Symbol('closed'); + +export default class Blob { + constructor() { + Object.defineProperty(this, Symbol.toStringTag, { + value: 'Blob', + writable: false, + enumerable: false, + configurable: true + }); + + this[CLOSED] = false; + this[TYPE] = ''; + + const blobParts = arguments[0]; + const options = arguments[1]; + + const buffers = []; + + if (blobParts) { + const a = blobParts; + const length = Number(a.length); + for (let i = 0; i < length; i++) { + const element = a[i]; + let buffer; + if (element instanceof Buffer) { + buffer = element; + } else if (ArrayBuffer.isView(element)) { + buffer = new Buffer(new Uint8Array(element.buffer, element.byteOffset, element.byteLength)); + } else if (element instanceof ArrayBuffer) { + buffer = new Buffer(new Uint8Array(element)); + } else if (element instanceof Blob) { + buffer = element[BUFFER]; + } else { + buffer = new Buffer(typeof element === 'string' ? element : String(element)); + } + buffers.push(buffer); + } + } + + this[BUFFER] = Buffer.concat(buffers); + + let type = options && options.type !== undefined && String(options.type).toLowerCase(); + if (type && !/[^\u0020-\u007E]/.test(type)) { + this[TYPE] = type; + } + } + get size() { + return this[CLOSED] ? 0 : this[BUFFER].length; + } + get type() { + return this[TYPE]; + } + get isClosed() { + return this[CLOSED]; + } + slice() { + const size = this.size; + + const start = arguments[0]; + const end = arguments[1]; + let relativeStart, relativeEnd; + if (start === undefined) { + relativeStart = 0; + } else if (start < 0) { + relativeStart = Math.max(size + start, 0); + } else { + relativeStart = Math.min(start, size); + } + if (end === undefined) { + relativeEnd = size; + } else if (end < 0) { + relativeEnd = Math.max(size + end, 0); + } else { + relativeEnd = Math.min(end, size); + } + const span = Math.max(relativeEnd - relativeStart, 0); + + const buffer = this[BUFFER]; + const slicedBuffer = buffer.slice( + relativeStart, + relativeStart + span + ); + const blob = new Blob([], { type: arguments[2] }); + blob[BUFFER] = slicedBuffer; + blob[CLOSED] = this[CLOSED]; + return blob; + } + close() { + this[CLOSED] = true; + } +} + +Object.defineProperty(Blob.prototype, Symbol.toStringTag, { + value: 'BlobPrototype', + writable: false, + enumerable: false, + configurable: true +}); diff --git a/src/body.js b/src/body.js index 9f3e1f1..0ff922e 100644 --- a/src/body.js +++ b/src/body.js @@ -9,6 +9,7 @@ import {convert} from 'encoding'; import bodyStream from 'is-stream'; import toArrayBuffer from 'buffer-to-arraybuffer'; import {PassThrough} from 'stream'; +import Blob, {BUFFER} from './blob.js'; import FetchError from './fetch-error.js'; const DISTURBED = Symbol('disturbed'); @@ -26,6 +27,9 @@ export default class Body { size = 0, timeout = 0 } = {}) { + if (body instanceof Blob) { + body = body[BUFFER]; + } this.body = body; this[DISTURBED] = false; this.size = size; @@ -45,6 +49,24 @@ export default class Body { return this[CONSUME_BODY]().then(buf => toArrayBuffer(buf)); } + /** + * Return raw response as Blob + * + * @return Promise + */ + blob() { + let ct = this.headers && this.headers.get('content-type') || ''; + return this[CONSUME_BODY]().then(buf => Object.assign( + // Prevent copying + new Blob([], { + type: ct.toLowerCase() + }), + { + [BUFFER]: buf + } + )); + } + /** * Decode response as json * diff --git a/test/test.js b/test/test.js index 892f1b4..4204e19 100644 --- a/test/test.js +++ b/test/test.js @@ -26,6 +26,7 @@ import Headers from '../src/headers.js'; import Response from '../src/response.js'; import Request from '../src/request.js'; import Body from '../src/body.js'; +import Blob from '../src/blob.js'; import FetchError from '../src/fetch-error.js'; // test with native promise on node 0.11, and bluebird for node 0.10 fetch.Promise = fetch.Promise || bluebird; @@ -1398,6 +1399,20 @@ describe(`node-fetch with FOLLOW_SPEC = ${defaultFollowSpec}`, () => { }); }); + it('should support blob round-trip', function() { + url = `${base}hello`; + + return fetch(url).then(res => res.blob()).then(blob => { + url = `${base}inspect`; + return fetch(url, { + method: 'POST', + body: blob + }); + }).then(res => res.json()).then(({body}) => { + expect(body).to.equal('world'); + }); + }); + it('should support wrapping Request instance', function() { url = `${base}hello`; @@ -1494,6 +1509,25 @@ describe(`node-fetch with FOLLOW_SPEC = ${defaultFollowSpec}`, () => { }); }); + it('should support blob() method in Request constructor', function() { + const res = new Response('a=1', { + headers: { + 'Content-Type': 'text/plain' + } + }); + return res.blob().then(function(result) { + expect(result).to.be.an.instanceOf(Blob); + expect(result.isClosed).to.be.false; + expect(result.size).to.equal(3); + expect(result.type).to.equal('text/plain'); + + result.close(); + expect(result.isClosed).to.be.true; + expect(result.size).to.equal(0); + expect(result.type).to.equal('text/plain'); + }); + }); + it('should support clone() method in Response constructor', function() { let body = resumer().queue('a=1').end(); body = body.pipe(new stream.PassThrough()); @@ -1620,6 +1654,28 @@ describe(`node-fetch with FOLLOW_SPEC = ${defaultFollowSpec}`, () => { }); }); + it('should support blob() method in Request constructor', function() { + url = base; + var req = new Request(url, { + body: 'a=1', + headers: { + 'Content-Type': 'text/plain' + } + }); + expect(req.url).to.equal(url); + return req.blob().then(function(result) { + expect(result).to.be.an.instanceOf(Blob); + expect(result.isClosed).to.be.false; + expect(result.size).to.equal(3); + expect(result.type).to.equal('text/plain'); + + result.close(); + expect(result.isClosed).to.be.true; + expect(result.size).to.equal(0); + expect(result.type).to.equal('text/plain'); + }); + }); + it('should support arbitrary url in Request constructor', function() { url = 'anything'; const req = new Request(url); @@ -1660,9 +1716,10 @@ describe(`node-fetch with FOLLOW_SPEC = ${defaultFollowSpec}`, () => { }); }); - it('should support arrayBuffer(), text(), json() and buffer() method in Body constructor', function() { + it('should support arrayBuffer(), blob(), text(), json() and buffer() method in Body constructor', function() { const body = new Body('a=1'); expect(body).to.have.property('arrayBuffer'); + expect(body).to.have.property('blob'); expect(body).to.have.property('text'); expect(body).to.have.property('json'); expect(body).to.have.property('buffer'); From 4d944365dfeff3ab0cb8b978c1e6fa9e3b640e2e Mon Sep 17 00:00:00 2001 From: Timothy Gu Date: Mon, 5 Dec 2016 15:21:19 -0800 Subject: [PATCH 45/61] Fix tests added in the last commit --- test/test.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/test/test.js b/test/test.js index 4204e19..2512308 100644 --- a/test/test.js +++ b/test/test.js @@ -1509,8 +1509,9 @@ describe(`node-fetch with FOLLOW_SPEC = ${defaultFollowSpec}`, () => { }); }); - it('should support blob() method in Request constructor', function() { + it('should support blob() method in Response constructor', function() { const res = new Response('a=1', { + method: 'POST', headers: { 'Content-Type': 'text/plain' } @@ -1657,6 +1658,7 @@ describe(`node-fetch with FOLLOW_SPEC = ${defaultFollowSpec}`, () => { it('should support blob() method in Request constructor', function() { url = base; var req = new Request(url, { + method: 'POST', body: 'a=1', headers: { 'Content-Type': 'text/plain' From 552c1a601de6ed62175165d08b65a7d8c8b08438 Mon Sep 17 00:00:00 2001 From: Timothy Gu Date: Mon, 5 Dec 2016 18:46:02 -0800 Subject: [PATCH 46/61] Bring coverage up to 100% --- .babelrc | 7 ++++++- src/headers.js | 1 + 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/.babelrc b/.babelrc index 50318ff..3cd9d25 100644 --- a/.babelrc +++ b/.babelrc @@ -13,7 +13,12 @@ [ "es2015", { "loose": true } ] ], "plugins": [ - "istanbul" + [ "istanbul", { + "exclude": [ + "src/blob.js", + "test" + ] + } ] ] }, "rollup": { diff --git a/src/headers.js b/src/headers.js index 1edf190..eb47f1c 100644 --- a/src/headers.js +++ b/src/headers.js @@ -245,6 +245,7 @@ function createHeadersIterator(target, kind) { const HeadersIteratorPrototype = Object.setPrototypeOf({ next() { + // istanbul ignore if if (!this || Object.getPrototypeOf(this) !== HeadersIteratorPrototype) { throw new TypeError('Value of `this` is not a HeadersIterator'); From 385ca6b2b010f3bcca6557de64536f2718d97030 Mon Sep 17 00:00:00 2001 From: Timothy Gu Date: Mon, 5 Dec 2016 19:35:23 -0800 Subject: [PATCH 47/61] To 100% branches coverage --- package.json | 1 + test/test.js | 59 ++++++++++++++++++++++++++++++++++++++++++++-------- 2 files changed, 51 insertions(+), 9 deletions(-) diff --git a/package.json b/package.json index d1ff669..ecae2d3 100644 --- a/package.json +++ b/package.json @@ -39,6 +39,7 @@ "chai": "^3.5.0", "chai-as-promised": "^6.0.0", "chai-iterator": "^1.1.1", + "chai-string": "^1.3.0", "codecov": "^1.0.1", "cross-env": "2.0.1", "form-data": ">=1.0.0", diff --git a/test/test.js b/test/test.js index 2512308..97ebb70 100644 --- a/test/test.js +++ b/test/test.js @@ -4,6 +4,7 @@ import repeat from 'babel-runtime/core-js/string/repeat'; import chai from 'chai'; import chaiPromised from 'chai-as-promised'; import chaiIterator from 'chai-iterator'; +import chaiString from 'chai-string'; import bluebird from 'bluebird'; import then from 'promise'; import {spawn} from 'child_process'; @@ -16,6 +17,7 @@ import * as fs from 'fs'; chai.use(chaiPromised); chai.use(chaiIterator); +chai.use(chaiString); const expect = chai.expect; import TestServer from './server'; @@ -630,6 +632,44 @@ describe(`node-fetch with FOLLOW_SPEC = ${defaultFollowSpec}`, () => { }); }); + it('should set default User-Agent', function () { + url = `${base}inspect`; + fetch(url).then(res => res.json()).then(res => { + expect(res.headers['user-agent']).to.startWith('node-fetch/'); + }); + }); + + it('should allow setting User-Agent', function () { + url = `${base}inspect`; + opts = { + headers: { + 'user-agent': 'faked' + } + }; + fetch(url, opts).then(res => res.json()).then(res => { + expect(res.headers['user-agent']).to.equal('faked'); + }); + }); + + it('should set default Accept header', function () { + url = `${base}inspect`; + fetch(url).then(res => res.json()).then(res => { + expect(res.headers.accept).to.equal('*/*'); + }); + }); + + it('should allow setting Accept header', function () { + url = `${base}inspect`; + opts = { + headers: { + 'accept': 'application/json' + } + }; + fetch(url, opts).then(res => res.json()).then(res => { + expect(res.headers.accept).to.equal('application/json'); + }); + }); + it('should allow POST request', function() { url = `${base}inspect`; opts = { @@ -640,6 +680,7 @@ describe(`node-fetch with FOLLOW_SPEC = ${defaultFollowSpec}`, () => { }).then(res => { expect(res.method).to.equal('POST'); expect(res.headers['transfer-encoding']).to.be.undefined; + expect(res.headers['content-type']).to.be.undefined; expect(res.headers['content-length']).to.equal('0'); }); }); @@ -656,6 +697,7 @@ describe(`node-fetch with FOLLOW_SPEC = ${defaultFollowSpec}`, () => { expect(res.method).to.equal('POST'); expect(res.body).to.equal('a=1'); expect(res.headers['transfer-encoding']).to.be.undefined; + expect(res.headers['content-type']).to.equal('text/plain;charset=UTF-8'); expect(res.headers['content-length']).to.equal('3'); }); }); @@ -672,6 +714,7 @@ describe(`node-fetch with FOLLOW_SPEC = ${defaultFollowSpec}`, () => { expect(res.method).to.equal('POST'); expect(res.body).to.equal('a=1'); expect(res.headers['transfer-encoding']).to.equal('chunked'); + expect(res.headers['content-type']).to.be.undefined; expect(res.headers['content-length']).to.be.undefined; }); }); @@ -691,6 +734,7 @@ describe(`node-fetch with FOLLOW_SPEC = ${defaultFollowSpec}`, () => { expect(res.method).to.equal('POST'); expect(res.body).to.equal('a=1'); expect(res.headers['transfer-encoding']).to.equal('chunked'); + expect(res.headers['content-type']).to.be.undefined; expect(res.headers['content-length']).to.be.undefined; }); }); @@ -708,7 +752,7 @@ describe(`node-fetch with FOLLOW_SPEC = ${defaultFollowSpec}`, () => { return res.json(); }).then(res => { expect(res.method).to.equal('POST'); - expect(res.headers['content-type']).to.contain('multipart/form-data'); + expect(res.headers['content-type']).to.startWith('multipart/form-data;boundary='); expect(res.headers['content-length']).to.be.a('string'); expect(res.body).to.equal('a=1'); }); @@ -728,7 +772,7 @@ describe(`node-fetch with FOLLOW_SPEC = ${defaultFollowSpec}`, () => { return res.json(); }).then(res => { expect(res.method).to.equal('POST'); - expect(res.headers['content-type']).to.contain('multipart/form-data'); + expect(res.headers['content-type']).to.startWith('multipart/form-data;boundary='); expect(res.headers['content-length']).to.be.undefined; expect(res.body).to.contain('my_field='); }); @@ -751,7 +795,7 @@ describe(`node-fetch with FOLLOW_SPEC = ${defaultFollowSpec}`, () => { return res.json(); }).then(res => { expect(res.method).to.equal('POST'); - expect(res.headers['content-type']).to.contain('multipart/form-data'); + expect(res.headers['content-type']).to.startWith('multipart/form-data; boundary='); expect(res.headers['content-length']).to.be.a('string'); expect(res.headers.b).to.equal('2'); expect(res.body).to.equal('a=1'); @@ -1659,22 +1703,19 @@ describe(`node-fetch with FOLLOW_SPEC = ${defaultFollowSpec}`, () => { url = base; var req = new Request(url, { method: 'POST', - body: 'a=1', - headers: { - 'Content-Type': 'text/plain' - } + body: new Buffer('a=1') }); expect(req.url).to.equal(url); return req.blob().then(function(result) { expect(result).to.be.an.instanceOf(Blob); expect(result.isClosed).to.be.false; expect(result.size).to.equal(3); - expect(result.type).to.equal('text/plain'); + expect(result.type).to.equal(''); result.close(); expect(result.isClosed).to.be.true; expect(result.size).to.equal(0); - expect(result.type).to.equal('text/plain'); + expect(result.type).to.equal(''); }); }); From a604069860d4629ff54cfaebce697e9ea968f34c Mon Sep 17 00:00:00 2001 From: Timothy Gu Date: Mon, 5 Dec 2016 20:25:13 -0800 Subject: [PATCH 48/61] More exact content-type and content-length Set content-type of requests with body being objects to text/plain --- src/body.js | 97 +++++++++++++++++++++++++++++++++++++++++++------- src/index.js | 19 ++-------- src/request.js | 2 +- test/test.js | 10 +++++- 4 files changed, 97 insertions(+), 31 deletions(-) diff --git a/src/body.js b/src/body.js index 0ff922e..890f2fc 100644 --- a/src/body.js +++ b/src/body.js @@ -27,8 +27,21 @@ export default class Body { size = 0, timeout = 0 } = {}) { - if (body instanceof Blob) { - body = body[BUFFER]; + if (body == null) { + // body is undefined or null + body = null; + } else if (typeof body === 'string') { + // body is string + } else if (body instanceof Blob) { + // body is blob + } else if (Buffer.isBuffer(body)) { + // body is buffer + } else if (bodyStream(body)) { + // body is stream + } else { + // none of the above + // coerce to string + body = String(body); } this.body = body; this[DISTURBED] = false; @@ -117,7 +130,7 @@ export default class Body { this[DISTURBED] = true; // body is null - if (!this.body) { + if (this.body === null) { return Body.Promise.resolve(new Buffer(0)); } @@ -126,11 +139,21 @@ export default class Body { return Body.Promise.resolve(new Buffer(this.body)); } + // body is blob + if (this.body instanceof Blob) { + return Body.Promise.resolve(this.body[BUFFER]); + } + // body is buffer if (Buffer.isBuffer(this.body)) { return Body.Promise.resolve(this.body); } + // should never happen + if (!bodyStream(this.body)) { + return Body.Promise.resolve(new Buffer(0)); + } + // body is stream // get ready to actually consume the body let accum = []; @@ -281,30 +304,80 @@ export function clone(instance) { * @param Mixed instance Response or Request instance */ export function extractContentType(instance) { - // detect form data input from form-data module - if (typeof instance.body.getBoundary === 'function') { - return `multipart/form-data;boundary=${instance.body.getBoundary()}`; - } + const {body} = instance; - if (typeof instance.body === 'string') { + if (body === null) { + // body is null + return null; + } else if (typeof body === 'string') { + // body is string return 'text/plain;charset=UTF-8'; + } else if (body instanceof Blob) { + // body is blob + return body.type || null; + } else if (Buffer.isBuffer(body)) { + // body is buffer + return null; + } else if (typeof body.getBoundary === 'function') { + // detect form data input from form-data module + return `multipart/form-data;boundary=${body.getBoundary()}`; + } else { + // body is stream + // can't really do much about this + return null; } } export function getTotalBytes(instance) { const {body} = instance; - if (typeof body === 'string') { + if (body === null) { + // body is null + return 0; + } else if (typeof body === 'string') { + // body is string return Buffer.byteLength(body); + } else if (body instanceof Blob) { + // body is blob + return body.size; } else if (body && typeof body.getLengthSync === 'function') { // detect form data input from form-data module if (body._lengthRetrievers && body._lengthRetrievers.length == 0 || // 1.x body.hasKnownLength && body.hasKnownLength()) { // 2.x return body.getLengthSync(); } - } else if (body === undefined || body === null) { - // this is only necessary for older nodejs releases (before iojs merge) - return 0; + return null; + } else { + // body is stream + // can't really do much about this + return null; + } +} + +export function writeToStream(dest, instance) { + const {body} = instance; + + if (body === null) { + // body is null + dest.end(); + } else if (typeof body === 'string') { + // body is string + dest.write(body); + dest.end(); + } else if (body instanceof Blob) { + // body is blob + dest.write(body[BUFFER]); + dest.end(); + } else if (Buffer.isBuffer(body)) { + // body is buffer + dest.write(body); + dest.end() + } else if (bodyStream(body)) { + // body is stream + body.pipe(dest); + } else { + // should never happen + dest.end(); } } diff --git a/src/index.js b/src/index.js index 9fb2440..e9f2d55 100644 --- a/src/index.js +++ b/src/index.js @@ -11,7 +11,7 @@ import * as https from 'https'; import * as zlib from 'zlib'; import {PassThrough} from 'stream'; -import Body from './body'; +import Body, { writeToStream } from './body'; import Response from './response'; import Headers from './headers'; import Request, { getNodeRequestOptions } from './request'; @@ -184,22 +184,7 @@ function fetch(url, opts) { return; }); - // accept string, buffer, readable stream or null as body - // per spec we will call tostring on non-stream objects - if (typeof request.body === 'string') { - req.write(request.body); - req.end(); - } else if (request.body instanceof Buffer) { - req.write(request.body); - req.end() - } else if (request.body && typeof request.body === 'object' && request.body.pipe) { - request.body.pipe(req); - } else if (request.body && typeof request.body === 'object') { - req.write(request.body.toString()); - req.end(); - } else { - req.end(); - } + writeToStream(req, request); }); }; diff --git a/src/request.js b/src/request.js index 0241c70..83fb336 100644 --- a/src/request.js +++ b/src/request.js @@ -63,7 +63,7 @@ export default class Request extends Body { if (init.body != null) { const contentType = extractContentType(this); - if (contentType && !this.headers.has('Content-Type')) { + if (contentType !== null && !this.headers.has('Content-Type')) { this.headers.append('Content-Type', contentType); } } diff --git a/test/test.js b/test/test.js index 97ebb70..2c06176 100644 --- a/test/test.js +++ b/test/test.js @@ -814,6 +814,8 @@ describe(`node-fetch with FOLLOW_SPEC = ${defaultFollowSpec}`, () => { }).then(res => { expect(res.method).to.equal('POST'); expect(res.body).to.equal('[object Object]'); + expect(res.headers['content-type']).to.equal('text/plain;charset=UTF-8'); + expect(res.headers['content-length']).to.equal('15'); }); }); @@ -1446,14 +1448,20 @@ describe(`node-fetch with FOLLOW_SPEC = ${defaultFollowSpec}`, () => { it('should support blob round-trip', function() { url = `${base}hello`; + let length, type; + return fetch(url).then(res => res.blob()).then(blob => { url = `${base}inspect`; + length = blob.size; + type = blob.type; return fetch(url, { method: 'POST', body: blob }); - }).then(res => res.json()).then(({body}) => { + }).then(res => res.json()).then(({body, headers}) => { expect(body).to.equal('world'); + expect(headers['content-type']).to.equal(type); + expect(headers['content-length']).to.equal(String(length)); }); }); From 90d3bc443669b8a1cfe57e0972992a110c7413de Mon Sep 17 00:00:00 2001 From: Timothy Gu Date: Mon, 5 Dec 2016 20:30:00 -0800 Subject: [PATCH 49/61] Set content-length for buffer bodies --- src/body.js | 3 +++ test/test.js | 4 ++-- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/src/body.js b/src/body.js index 890f2fc..edd15ed 100644 --- a/src/body.js +++ b/src/body.js @@ -340,6 +340,9 @@ export function getTotalBytes(instance) { } else if (body instanceof Blob) { // body is blob return body.size; + } else if (Buffer.isBuffer(body)) { + // body is buffer + return body.length; } else if (body && typeof body.getLengthSync === 'function') { // detect form data input from form-data module if (body._lengthRetrievers && body._lengthRetrievers.length == 0 || // 1.x diff --git a/test/test.js b/test/test.js index 2c06176..11f5eb6 100644 --- a/test/test.js +++ b/test/test.js @@ -713,9 +713,9 @@ describe(`node-fetch with FOLLOW_SPEC = ${defaultFollowSpec}`, () => { }).then(res => { expect(res.method).to.equal('POST'); expect(res.body).to.equal('a=1'); - expect(res.headers['transfer-encoding']).to.equal('chunked'); + expect(res.headers['transfer-encoding']).to.be.undefined; expect(res.headers['content-type']).to.be.undefined; - expect(res.headers['content-length']).to.be.undefined; + expect(res.headers['content-length']).to.equal('3'); }); }); From 4a153ff0042c0e4dcdb7df9398c965ba5695337c Mon Sep 17 00:00:00 2001 From: Timothy Gu Date: Mon, 5 Dec 2016 20:40:23 -0800 Subject: [PATCH 50/61] Update changelog See #209. --- CHANGELOG.md | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 857fc8d..20659ab 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,33 @@ Changelog ========= +# 2.x release + +## v2.0.0-alpha.1 (UNRELEASED) + +- Major: Node.js 0.10.x support is dropped +- Major: rewrite in transpiled ES2015 +- Major: internal methods are no longer exposed +- Major: throw error when a GET/HEAD Request is constructed with a non-null body (per spec) +- Major: `response.text()` no longer attempts to detect encoding, instead always opting for UTF-8 (per spec); use `response.textConverted()` for the old behavior +- Major: make `response.json()` throw error instead of returning an empty object on 204 no-content respose (per spec; reverts behavior set in v1.6.2) +- Major: arrays as parameters to `headers.append` and `headers.set` are joined as a string (per spec) +- Enhance: start testing on Node.js 4, 6, 7 +- Enhance: use Rollup to produce a distributed bundle (less memory overhead and faster startup) +- Enhance: make `toString()` on Headers, Requests, and Responses return correct IDL class strings +- Enhance: add an option to conform to latest spec at the expense of reduced compatibility +- Enhance: set `Content-Length` header for Buffers as well +- Enhance: add `response.arrayBuffer()` (also applies to Requests) +- Enhance: add experimental `response.blob()` (also applies to Requests) +- Enhance: make Headers iterable +- Enhance: make Headers constructor accept an array of tuples +- Enhance: make sure header names and values are valid in HTTP +- Fix: coerce Headers prototype function parameters to strings, where applicable +- Fix: fix Request and Response with `null` body +- Fix: support WHATWG URL objects, created by `whatwg-url` package or `require('url').URL` in Node.js 7+ +- Other: use Codecov for code coverage tracking + + # 1.x release ## v1.6.3 From 0f7e6c15d3ec340b4e769ab48b421fa2b3393e2d Mon Sep 17 00:00:00 2001 From: Timothy Gu Date: Mon, 5 Dec 2016 21:09:54 -0800 Subject: [PATCH 51/61] Back to 100% --- src/body.js | 9 ++++----- test/test.js | 43 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 47 insertions(+), 5 deletions(-) diff --git a/src/body.js b/src/body.js index edd15ed..c017b69 100644 --- a/src/body.js +++ b/src/body.js @@ -149,7 +149,7 @@ export default class Body { return Body.Promise.resolve(this.body); } - // should never happen + // istanbul ignore if: should never happen if (!bodyStream(this.body)) { return Body.Promise.resolve(new Buffer(0)); } @@ -306,6 +306,8 @@ export function clone(instance) { export function extractContentType(instance) { const {body} = instance; + // istanbul ignore if: Currently, because of a guard in Request, body + // can never be null. Included here for completeness. if (body === null) { // body is null return null; @@ -375,12 +377,9 @@ export function writeToStream(dest, instance) { // body is buffer dest.write(body); dest.end() - } else if (bodyStream(body)) { + } else { // body is stream body.pipe(dest); - } else { - // should never happen - dest.end(); } } diff --git a/test/test.js b/test/test.js index 11f5eb6..1c26044 100644 --- a/test/test.js +++ b/test/test.js @@ -719,6 +719,42 @@ describe(`node-fetch with FOLLOW_SPEC = ${defaultFollowSpec}`, () => { }); }); + it('should allow POST request with blob body without type', function() { + url = `${base}inspect`; + opts = { + method: 'POST' + , body: new Blob(['a=1']) + }; + return fetch(url, opts).then(res => { + return res.json(); + }).then(res => { + expect(res.method).to.equal('POST'); + expect(res.body).to.equal('a=1'); + expect(res.headers['transfer-encoding']).to.be.undefined; + expect(res.headers['content-type']).to.be.undefined; + expect(res.headers['content-length']).to.equal('3'); + }); + }); + + it('should allow POST request with blob body with type', function() { + url = `${base}inspect`; + opts = { + method: 'POST', + body: new Blob(['a=1'], { + type: 'text/plain;charset=UTF-8' + }) + }; + return fetch(url, opts).then(res => { + return res.json(); + }).then(res => { + expect(res.method).to.equal('POST'); + expect(res.body).to.equal('a=1'); + expect(res.headers['transfer-encoding']).to.be.undefined; + expect(res.headers['content-type']).to.equal('text/plain;charset=utf-8'); + expect(res.headers['content-length']).to.equal('3'); + }); + }); + it('should allow POST request with readable stream as body', function() { let body = resumer().queue('a=1').end(); body = body.pipe(new stream.PassThrough()); @@ -1628,6 +1664,13 @@ describe(`node-fetch with FOLLOW_SPEC = ${defaultFollowSpec}`, () => { }); }); + it('should support blob as body in Response constructor', function() { + const res = new Response(new Blob(['a=1'])); + return res.text().then(result => { + expect(result).to.equal('a=1'); + }); + }); + it('should default to null as body', function() { const res = new Response(); expect(res.body).to.equal(null); From 030bf279438e203f8ff05cffaf8b8ed7e88dff77 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jimmy=20Karl=20Roland=20W=C3=A4rting?= Date: Fri, 9 Dec 2016 04:18:06 +0100 Subject: [PATCH 52/61] Documentation update for v2 (#214) --- LIMITS.md | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/LIMITS.md b/LIMITS.md index d0d41fc..f49fe28 100644 --- a/LIMITS.md +++ b/LIMITS.md @@ -2,7 +2,7 @@ Known differences ================= -*As of 1.x release* +*As of 2.x release* - Topics such as Cross-Origin, Content Security Policy, Mixed Content, Service Workers are ignored, given our server-side context. @@ -16,12 +16,10 @@ Known differences - Also, you can handle rejected fetch requests through checking `err.type` and `err.code`. -- Only support `res.text()`, `res.json()`, `res.buffer()` at the moment, until there are good use-cases for blob/arrayBuffer. +- Only support `res.text()`, `res.json()`, `res.blob()`, `res.arraybuffer()`, `res.buffer()` - There is currently no built-in caching, as server-side caching varies by use-cases. - Current implementation lacks server-side cookie store, you will need to extract `Set-Cookie` headers manually. - If you are using `res.clone()` and writing an isomorphic app, note that stream on Node.js have a smaller internal buffer size (16Kb, aka `highWaterMark`) from client-side browsers (>1Mb, not consistent across browsers). - -- ES6 features such as `headers.entries()` are missing at the moment, but you can use `headers.raw()` to retrieve the raw headers object. From 9351084a98f15f1f412c2a84d193bf27a2a35244 Mon Sep 17 00:00:00 2001 From: Timothy Gu Date: Mon, 12 Dec 2016 12:54:53 -0800 Subject: [PATCH 53/61] Update README for ES2015 --- README.md | 110 +++++++++++++++++++++++------------------------------- 1 file changed, 46 insertions(+), 64 deletions(-) diff --git a/README.md b/README.md index ecb5b81..02cf186 100644 --- a/README.md +++ b/README.md @@ -41,44 +41,40 @@ See Matt Andrews' [isomorphic-fetch](https://github.com/matthew-andrews/isomorph # Usage ```javascript -var fetch = require('node-fetch'); +import fetch from 'node-fetch'; +// or +// const fetch = require('node-fetch'); -// if you are on node v0.10, set a Promise library first, eg. -// fetch.Promise = require('bluebird'); +// if you are using your own Promise library, set it through fetch.Promise. Eg. + +// import Bluebird from 'bluebird'; +// fetch.Promise = Bluebird; // plain text or html fetch('https://github.com/') - .then(function(res) { - return res.text(); - }).then(function(body) { - console.log(body); - }); + .then(res => res.text()) + .then(body => console.log(body)); // json fetch('https://api.github.com/users/github') - .then(function(res) { - return res.json(); - }).then(function(json) { - console.log(json); - }); + .then(res => res.json()) + .then(json => console.log(json)); // catching network error // 3xx-5xx responses are NOT network errors, and should be handled in then() // you only need one catch() at the end of your promise chain fetch('http://domain.invalid/') - .catch(function(err) { - console.log(err); - }); + .catch(err => console.error(err)); // stream // the node.js way is to use stream when possible fetch('https://assets-cdn.github.com/images/modules/logos_page/Octocat.png') - .then(function(res) { - var dest = fs.createWriteStream('./octocat.png'); + .then(res => { + const dest = fs.createWriteStream('./octocat.png'); res.body.pipe(dest); }); @@ -86,18 +82,17 @@ fetch('https://assets-cdn.github.com/images/modules/logos_page/Octocat.png') // if you prefer to cache binary data in full, use buffer() // note that buffer() is a node-fetch only API -var fileType = require('file-type'); +import fileType from 'file-type'; + fetch('https://assets-cdn.github.com/images/modules/logos_page/Octocat.png') - .then(function(res) { - return res.buffer(); - }).then(function(buffer) { - fileType(buffer); - }); + .then(res => res.buffer()) + .then(buffer => fileType(buffer)) + .then(type => { /* ... */ }); // meta fetch('https://github.com/') - .then(function(res) { + .then(res => { console.log(res.ok); console.log(res.status); console.log(res.statusText); @@ -108,22 +103,17 @@ fetch('https://github.com/') // post fetch('http://httpbin.org/post', { method: 'POST', body: 'a=1' }) - .then(function(res) { - return res.json(); - }).then(function(json) { - console.log(json); - }); + .then(res => res.json()) + .then(json => console.log(json)); -// post with stream from resumer +// post with stream from file -var resumer = require('resumer'); -var stream = resumer().queue('a=1').end(); +import { createReadStream } from 'fs'; + +const stream = createReadStream('input.txt'); fetch('http://httpbin.org/post', { method: 'POST', body: stream }) - .then(function(res) { - return res.json(); - }).then(function(json) { - console.log(json); - }); + .then(res => res.json()) + .then(json => console.log(json)); // post with JSON @@ -133,45 +123,37 @@ fetch('http://httpbin.org/post', { body: JSON.stringify(body), headers: { 'Content-Type': 'application/json' }, }) - .then(function(res) { - return res.json(); - }).then(function(json) { - console.log(json); - }); + .then(res => res.json()) + .then(json => console.log(json)); // post with form-data (detect multipart) -var FormData = require('form-data'); -var form = new FormData(); +import FormData from 'form-data'; + +const form = new FormData(); form.append('a', 1); fetch('http://httpbin.org/post', { method: 'POST', body: form }) - .then(function(res) { - return res.json(); - }).then(function(json) { - console.log(json); - }); + .then(res => res.json()) + .then(json => console.log(json)); // post with form-data (custom headers) // note that getHeaders() is non-standard API -var FormData = require('form-data'); -var form = new FormData(); +import FormData from 'form-data'; + +const form = new FormData(); form.append('a', 1); fetch('http://httpbin.org/post', { method: 'POST', body: form, headers: form.getHeaders() }) - .then(function(res) { - return res.json(); - }).then(function(json) { - console.log(json); - }); + .then(res => res.json()) + .then(json => console.log(json)); -// node 0.12+, yield with co +// node 7+ with async function -var co = require('co'); -co(function *() { - var res = yield fetch('https://api.github.com/users/github'); - var json = yield res.json(); - console.log(res); -}); +(async function () { + const res = await fetch('https://api.github.com/users/github'); + const json = await res.json(); + console.log(json); +})(); ``` See [test cases](https://github.com/bitinn/node-fetch/blob/master/test/test.js) for more examples. From 29e9f5eef69caeeff4702056595fc55a61c8301b Mon Sep 17 00:00:00 2001 From: Timothy Gu Date: Mon, 12 Dec 2016 13:50:05 -0800 Subject: [PATCH 54/61] Add guide to upgrade to v2 --- UPGRADE-GUIDE.md | 82 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 82 insertions(+) create mode 100644 UPGRADE-GUIDE.md diff --git a/UPGRADE-GUIDE.md b/UPGRADE-GUIDE.md new file mode 100644 index 0000000..79308f3 --- /dev/null +++ b/UPGRADE-GUIDE.md @@ -0,0 +1,82 @@ +# Upgrade to node-fetch v2 + +node-fetch v2 brings about many changes that increase the compliance of +WHATWG's Fetch Standard. However, many of these changes meant that apps written +for node-fetch v1 needs to be updated to work with node-fetch v2 and be +conformant with the Fetch Standard. + +## `.text()` no longer tries to detect encoding + +Currently, `response.text()` attempts to guess the text encoding of the input +material and decode it for the user. However, it runs counter to the Fetch +Standard which demands `.text()` to always use UTF-8. + +In "response" to that, we have changed `.text()` to use UTF-8. A new function +**`response.textConverted()`** is created that maintains the behavior of +`.text()` last year. + +## Internal methods hidden + +Currently, the user can access internal methods such as `_clone()`, +`_decode()`, and `_convert()` on the `response` object. While these methods +should never have been used, node-fetch v2 makes these functions completely +inaccessible, and may break your app. + +If you have a use case that requires these methods to be available, feel free +to file an issue and we will be happy to help you solve the problem. + +## Headers + +The `Headers` class has gotten a lot of updates to make it spec-compliant. + +```js +////////////////////////////////////////////////////////////////////////////// +// If you are using an object as the initializer, all arrays will be reduced +// to a string. +const headers = new Headers({ + 'Abc': 'string', + 'Multi': [ 'header1', 'header2' ] +}); + +// before after +headers.get('Multi') => headers.get('Multi') => + 'header1'; 'header1,header2'; +headers.getAll('Multi') => headers.getAll('Multi') => + [ 'header1', 'header2' ]; [ 'header1,header2' ]; + +// Instead, to preserve the older behavior, you can use the header pair array +// syntax. +const headers = new Headers([ + [ 'Abc', 'string' ], + [ 'Multi', 'header1' ], + [ 'Multi', 'header2' ] +]); + + +////////////////////////////////////////////////////////////////////////////// +// All method parameters are now stringified. +const headers = new Headers(); +headers.set('null-header', null); +headers.set('undefined', undefined); + +// before after +headers.get('null-header') headers.get('null-header') + => null => 'null' +headers.get(undefined) headers.get(undefined) + => throws => 'undefined' + + +////////////////////////////////////////////////////////////////////////////// +// Invalid HTTP header names and values are now rejected outright. +const headers = new Headers(); +headers.set('Héy', 'ok'); // now throws +headers.get('Héy'); // now throws +new Headers({ 'Héy': 'ok' }); // now throws +``` + +## 0.10.x support dropped + +If you are still using Node.js v0.10, upgrade ASAP. Not only has Node.js +dropped support for that release branch, it has become too much work for us to +maintain. Therefore, we have dropped official support for v0.10 (it may still +work but don't expect them to do so). From 01bf40d5a4114c3c84954b24e5af8f57d25330bb Mon Sep 17 00:00:00 2001 From: Timothy Gu Date: Wed, 14 Dec 2016 13:51:43 -0800 Subject: [PATCH 55/61] Slightly expand upgrade guide --- UPGRADE-GUIDE.md | 39 ++++++++++++++++++++++++++------------- 1 file changed, 26 insertions(+), 13 deletions(-) diff --git a/UPGRADE-GUIDE.md b/UPGRADE-GUIDE.md index 79308f3..592b2ed 100644 --- a/UPGRADE-GUIDE.md +++ b/UPGRADE-GUIDE.md @@ -1,13 +1,13 @@ # Upgrade to node-fetch v2 node-fetch v2 brings about many changes that increase the compliance of -WHATWG's Fetch Standard. However, many of these changes meant that apps written -for node-fetch v1 needs to be updated to work with node-fetch v2 and be -conformant with the Fetch Standard. +WHATWG's [Fetch Standard][whatwg-fetch]. However, many of these changes meant +that apps written for node-fetch v1 needs to be updated to work with node-fetch +v2 and be conformant with the Fetch Standard. ## `.text()` no longer tries to detect encoding -Currently, `response.text()` attempts to guess the text encoding of the input +In v1, `response.text()` attempts to guess the text encoding of the input material and decode it for the user. However, it runs counter to the Fetch Standard which demands `.text()` to always use UTF-8. @@ -17,22 +17,29 @@ In "response" to that, we have changed `.text()` to use UTF-8. A new function ## Internal methods hidden -Currently, the user can access internal methods such as `_clone()`, -`_decode()`, and `_convert()` on the `response` object. While these methods -should never have been used, node-fetch v2 makes these functions completely -inaccessible, and may break your app. +In v1, the user can access internal methods such as `_clone()`, `_decode()`, +and `_convert()` on the `response` object. While these methods should never +have been used, node-fetch v2 makes these functions completely inaccessible. +If your app makes use of these functions, it may break when upgrading to v2. If you have a use case that requires these methods to be available, feel free to file an issue and we will be happy to help you solve the problem. ## Headers -The `Headers` class has gotten a lot of updates to make it spec-compliant. +The main goal we have for the `Headers` class in v2 is to make it completely +spec-compliant. However, due to changes in the Fetch Standard itself, total +spec compliance would mean incompatibility with all current major browser +implementations. + +Therefore, in v2, only a limited set of changes was applied to preserve +compatibility with browsers by default. See [#181] for more information on why +a feature is enabled or disabled. ```js ////////////////////////////////////////////////////////////////////////////// -// If you are using an object as the initializer, all arrays will be reduced -// to a string. +// If you are using an object as the initializer, all values will be +// stringified. For arrays, the members will be joined with a comma. const headers = new Headers({ 'Abc': 'string', 'Multi': [ 'header1', 'header2' ] @@ -78,5 +85,11 @@ new Headers({ 'Héy': 'ok' }); // now throws If you are still using Node.js v0.10, upgrade ASAP. Not only has Node.js dropped support for that release branch, it has become too much work for us to -maintain. Therefore, we have dropped official support for v0.10 (it may still -work but don't expect them to do so). +maintain. Therefore, we have dropped official support for v0.10. + +That being said, node-fetch may still work with v0.10, but as we are not +actively trying to support that version, it is in the user's best interest to +upgrade. + +[whatwg-fetch]: https://fetch.spec.whatwg.org/ +[#181]: https://github.com/bitinn/node-fetch/issues/181 From 7f928254118d0ac87dae5f90a20161c6c84c9f6a Mon Sep 17 00:00:00 2001 From: Timothy Gu Date: Wed, 14 Dec 2016 13:54:31 -0800 Subject: [PATCH 56/61] Upgrade Rollup --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index ecae2d3..d69dd48 100644 --- a/package.json +++ b/package.json @@ -48,7 +48,7 @@ "parted": "^0.1.1", "promise": "^7.1.1", "resumer": "0.0.0", - "rollup": "^0.36.4", + "rollup": "^0.37.0", "rollup-plugin-babel": "^2.6.1", "rollup-plugin-node-resolve": "^2.0.0", "whatwg-url": "^4.0.0" From fa225291280e161dedca5b923c73f82cda23c3fe Mon Sep 17 00:00:00 2001 From: Nathan Rajlich Date: Wed, 14 Dec 2016 14:09:27 -0800 Subject: [PATCH 57/61] proper stack first line for FetchError instances (#215) The `.stack` property gets cached in the `captureStackTrace()` call, so whatever is set as the `name` and `message` at that time will be used for the first line of the stack trace. Before this patch, FetchError's stack would just say "Error" as the first line. Now they correctly display the "${name}: ${message}" of the error instances. Test case included. Signed-off-by: Timothy Gu --- src/fetch-error.js | 6 +++--- test/test.js | 4 +++- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/src/fetch-error.js b/src/fetch-error.js index a417919..4d4b932 100644 --- a/src/fetch-error.js +++ b/src/fetch-error.js @@ -16,9 +16,6 @@ export default function FetchError(message, type, systemError) { Error.call(this, message); - // hide custom error implementation details from end-users - Error.captureStackTrace(this, this.constructor); - this.message = message; this.type = type; @@ -27,7 +24,10 @@ export default function FetchError(message, type, systemError) { this.code = this.errno = systemError.code; } + // hide custom error implementation details from end-users + Error.captureStackTrace(this, this.constructor); } FetchError.prototype = Object.create(Error.prototype); +FetchError.prototype.constructor = FetchError; FetchError.prototype.name = 'FetchError'; diff --git a/test/test.js b/test/test.js index 1c26044..ae4972b 100644 --- a/test/test.js +++ b/test/test.js @@ -1819,7 +1819,7 @@ describe(`node-fetch with FOLLOW_SPEC = ${defaultFollowSpec}`, () => { expect(body).to.have.property('buffer'); }); - it('should create custom FetchError', function() { + it('should create custom FetchError', function funcName() { const systemError = new Error('system'); systemError.code = 'ESOMEERROR'; @@ -1831,6 +1831,8 @@ describe(`node-fetch with FOLLOW_SPEC = ${defaultFollowSpec}`, () => { expect(err.type).to.equal('test-error'); expect(err.code).to.equal('ESOMEERROR'); expect(err.errno).to.equal('ESOMEERROR'); + expect(err.stack).to.include('funcName') + .and.to.startWith(`${err.name}: ${err.message}`); }); it('should support https request', function() { From 79704910008a6833f8354420d5caf69d001dff29 Mon Sep 17 00:00:00 2001 From: Timothy Gu Date: Wed, 14 Dec 2016 15:44:30 -0800 Subject: [PATCH 58/61] Use template literals in Request Closes #217. --- src/request.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/request.js b/src/request.js index 83fb336..50d7117 100644 --- a/src/request.js +++ b/src/request.js @@ -31,7 +31,7 @@ export default class Request extends Body { parsedURL = parse_url(input.href); } else { // coerce input to a string before attempting to parse - parsedURL = parse_url(input + ''); + parsedURL = parse_url(`${input}`); } input = {}; } else { From 151de2bdfb39bc4786b7a07e1e116a5690df6b94 Mon Sep 17 00:00:00 2001 From: Timothy Gu Date: Sat, 14 Jan 2017 20:50:10 -0800 Subject: [PATCH 59/61] Use ES2015 export syntax (#212) * Implement Rollup's external module check * Use ES2015 export syntax More friendly to ES2015 environments. --- .babelrc | 12 +++++------ build/babel-plugin.js | 48 ++++++++++++++++++++++++++++++++++++++++++ build/rollup-plugin.js | 16 ++++++++++++++ package.json | 3 ++- rollup.config.js | 14 ++++++++++-- src/index.js | 12 +++++------ test/test.js | 18 ++++++++++------ 7 files changed, 101 insertions(+), 22 deletions(-) create mode 100644 build/babel-plugin.js create mode 100644 build/rollup-plugin.js diff --git a/.babelrc b/.babelrc index 3cd9d25..f2845f2 100644 --- a/.babelrc +++ b/.babelrc @@ -6,6 +6,10 @@ "test": { "presets": [ [ "es2015", { "loose": true } ] + ], + "plugins": [ + "transform-runtime", + "./build/babel-plugin" ] }, "coverage": { @@ -13,12 +17,8 @@ [ "es2015", { "loose": true } ] ], "plugins": [ - [ "istanbul", { - "exclude": [ - "src/blob.js", - "test" - ] - } ] + [ "istanbul", { "exclude": [ "src/blob.js", "build", "test" ] } ], + "./build/babel-plugin" ] }, "rollup": { diff --git a/build/babel-plugin.js b/build/babel-plugin.js new file mode 100644 index 0000000..08efac9 --- /dev/null +++ b/build/babel-plugin.js @@ -0,0 +1,48 @@ +// This Babel plugin makes it possible to do CommonJS-style function exports + +const walked = Symbol('walked'); + +module.exports = ({ types: t }) => ({ + visitor: { + Program: { + exit(program) { + if (program[walked]) { + return; + } + + for (let path of program.get('body')) { + if (path.isExpressionStatement()) { + const expr = path.get('expression'); + if (expr.isAssignmentExpression() && + expr.get('left').matchesPattern('exports.*')) { + const prop = expr.get('left').get('property'); + if (prop.isIdentifier({ name: 'default' })) { + program.unshiftContainer('body', [ + t.expressionStatement( + t.assignmentExpression('=', + t.identifier('exports'), + t.assignmentExpression('=', + t.memberExpression( + t.identifier('module'), t.identifier('exports') + ), + expr.node.right + ) + ) + ), + t.expressionStatement( + t.assignmentExpression('=', + expr.node.left, t.identifier('exports') + ) + ) + ]); + path.remove(); + } + } + } + } + + program[walked] = true; + } + } + } +}); diff --git a/build/rollup-plugin.js b/build/rollup-plugin.js new file mode 100644 index 0000000..411d677 --- /dev/null +++ b/build/rollup-plugin.js @@ -0,0 +1,16 @@ +export default function tweakDefault() { + return { + transformBundle: function (source) { + var lines = source.split('\n'); + for (var i = 0; i < lines.length; i++) { + var line = lines[i]; + var matches = /^exports\['default'] = (.*);$/.exec(line); + if (matches) { + lines[i] = 'module.exports = exports = ' + matches[1] + ';'; + break; + } + } + return lines.join('\n'); + } + }; +} diff --git a/package.json b/package.json index d69dd48..84a8909 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,7 @@ "lib/index.es.js" ], "scripts": { - "build": "rollup -c", + "build": "cross-env BABEL_ENV=rollup rollup -c", "prepublish": "npm run build", "test": "cross-env BABEL_ENV=test mocha --compilers js:babel-register test/test.js", "report": "cross-env BABEL_ENV=coverage nyc --reporter lcov --reporter text mocha -R spec test/test.js", @@ -43,6 +43,7 @@ "codecov": "^1.0.1", "cross-env": "2.0.1", "form-data": ">=1.0.0", + "is-builtin-module": "^1.0.0", "mocha": "^3.1.2", "nyc": "^10.0.0", "parted": "^0.1.1", diff --git a/rollup.config.js b/rollup.config.js index 4a57423..3e3ed20 100644 --- a/rollup.config.js +++ b/rollup.config.js @@ -1,5 +1,7 @@ +import isBuiltin from 'is-builtin-module'; import babel from 'rollup-plugin-babel'; import resolve from 'rollup-plugin-node-resolve'; +import tweakDefault from './build/rollup-plugin'; process.env.BABEL_ENV = 'rollup'; @@ -8,10 +10,18 @@ export default { plugins: [ babel({ runtimeHelpers: true - }) + }), + tweakDefault() ], targets: [ { dest: 'lib/index.js', format: 'cjs' }, { dest: 'lib/index.es.js', format: 'es' } - ] + ], + external: function (id) { + if (isBuiltin(id)) { + return true; + } + id = id.split('/').slice(0, id[0] === '@' ? 2 : 1).join('/'); + return !!require('./package.json').dependencies[id]; + } }; diff --git a/src/index.js b/src/index.js index e9f2d55..ef668b8 100644 --- a/src/index.js +++ b/src/index.js @@ -24,7 +24,7 @@ import FetchError from './fetch-error'; * @param Object opts Fetch options * @return Promise */ -function fetch(url, opts) { +export default function fetch(url, opts) { // allow custom promise if (!fetch.Promise) { @@ -189,8 +189,6 @@ function fetch(url, opts) { }; -module.exports = fetch; - /** * Redirect code matching * @@ -210,6 +208,8 @@ fetch.Promise = global.Promise; * objects; existing objects are not affected. */ fetch.FOLLOW_SPEC = false; -fetch.Response = Response; -fetch.Headers = Headers; -fetch.Request = Request; +export { + Headers, + Request, + Response +}; diff --git a/test/test.js b/test/test.js index ae4972b..df0c6ff 100644 --- a/test/test.js +++ b/test/test.js @@ -23,10 +23,14 @@ const expect = chai.expect; import TestServer from './server'; // test subjects -import fetch from '../src/index.js'; -import Headers from '../src/headers.js'; -import Response from '../src/response.js'; -import Request from '../src/request.js'; +import fetch, { + Headers, + Request, + Response +} from '../src/'; +import HeadersOrig from '../src/headers.js'; +import RequestOrig from '../src/request.js'; +import ResponseOrig from '../src/response.js'; import Body from '../src/body.js'; import Blob from '../src/blob.js'; import FetchError from '../src/fetch-error.js'; @@ -93,9 +97,9 @@ describe(`node-fetch with FOLLOW_SPEC = ${defaultFollowSpec}`, () => { }); it('should expose Headers, Response and Request constructors', function() { - expect(fetch.Headers).to.equal(Headers); - expect(fetch.Response).to.equal(Response); - expect(fetch.Request).to.equal(Request); + expect(Headers).to.equal(HeadersOrig); + expect(Response).to.equal(ResponseOrig); + expect(Request).to.equal(RequestOrig); }); (supportToString ? it : it.skip)('should support proper toString output for Headers, Response and Request objects', function() { From f198f937677bafa54eaaed4b5635410c456da4ae Mon Sep 17 00:00:00 2001 From: Timothy Gu Date: Sat, 14 Jan 2017 20:56:26 -0800 Subject: [PATCH 60/61] test: remove fallbacks for Node.js 0.10 --- test/test.js | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/test/test.js b/test/test.js index df0c6ff..e588196 100644 --- a/test/test.js +++ b/test/test.js @@ -46,7 +46,6 @@ try { const supportToString = ({ [Symbol.toStringTag]: 'z' }).toString() === '[object z]'; -const supportIterator = !!(global.Symbol && global.Symbol.iterator); const local = new TestServer(); const base = `http://${local.hostname}:${local.port}/`; @@ -1242,9 +1241,7 @@ describe(`node-fetch with FOLLOW_SPEC = ${defaultFollowSpec}`, () => { ['a', '1'] ]); headers.append('b', '3'); - if (supportIterator) { - expect(headers).to.be.iterable; - } + expect(headers).to.be.iterable; const result = []; for (let pair of headers) { @@ -1262,7 +1259,7 @@ describe(`node-fetch with FOLLOW_SPEC = ${defaultFollowSpec}`, () => { ]); }); - (supportIterator ? it : it.skip)('should allow iterating through all headers with entries()', function() { + it('should allow iterating through all headers with entries()', function() { const headers = new Headers([ ['b', '2'], ['c', '4'], @@ -1283,7 +1280,7 @@ describe(`node-fetch with FOLLOW_SPEC = ${defaultFollowSpec}`, () => { ]); }); - (supportIterator ? it : it.skip)('should allow iterating through all headers with keys()', function() { + it('should allow iterating through all headers with keys()', function() { const headers = new Headers([ ['b', '2'], ['c', '4'], @@ -1295,7 +1292,7 @@ describe(`node-fetch with FOLLOW_SPEC = ${defaultFollowSpec}`, () => { .and.to.iterate.over(Headers.FOLLOW_SPEC ? ['a', 'b', 'c'] : ['b', 'b', 'c', 'a']); }); - (supportIterator ? it : it.skip)('should allow iterating through all headers with values()', function() { + it('should allow iterating through all headers with values()', function() { const headers = new Headers([ ['b', '2'], ['c', '4'], From 502b6042081a6c564dd2125a1dd5c4e3864b99dd Mon Sep 17 00:00:00 2001 From: Timothy Gu Date: Sat, 14 Jan 2017 21:11:30 -0800 Subject: [PATCH 61/61] Fix Headers iterable initializer handling --- src/headers.js | 10 ++++++++-- test/test.js | 22 ++++++++++++++++++++-- 2 files changed, 28 insertions(+), 4 deletions(-) diff --git a/src/headers.js b/src/headers.js index eb47f1c..020ca70 100644 --- a/src/headers.js +++ b/src/headers.js @@ -44,10 +44,14 @@ export default class Headers { this.append(name, value); } } - } else if (Array.isArray(headers)) { + } else if (typeof headers === 'object' && headers[Symbol.iterator]) { // array of tuples for (let el of headers) { - if (!Array.isArray(el) || el.length !== 2) { + if (typeof el !== 'object' || !el[Symbol.iterator]) { + throw new TypeError('Header pairs must be an iterable object'); + } + el = Array.from(el); + if (el.length !== 2) { throw new TypeError('Header pairs must contain exactly two items'); } this.append(el[0], el[1]); @@ -59,6 +63,8 @@ export default class Headers { // will handle it. this.append(prop, headers[prop]); } + } else if (headers != null) { + throw new TypeError('Provided initializer must be an object'); } Object.defineProperty(this, Symbol.toStringTag, { diff --git a/test/test.js b/test/test.js index e588196..129ed03 100644 --- a/test/test.js +++ b/test/test.js @@ -1435,19 +1435,37 @@ describe(`node-fetch with FOLLOW_SPEC = ${defaultFollowSpec}`, () => { expect(h3Raw['b']).to.include('1'); }); - it('should accept headers as an array of tuples', function() { - const headers = new Headers([ + it('should accept headers as an iterable of tuples', function() { + let headers; + + headers = new Headers([ ['a', '1'], ['b', '2'], ['a', '3'] ]); expect(headers.getAll('a')).to.deep.equal(['1', '3']); expect(headers.getAll('b')).to.deep.equal(['2']); + + headers = new Headers([ + new Set(['a', '1']), + ['b', '2'], + new Map([['a', null], ['3', null]]).keys() + ]); + expect(headers.getAll('a')).to.deep.equal(['1', '3']); + expect(headers.getAll('b')).to.deep.equal(['2']); + + headers = new Headers(new Map([ + ['a', '1'], + ['b', '2'] + ])); + expect(headers.getAll('a')).to.deep.equal(['1']); + expect(headers.getAll('b')).to.deep.equal(['2']); }); it('should throw a TypeError if non-tuple exists in a headers initializer', function() { expect(() => new Headers([ ['b', '2', 'huh?'] ])).to.throw(TypeError); expect(() => new Headers([ 'b2' ])).to.throw(TypeError); + expect(() => new Headers('b2')).to.throw(TypeError); }); it('should support fetch with Request instance', function() {