From 5b10a10743b8459f64fe83e0ff420f69da79c9a4 Mon Sep 17 00:00:00 2001 From: Yuchen Pei Date: Thu, 28 Jul 2022 15:32:10 +1000 Subject: Moving scripts and utilities into a new utilities dir --- .../hash_script/node_modules/node-fetch/.npmignore | 41 + .../node_modules/node-fetch/.travis.yml | 12 + .../node_modules/node-fetch/CHANGELOG.md | 158 +++ .../node_modules/node-fetch/ERROR-HANDLING.md | 21 + .../hash_script/node_modules/node-fetch/LICENSE.md | 22 + .../hash_script/node_modules/node-fetch/LIMITS.md | 27 + .../hash_script/node_modules/node-fetch/README.md | 210 +++ .../hash_script/node_modules/node-fetch/index.js | 271 ++++ .../node_modules/node-fetch/lib/body.js | 261 ++++ .../node_modules/node-fetch/lib/fetch-error.js | 34 + .../node_modules/node-fetch/lib/headers.js | 141 ++ .../node_modules/node-fetch/lib/index.js | 1416 +++++++++++++++++++ .../node_modules/node-fetch/lib/request.js | 75 + .../node_modules/node-fetch/lib/response.js | 50 + .../node_modules/node-fetch/package.json | 110 ++ .../node_modules/node-fetch/test/dummy.txt | 1 + .../node_modules/node-fetch/test/server.js | 340 +++++ .../node_modules/node-fetch/test/test.js | 1487 ++++++++++++++++++++ 18 files changed, 4677 insertions(+) create mode 100644 utilities/hash_script/node_modules/node-fetch/.npmignore create mode 100644 utilities/hash_script/node_modules/node-fetch/.travis.yml create mode 100644 utilities/hash_script/node_modules/node-fetch/CHANGELOG.md create mode 100644 utilities/hash_script/node_modules/node-fetch/ERROR-HANDLING.md create mode 100644 utilities/hash_script/node_modules/node-fetch/LICENSE.md create mode 100644 utilities/hash_script/node_modules/node-fetch/LIMITS.md create mode 100644 utilities/hash_script/node_modules/node-fetch/README.md create mode 100644 utilities/hash_script/node_modules/node-fetch/index.js create mode 100644 utilities/hash_script/node_modules/node-fetch/lib/body.js create mode 100644 utilities/hash_script/node_modules/node-fetch/lib/fetch-error.js create mode 100644 utilities/hash_script/node_modules/node-fetch/lib/headers.js create mode 100644 utilities/hash_script/node_modules/node-fetch/lib/index.js create mode 100644 utilities/hash_script/node_modules/node-fetch/lib/request.js create mode 100644 utilities/hash_script/node_modules/node-fetch/lib/response.js create mode 100644 utilities/hash_script/node_modules/node-fetch/package.json create mode 100644 utilities/hash_script/node_modules/node-fetch/test/dummy.txt create mode 100644 utilities/hash_script/node_modules/node-fetch/test/server.js create mode 100644 utilities/hash_script/node_modules/node-fetch/test/test.js (limited to 'utilities/hash_script/node_modules/node-fetch') diff --git a/utilities/hash_script/node_modules/node-fetch/.npmignore b/utilities/hash_script/node_modules/node-fetch/.npmignore new file mode 100644 index 0000000..a9cb254 --- /dev/null +++ b/utilities/hash_script/node_modules/node-fetch/.npmignore @@ -0,0 +1,41 @@ +# Logs +logs +*.log + +# Runtime data +pids +*.pid +*.seed + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# Coverage directory used by tools like istanbul +coverage + +# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) +.grunt + +# Compiled binary addons (http://nodejs.org/api/addons.html) +build/Release + +# Dependency directory +# Commenting this out is preferred by some people, see +# https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git- +node_modules + +# Users Environment Variables +.lock-wscript + +# OS files +.DS_Store + +# Coveralls token files +.coveralls.yml + +## ignore some files from 2.x branch + +.nyc_output +lib/index.js +lib/index.es.js +package-lock.json diff --git a/utilities/hash_script/node_modules/node-fetch/.travis.yml b/utilities/hash_script/node_modules/node-fetch/.travis.yml new file mode 100644 index 0000000..44b72f0 --- /dev/null +++ b/utilities/hash_script/node_modules/node-fetch/.travis.yml @@ -0,0 +1,12 @@ +language: node_js +node_js: + - "0.10" + - "0.12" + - "node" +env: + - FORMDATA_VERSION=1.0.0 + - FORMDATA_VERSION=2.1.0 +before_script: + - 'if [ "$FORMDATA_VERSION" ]; then npm install form-data@^$FORMDATA_VERSION; fi' +before_install: if [[ `npm -v` < 3 ]]; then npm install -g npm@1.4.28; fi +script: npm run coverage \ No newline at end of file diff --git a/utilities/hash_script/node_modules/node-fetch/CHANGELOG.md b/utilities/hash_script/node_modules/node-fetch/CHANGELOG.md new file mode 100644 index 0000000..ea8ebe7 --- /dev/null +++ b/utilities/hash_script/node_modules/node-fetch/CHANGELOG.md @@ -0,0 +1,158 @@ + +Changelog +========= + + +# 1.x release + +(Note: `1.x` will only have backported bugfix releases beyond `1.7.0`) + +## v1.7.2 + +- Fix: when using node-fetch with test framework such as `jest`, `instanceof` check could fail in `Headers` class. This is causing some header values, such as `set-cookie`, to be dropped incorrectly. + +## v1.7.1 + +- Fix: close local test server properly under Node 8. + +## v1.7.0 + +- Fix: revert change in `v1.6.2` where 204 no-content response is handled with a special case, this conflicts with browser Fetch implementation (as browsers always throw when res.json() parses an empty string). Since this is an operational error, it's wrapped in a `FetchError` for easier error handling. +- Fix: move code coverage tool to codecov platform and update travis config + +## v1.6.3 + +- Enhance: error handling document to explain `FetchError` design +- Fix: support `form-data` 2.x releases (requires `form-data` >= 2.1.0) + +## v1.6.2 + +- Enhance: minor document update +- Fix: response.json() returns empty object on 204 no-content response instead of throwing a syntax error + +## v1.6.1 + +- Fix: if `res.body` is a non-stream non-formdata object, we will call `body.toString` and send it as a string +- Fix: `counter` value is incorrectly set to `follow` value when wrapping Request instance +- Fix: documentation update + +## v1.6.0 + +- Enhance: added `res.buffer()` api for convenience, it returns body as a Node.js buffer +- Enhance: better old server support by handling raw deflate response +- Enhance: skip encoding detection for non-HTML/XML response +- Enhance: minor document update +- Fix: HEAD request doesn't need decompression, as body is empty +- Fix: `req.body` now accepts a Node.js buffer + +## v1.5.3 + +- Fix: handle 204 and 304 responses when body is empty but content-encoding is gzip/deflate +- Fix: allow resolving response and cloned response in any order +- Fix: avoid setting `content-length` when `form-data` body use streams +- Fix: send DELETE request with content-length when body is present +- Fix: allow any url when calling new Request, but still reject non-http(s) url in fetch + +## v1.5.2 + +- Fix: allow node.js core to handle keep-alive connection pool when passing a custom agent + +## v1.5.1 + +- Fix: redirect mode `manual` should work even when there is no redirection or broken redirection + +## v1.5.0 + +- Enhance: rejected promise now use custom `Error` (thx to @pekeler) +- Enhance: `FetchError` contains `err.type` and `err.code`, allows for better error handling (thx to @pekeler) +- Enhance: basic support for redirect mode `manual` and `error`, allows for location header extraction (thx to @jimmywarting for the initial PR) + +## v1.4.1 + +- Fix: wrapping Request instance with FormData body again should preserve the body as-is + +## v1.4.0 + +- Enhance: Request and Response now have `clone` method (thx to @kirill-konshin for the initial PR) +- Enhance: Request and Response now have proper string and buffer body support (thx to @kirill-konshin) +- Enhance: Body constructor has been refactored out (thx to @kirill-konshin) +- Enhance: Headers now has `forEach` method (thx to @tricoder42) +- Enhance: back to 100% code coverage +- Fix: better form-data support (thx to @item4) +- Fix: better character encoding detection under chunked encoding (thx to @dsuket for the initial PR) + +## v1.3.3 + +- Fix: make sure `Content-Length` header is set when body is string for POST/PUT/PATCH requests +- Fix: handle body stream error, for cases such as incorrect `Content-Encoding` header +- Fix: when following certain redirects, use `GET` on subsequent request per Fetch Spec +- Fix: `Request` and `Response` constructors now parse headers input using `Headers` + +## v1.3.2 + +- Enhance: allow auto detect of form-data input (no `FormData` spec on node.js, this is form-data specific feature) + +## v1.3.1 + +- Enhance: allow custom host header to be set (server-side only feature, as it's a forbidden header on client-side) + +## v1.3.0 + +- Enhance: now `fetch.Request` is exposed as well + +## v1.2.1 + +- Enhance: `Headers` now normalized `Number` value to `String`, prevent common mistakes + +## v1.2.0 + +- Enhance: now fetch.Headers and fetch.Response are exposed, making testing easier + +## v1.1.2 + +- Fix: `Headers` should only support `String` and `Array` properties, and ignore others + +## v1.1.1 + +- Enhance: now req.headers accept both plain object and `Headers` instance + +## v1.1.0 + +- Enhance: timeout now also applies to response body (in case of slow response) +- Fix: timeout is now cleared properly when fetch is done/has failed + +## v1.0.6 + +- Fix: less greedy content-type charset matching + +## v1.0.5 + +- Fix: when `follow = 0`, fetch should not follow redirect +- Enhance: update tests for better coverage +- Enhance: code formatting +- Enhance: clean up doc + +## v1.0.4 + +- Enhance: test iojs support +- Enhance: timeout attached to socket event only fire once per redirect + +## v1.0.3 + +- Fix: response size limit should reject large chunk +- Enhance: added character encoding detection for xml, such as rss/atom feed (encoding in DTD) + +## v1.0.2 + +- Fix: added res.ok per spec change + +## v1.0.0 + +- Enhance: better test coverage and doc + + +# 0.x release + +## v0.1 + +- Major: initial public release diff --git a/utilities/hash_script/node_modules/node-fetch/ERROR-HANDLING.md b/utilities/hash_script/node_modules/node-fetch/ERROR-HANDLING.md new file mode 100644 index 0000000..0e4025d --- /dev/null +++ b/utilities/hash_script/node_modules/node-fetch/ERROR-HANDLING.md @@ -0,0 +1,21 @@ + +Error handling with node-fetch +============================== + +Because `window.fetch` isn't designed to transparent about the cause of request errors, we have to come up with our own solutions. + +The basics: + +- All [operational errors](https://www.joyent.com/node-js/production/design/errors) are rejected as [FetchError](https://github.com/bitinn/node-fetch/blob/master/lib/fetch-error.js), you can handle them all through promise `catch` clause. + +- All errors comes with `err.message` detailing the cause of errors. + +- All errors originated from `node-fetch` are marked with custom `err.type`. + +- All errors originated from Node.js core are marked with `err.type = system`, and contains addition `err.code` and `err.errno` for error handling, they are alias to error codes thrown by Node.js core. + +- [Programmer errors](https://www.joyent.com/node-js/production/design/errors) are either thrown as soon as possible, or rejected with default `Error` with `err.message` for ease of troubleshooting. + +List of error types: + +- Because we maintain 100% coverage, see [test.js](https://github.com/bitinn/node-fetch/blob/master/test/test.js) for a full list of custom `FetchError` types, as well as some of the common errors from Node.js diff --git a/utilities/hash_script/node_modules/node-fetch/LICENSE.md b/utilities/hash_script/node_modules/node-fetch/LICENSE.md new file mode 100644 index 0000000..660ffec --- /dev/null +++ b/utilities/hash_script/node_modules/node-fetch/LICENSE.md @@ -0,0 +1,22 @@ +The MIT License (MIT) + +Copyright (c) 2016 David Frank + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + diff --git a/utilities/hash_script/node_modules/node-fetch/LIMITS.md b/utilities/hash_script/node_modules/node-fetch/LIMITS.md new file mode 100644 index 0000000..d0d41fc --- /dev/null +++ b/utilities/hash_script/node_modules/node-fetch/LIMITS.md @@ -0,0 +1,27 @@ + +Known differences +================= + +*As of 1.x release* + +- Topics such as Cross-Origin, Content Security Policy, Mixed Content, Service Workers are ignored, given our server-side context. + +- URL input must be an absolute URL, using either `http` or `https` as scheme. + +- On the upside, there are no forbidden headers, and `res.url` contains the final url when following redirects. + +- For convenience, `res.body` is a transform stream, so decoding can be handled independently. + +- Similarly, `req.body` can either be a string, a buffer or a readable stream. + +- 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. + +- 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. diff --git a/utilities/hash_script/node_modules/node-fetch/README.md b/utilities/hash_script/node_modules/node-fetch/README.md new file mode 100644 index 0000000..0bfb387 --- /dev/null +++ b/utilities/hash_script/node_modules/node-fetch/README.md @@ -0,0 +1,210 @@ + +node-fetch +========== + +[![npm version][npm-image]][npm-url] +[![build status][travis-image]][travis-url] +[![coverage status][codecov-image]][codecov-url] + +A light-weight module that brings `window.fetch` to Node.js + + +# Motivation + +Instead of implementing `XMLHttpRequest` in Node.js to run browser-specific [Fetch polyfill](https://github.com/github/fetch), why not go from native `http` to `Fetch` API directly? Hence `node-fetch`, minimal code for a `window.fetch` compatible API on Node.js runtime. + +See Matt Andrews' [isomorphic-fetch](https://github.com/matthew-andrews/isomorphic-fetch) for isomorphic usage (exports `node-fetch` for server-side, `whatwg-fetch` for client-side). + + +# Features + +- Stay consistent with `window.fetch` API. +- Make conscious trade-off when following [whatwg fetch spec](https://fetch.spec.whatwg.org/) and [stream spec](https://streams.spec.whatwg.org/) implementation details, document known difference. +- Use native promise, but allow substituting it with [insert your favorite promise library]. +- Use native stream for body, on both request and response. +- Decode content encoding (gzip/deflate) properly, and convert string output (such as `res.text()` and `res.json()`) to UTF-8 automatically. +- Useful extensions such as timeout, redirect limit, response size limit, [explicit errors](https://github.com/bitinn/node-fetch/blob/master/ERROR-HANDLING.md) for troubleshooting. + + +# Difference from client-side fetch + +- See [Known Differences](https://github.com/bitinn/node-fetch/blob/master/LIMITS.md) for details. +- If you happen to use a missing feature that `window.fetch` offers, feel free to open an issue. +- Pull requests are welcomed too! + + +# Install + +`npm install node-fetch --save` + + +# Usage + +```javascript +var fetch = require('node-fetch'); + +// if you are on node v0.10, set a Promise library first, eg. +// fetch.Promise = require('bluebird'); + +// plain text or html + +fetch('https://github.com/') + .then(function(res) { + return res.text(); + }).then(function(body) { + console.log(body); + }); + +// json + +fetch('https://api.github.com/users/github') + .then(function(res) { + return res.json(); + }).then(function(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); + }); + +// 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'); + res.body.pipe(dest); + }); + +// buffer +// 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'); +fetch('https://assets-cdn.github.com/images/modules/logos_page/Octocat.png') + .then(function(res) { + return res.buffer(); + }).then(function(buffer) { + fileType(buffer); + }); + +// meta + +fetch('https://github.com/') + .then(function(res) { + console.log(res.ok); + console.log(res.status); + console.log(res.statusText); + console.log(res.headers.raw()); + console.log(res.headers.get('content-type')); + }); + +// post + +fetch('http://httpbin.org/post', { method: 'POST', body: 'a=1' }) + .then(function(res) { + return res.json(); + }).then(function(json) { + console.log(json); + }); + +// post with stream from resumer + +var resumer = require('resumer'); +var stream = resumer().queue('a=1').end(); +fetch('http://httpbin.org/post', { method: 'POST', body: stream }) + .then(function(res) { + return res.json(); + }).then(function(json) { + console.log(json); + }); + +// post with form-data (detect multipart) + +var FormData = require('form-data'); +var 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); + }); + +// post with form-data (custom headers) +// note that getHeaders() is non-standard API + +var FormData = require('form-data'); +var 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); + }); + +// node 0.12+, yield with co + +var co = require('co'); +co(function *() { + var res = yield fetch('https://api.github.com/users/github'); + var json = yield res.json(); + console.log(res); +}); +``` + +See [test cases](https://github.com/bitinn/node-fetch/blob/master/test/test.js) for more examples. + + +# API + +## fetch(url, options) + +Returns a `Promise` + +### Url + +Should be an absolute url, eg `http://example.com/` + +### Options + +default values are shown, note that only `method`, `headers`, `redirect` and `body` are allowed in `window.fetch`, others are node.js extensions. + +``` +{ + method: 'GET' + , headers: {} // request header. format {a:'1'} or {b:['1','2','3']} + , redirect: 'follow' // set to `manual` to extract redirect headers, `error` to reject redirect + , follow: 20 // maximum redirect count. 0 to not follow redirect + , timeout: 0 // req/res timeout in ms, it resets on redirect. 0 to disable (OS limit applies) + , compress: true // support gzip/deflate content encoding. false to disable + , size: 0 // maximum response body size in bytes. 0 to disable + , body: empty // request body. can be a string, buffer, readable stream + , agent: null // http.Agent instance, allows custom proxy, certificate etc. +} +``` + + +# License + +MIT + + +# Acknowledgement + +Thanks to [github/fetch](https://github.com/github/fetch) for providing a solid implementation reference. + + +[npm-image]: https://img.shields.io/npm/v/node-fetch.svg?style=flat-square +[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 +[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 diff --git a/utilities/hash_script/node_modules/node-fetch/index.js b/utilities/hash_script/node_modules/node-fetch/index.js new file mode 100644 index 0000000..8f6730d --- /dev/null +++ b/utilities/hash_script/node_modules/node-fetch/index.js @@ -0,0 +1,271 @@ + +/** + * index.js + * + * a request API compatible with window.fetch + */ + +var parse_url = require('url').parse; +var resolve_url = require('url').resolve; +var http = require('http'); +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'); + +// commonjs +module.exports = Fetch; +// es6 default export compatibility +module.exports.default = module.exports; + +/** + * Fetch class + * + * @param Mixed url Absolute url or Request instance + * @param Object opts Fetch options + * @return Promise + */ +function Fetch(url, opts) { + + // allow call as function + if (!(this instanceof Fetch)) + return new Fetch(url, opts); + + // allow custom promise + if (!Fetch.Promise) { + throw new Error('native promise missing, set Fetch.Promise to your favorite alternative'); + } + + Body.Promise = Fetch.Promise; + + var self = this; + + // wrap http.request into fetch + return new Fetch.Promise(function(resolve, reject) { + // build request object + var options = new Request(url, opts); + + if (!options.protocol || !options.hostname) { + throw new Error('only absolute urls are supported'); + } + + if (options.protocol !== 'http:' && options.protocol !== 'https:') { + throw new Error('only http(s) protocols are supported'); + } + + var send; + if (options.protocol === 'https:') { + send = https.request; + } else { + send = http.request; + } + + // normalize headers + var 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', '*/*'); + } + + // 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') { + 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]; + } + + // send request + var req = send(options); + var reqTimeout; + + if (options.timeout) { + req.once('socket', function(socket) { + reqTimeout = setTimeout(function() { + req.abort(); + reject(new FetchError('network timeout at: ' + options.url, 'request-timeout')); + }, options.timeout); + }); + } + + req.on('error', function(err) { + clearTimeout(reqTimeout); + reject(new FetchError('request to ' + options.url + ' failed, reason: ' + err.message, 'system', err)); + }); + + req.on('response', function(res) { + clearTimeout(reqTimeout); + + // handle redirect + if (self.isRedirect(res.statusCode) && options.redirect !== 'manual') { + if (options.redirect === 'error') { + reject(new FetchError('redirect mode is set to error: ' + options.url, 'no-redirect')); + return; + } + + if (options.counter >= options.follow) { + reject(new FetchError('maximum redirect reached at: ' + options.url, 'max-redirect')); + return; + } + + if (!res.headers.location) { + reject(new FetchError('redirect location header missing at: ' + options.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')) + { + options.method = 'GET'; + delete options.body; + delete options.headers['content-length']; + } + + options.counter++; + + resolve(Fetch(resolve_url(options.url, res.headers.location), options)); + return; + } + + // normalize location header for manual redirect mode + var 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 = { + url: options.url + , status: res.statusCode + , statusText: res.statusMessage + , headers: headers + , size: options.size + , timeout: options.timeout + }; + + // response object + var output; + + // in following scenarios we ignore compression support + // 1. compression support is disabled + // 2. HEAD request + // 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) { + output = new Response(body, response_options); + resolve(output); + return; + } + + // otherwise, check for gzip or deflate + var name = headers.get('content-encoding'); + + // for gzip + if (name == 'gzip' || name == 'x-gzip') { + body = body.pipe(zlib.createGunzip()); + output = new Response(body, response_options); + resolve(output); + return; + + // for deflate + } 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) { + // see http://stackoverflow.com/questions/37519828 + if ((chunk[0] & 0x0F) === 0x08) { + body = body.pipe(zlib.createInflate()); + } else { + body = body.pipe(zlib.createInflateRaw()); + } + output = new Response(body, response_options); + resolve(output); + }); + return; + } + + // otherwise, use response as-is + output = new Response(body, response_options); + resolve(output); + return; + }); + + // 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); + req.end(); + } else if (options.body instanceof Buffer) { + req.write(options.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()); + req.end(); + } else { + req.end(); + } + }); + +}; + +/** + * 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; +} + +// expose Promise +Fetch.Promise = global.Promise; +Fetch.Response = Response; +Fetch.Headers = Headers; +Fetch.Request = Request; diff --git a/utilities/hash_script/node_modules/node-fetch/lib/body.js b/utilities/hash_script/node_modules/node-fetch/lib/body.js new file mode 100644 index 0000000..19bc003 --- /dev/null +++ b/utilities/hash_script/node_modules/node-fetch/lib/body.js @@ -0,0 +1,261 @@ + +/** + * body.js + * + * 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'); + +module.exports = Body; + +/** + * Body class + * + * @param Stream body Readable stream + * @param Object opts Response options + * @return Void + */ +function Body(body, opts) { + + opts = opts || {}; + + 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 + */ +Body.prototype.json = function() { + + var self = this; + + return this._decode().then(function(buffer) { + try { + return JSON.parse(buffer.toString()); + } catch (err) { + return Body.Promise.reject(new FetchError('invalid json response body at ' + self.url + ' reason: ' + err.message, 'invalid-json')); + } + }); + +}; + +/** + * Decode response as text + * + * @return Promise + */ +Body.prototype.text = function() { + + return this._decode().then(function(buffer) { + return buffer.toString(); + }); + +}; + +/** + * Decode response as buffer (non-spec api) + * + * @return Promise + */ +Body.prototype.buffer = function() { + + return this._decode(); + +}; + +/** + * Decode buffers into utf-8 string + * + * @return Promise + */ +Body.prototype._decode = function() { + + var self = this; + + if (this.bodyUsed) { + return Body.Promise.reject(new Error('body used already for: ' + this.url)); + } + + this.bodyUsed = true; + this._bytes = 0; + this._abort = false; + this._raw = []; + + return new Body.Promise(function(resolve, reject) { + var resTimeout; + + // body is string + if (typeof self.body === 'string') { + self._bytes = self.body.length; + self._raw = [new Buffer(self.body)]; + return resolve(self._convert()); + } + + // body is buffer + if (self.body instanceof Buffer) { + self._bytes = self.body.length; + self._raw = [self.body]; + return resolve(self._convert()); + } + + // allow timeout on slow response body + if (self.timeout) { + resTimeout = setTimeout(function() { + self._abort = true; + reject(new FetchError('response timeout at ' + self.url + ' over limit: ' + self.timeout, 'body-timeout')); + }, self.timeout); + } + + // handle stream error, such as incorrect content-encoding + self.body.on('error', function(err) { + reject(new FetchError('invalid response body at: ' + self.url + ' reason: ' + err.message, 'system', err)); + }); + + // body is stream + self.body.on('data', function(chunk) { + if (self._abort || chunk === null) { + return; + } + + if (self.size && self._bytes + chunk.length > 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 = / 1 && arguments[1] !== undefined ? arguments[1] : {}, + _ref$size = _ref.size; + + let size = _ref$size === undefined ? 0 : _ref$size; + var _ref$timeout = _ref.timeout; + let timeout = _ref$timeout === undefined ? 0 : _ref$timeout; + + if (body == null) { + // body is undefined or null + body = null; + } else if (typeof body === 'string') { + // body is string + } else if (isURLSearchParams(body)) { + // body is a URLSearchParams + } else if (body instanceof Blob) { + // body is blob + } else if (Buffer.isBuffer(body)) { + // body is buffer + } else if (body instanceof Stream) { + // body is stream + } else { + // none of the above + // coerce to string + body = String(body); + } + this.body = body; + this[DISTURBED] = false; + this.size = size; + this.timeout = timeout; +} + +Body.prototype = { + get bodyUsed() { + return this[DISTURBED]; + }, + + /** + * Decode response as ArrayBuffer + * + * @return Promise + */ + arrayBuffer() { + return consumeBody.call(this).then(function (buf) { + return buf.buffer.slice(buf.byteOffset, buf.byteOffset + buf.byteLength); + }); + }, + + /** + * Return raw response as Blob + * + * @return Promise + */ + blob() { + let ct = this.headers && this.headers.get('content-type') || ''; + return consumeBody.call(this).then(function (buf) { + return Object.assign( + // Prevent copying + new Blob([], { + type: ct.toLowerCase() + }), { + [BUFFER]: buf + }); + }); + }, + + /** + * Decode response as json + * + * @return Promise + */ + json() { + var _this = this; + + return consumeBody.call(this).then(function (buffer) { + try { + return JSON.parse(buffer.toString()); + } catch (err) { + return Body.Promise.reject(new FetchError(`invalid json response body at ${_this.url} reason: ${err.message}`, 'invalid-json')); + } + }); + }, + + /** + * Decode response as text + * + * @return Promise + */ + text() { + return consumeBody.call(this).then(function (buffer) { + return buffer.toString(); + }); + }, + + /** + * Decode response as buffer (non-spec api) + * + * @return Promise + */ + buffer() { + return consumeBody.call(this); + }, + + /** + * Decode response as text, while automatically detecting the encoding and + * trying to decode to UTF-8 (non-spec api) + * + * @return Promise + */ + textConverted() { + var _this2 = this; + + return consumeBody.call(this).then(function (buffer) { + return convertBody(buffer, _this2.headers); + }); + } + +}; + +Body.mixIn = function (proto) { + for (const name of Object.getOwnPropertyNames(Body.prototype)) { + // istanbul ignore else: future proof + if (!(name in proto)) { + const desc = Object.getOwnPropertyDescriptor(Body.prototype, name); + Object.defineProperty(proto, name, desc); + } + } +}; + +/** + * Decode buffers into utf-8 string + * + * @return Promise + */ +function consumeBody(body) { + var _this3 = this; + + if (this[DISTURBED]) { + return Body.Promise.reject(new Error(`body used already for: ${this.url}`)); + } + + this[DISTURBED] = true; + + // body is null + if (this.body === null) { + return Body.Promise.resolve(Buffer.alloc(0)); + } + + // body is string + if (typeof this.body === 'string') { + return Body.Promise.resolve(Buffer.from(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); + } + + // istanbul ignore if: should never happen + if (!(this.body instanceof Stream)) { + return Body.Promise.resolve(Buffer.alloc(0)); + } + + // body is stream + // get ready to actually consume the body + let accum = []; + let accumBytes = 0; + let abort = false; + + return new Body.Promise(function (resolve, reject) { + let resTimeout; + + // allow timeout on slow response body + if (_this3.timeout) { + resTimeout = setTimeout(function () { + abort = true; + reject(new FetchError(`Response timeout while trying to fetch ${_this3.url} (over ${_this3.timeout}ms)`, 'body-timeout')); + }, _this3.timeout); + } + + // handle stream error, such as incorrect content-encoding + _this3.body.on('error', function (err) { + reject(new FetchError(`Invalid response body while trying to fetch ${_this3.url}: ${err.message}`, 'system', err)); + }); + + _this3.body.on('data', function (chunk) { + if (abort || chunk === null) { + return; + } + + if (_this3.size && accumBytes + chunk.length > _this3.size) { + abort = true; + reject(new FetchError(`content size at ${_this3.url} over limit: ${_this3.size}`, 'max-size')); + return; + } + + accumBytes += chunk.length; + accum.push(chunk); + }); + + _this3.body.on('end', function () { + if (abort) { + return; + } + + clearTimeout(resTimeout); + resolve(Buffer.concat(accum)); + }); + }); +} + +/** + * 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 Buffer buffer Incoming buffer + * @param String encoding Target encoding + * @return String + */ +function convertBody(buffer, headers) { + if (typeof convert !== 'function') { + throw new Error('The package `encoding` must be installed to use the textConverted() function'); + } + + const ct = headers.get('content-type'); + let charset = 'utf-8'; + let res, str; + + // header + if (ct) { + res = /charset=([^;]*)/i.exec(ct); + } + + // no charset in content type, peek at response body for at most 1024 bytes + str = buffer.slice(0, 1024).toString(); + + // html5 + if (!res && str) { + res = /= 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; +} +/** + * 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; +} + +/** + * headers.js + * + * Headers class offers convenient helpers + */ + +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; +} + +const MAP = Symbol('map'); +class Headers { + /** + * Headers class + * + * @param Object headers Response headers + * @return Void + */ + constructor() { + let init = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : undefined; + + this[MAP] = Object.create(null); + + if (init instanceof Headers) { + const rawHeaders = init.raw(); + const headerNames = Object.keys(rawHeaders); + + for (const headerName of headerNames) { + for (const value of rawHeaders[headerName]) { + this.append(headerName, value); + } + } + + return; + } + + // We don't worry about converting prop to ByteString here as append() + // will handle it. + if (init == null) { + // no op + } else if (typeof init === 'object') { + const method = init[Symbol.iterator]; + if (method != null) { + if (typeof method !== 'function') { + throw new TypeError('Header pairs must be iterable'); + } + + // sequence> + // Note: per spec we have to first exhaust the lists then process them + const pairs = []; + for (const pair of init) { + if (typeof pair !== 'object' || typeof pair[Symbol.iterator] !== 'function') { + throw new TypeError('Each header pair must be iterable'); + } + pairs.push(Array.from(pair)); + } + + for (const pair of pairs) { + if (pair.length !== 2) { + throw new TypeError('Each header pair must be a name/value tuple'); + } + this.append(pair[0], pair[1]); + } + } else { + // record + for (const key of Object.keys(init)) { + const value = init[key]; + this.append(key, value); + } + } + } else { + throw new TypeError('Provided initializer must be an object'); + } + + Object.defineProperty(this, Symbol.toStringTag, { + value: 'Headers', + writable: false, + enumerable: false, + configurable: true + }); + } + + /** + * Return first header value given name + * + * @param String name Header name + * @return Mixed + */ + get(name) { + const list = this[MAP][sanitizeName(name)]; + if (!list) { + return null; + } + + return list.join(', '); + } + + /** + * 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) { + let thisArg = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : undefined; + + let pairs = getHeaderPairs(this); + let i = 0; + while (i < pairs.length) { + var _pairs$i = pairs[i]; + const name = _pairs$i[0], + value = _pairs$i[1]; + + callback.call(thisArg, value, name, this); + pairs = getHeaderPairs(this); + i++; + } + } + + /** + * Overwrite header values given name + * + * @param String name Header name + * @param String value Header value + * @return Void + */ + set(name, value) { + this[MAP][sanitizeName(name)] = [sanitizeValue(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][sanitizeName(name)].push(sanitizeValue(value)); + } + + /** + * Check for header name existence + * + * @param String name Header name + * @return Boolean + */ + has(name) { + return !!this[MAP][sanitizeName(name)]; + } + + /** + * Delete all header values given name + * + * @param String name Header name + * @return Void + */ + delete(name) { + delete this[MAP][sanitizeName(name)]; + } + + /** + * Return raw headers (non-spec api) + * + * @return Object + */ + raw() { + return this[MAP]; + } + + /** + * Get an iterator on keys. + * + * @return Iterator + */ + keys() { + return createHeadersIterator(this, 'key'); + } + + /** + * Get an iterator on values. + * + * @return Iterator + */ + values() { + return createHeadersIterator(this, 'value'); + } + + /** + * Get an iterator on entries. + * + * This is the default iterator of the Headers object. + * + * @return Iterator + */ + [Symbol.iterator]() { + return createHeadersIterator(this, 'key+value'); + } +} +Headers.prototype.entries = Headers.prototype[Symbol.iterator]; + +Object.defineProperty(Headers.prototype, Symbol.toStringTag, { + value: 'HeadersPrototype', + writable: false, + enumerable: false, + configurable: true +}); + +function getHeaderPairs(headers, kind) { + const keys = Object.keys(headers[MAP]).sort(); + return keys.map(kind === 'key' ? function (k) { + return [k]; + } : function (k) { + return [k, headers.get(k)]; + }); +} + +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() { + // istanbul ignore if + if (!this || Object.getPrototypeOf(this) !== HeadersIteratorPrototype) { + throw new TypeError('Value of `this` is not a HeadersIterator'); + } + + var _INTERNAL = this[INTERNAL]; + const target = _INTERNAL.target, + kind = _INTERNAL.kind, + index = _INTERNAL.index; + + 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 +}); + +/** + * response.js + * + * Response class provides content decoding + */ + +var _require$2 = require('http'); + +const STATUS_CODES = _require$2.STATUS_CODES; + +/** + * Response class + * + * @param Stream body Readable stream + * @param Object opts Response options + * @return Void + */ + +class Response { + constructor() { + let body = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : null; + let opts = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; + + Body.call(this, body, opts); + + this.url = opts.url; + 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 + }); + } + + /** + * Convenience property representing if the request ended normally + */ + get ok() { + return this.status >= 200 && this.status < 300; + } + + /** + * 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 + }); + } +} + +Body.mixIn(Response.prototype); + +Object.defineProperty(Response.prototype, Symbol.toStringTag, { + value: 'ResponsePrototype', + writable: false, + enumerable: false, + configurable: true +}); + +/** + * request.js + * + * Request class contains server only options + */ + +var _require$3 = require('url'); + +const format_url = _require$3.format; +const parse_url = _require$3.parse; + + +const PARSED_URL = Symbol('url'); + +/** + * Request class + * + * @param Mixed input Url or Request instance + * @param Object init Custom options + * @return Void + */ +class Request { + constructor(input) { + let init = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; + + let parsedURL; + + // normalize input + if (!(input instanceof Request)) { + 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); + } + + 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'); + } + + let inputBody = init.body != null ? init.body : input instanceof Request && input.body !== null ? clone(input) : null; + + Body.call(this, inputBody, { + timeout: init.timeout || input.timeout || 0, + size: init.size || input.size || 0 + }); + + // fetch spec options + this.method = method.toUpperCase(); + this.redirect = init.redirect || input.redirect || 'follow'; + this.headers = new Headers(init.headers || input.headers || {}); + + if (init.body != null) { + const contentType = extractContentType(this); + if (contentType !== null && !this.headers.has('Content-Type')) { + this.headers.append('Content-Type', contentType); + } + } + + // 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; + + this[PARSED_URL] = parsedURL; + Object.defineProperty(this, Symbol.toStringTag, { + value: 'Request', + writable: false, + enumerable: false, + configurable: true + }); + } + + get url() { + return format_url(this[PARSED_URL]); + } + + /** + * Clone this request + * + * @return Request + */ + clone() { + return new Request(this); + } +} + +Body.mixIn(Request.prototype); + +Object.defineProperty(Request.prototype, Symbol.toStringTag, { + value: 'RequestPrototype', + writable: false, + enumerable: false, + configurable: true +}); + +function getNodeRequestOptions(request) { + const parsedURL = request[PARSED_URL]; + const headers = new Headers(request.headers); + + // fetch step 3 + if (!headers.has('Accept')) { + headers.set('Accept', '*/*'); + } + + // Basic fetch + if (!parsedURL.protocol || !parsedURL.hostname) { + throw new TypeError('Only absolute URLs are supported'); + } + + if (!/^https?:$/.test(parsedURL.protocol)) { + throw new TypeError('Only HTTP(S) protocols are supported'); + } + + // HTTP-network-or-cache fetch steps 5-9 + let contentLengthValue = null; + if (request.body == null && /^(POST|PUT)$/i.test(request.method)) { + contentLengthValue = '0'; + } + if (request.body != null) { + const totalBytes = getTotalBytes(request); + if (typeof totalBytes === 'number') { + contentLengthValue = String(totalBytes); + } + } + if (contentLengthValue) { + headers.set('Content-Length', contentLengthValue); + } + + // HTTP-network-or-cache fetch step 12 + if (!headers.has('User-Agent')) { + headers.set('User-Agent', 'node-fetch/1.0 (+https://github.com/bitinn/node-fetch)'); + } + + // HTTP-network-or-cache fetch step 16 + if (request.compress) { + headers.set('Accept-Encoding', 'gzip,deflate'); + } + if (!headers.has('Connection') && !request.agent) { + headers.set('Connection', 'close'); + } + + // HTTP-network fetch step 4 + // chunked encoding is handled by Node.js + + return Object.assign({}, parsedURL, { + method: request.method, + headers: headers.raw(), + agent: request.agent + }); +} + +/** + * index.js + * + * a request API compatible with window.fetch + */ + +const http = require('http'); +const https = require('https'); + +var _require = require('stream'); + +const PassThrough = _require.PassThrough; + +var _require2 = require('url'); + +const resolve_url = _require2.resolve; + +const zlib = require('zlib'); + +/** + * Fetch function + * + * @param Mixed url Absolute url or Request instance + * @param Object opts Fetch options + * @return Promise + */ +function fetch(url, opts) { + + // allow custom promise + if (!fetch.Promise) { + throw new Error('native promise missing, set fetch.Promise to your favorite alternative'); + } + + Body.Promise = fetch.Promise; + + // wrap http.request into fetch + return new fetch.Promise(function (resolve, reject) { + // build request object + const request = new Request(url, opts); + const options = getNodeRequestOptions(request); + + const send = (options.protocol === 'https:' ? https : http).request; + + // 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]; + } + + // send request + const req = send(options); + let reqTimeout; + + if (request.timeout) { + req.once('socket', function (socket) { + reqTimeout = setTimeout(function () { + req.abort(); + reject(new FetchError(`network timeout at: ${request.url}`, 'request-timeout')); + }, request.timeout); + }); + } + + req.on('error', function (err) { + clearTimeout(reqTimeout); + reject(new FetchError(`request to ${request.url} failed, reason: ${err.message}`, 'system', err)); + }); + + req.on('response', function (res) { + clearTimeout(reqTimeout); + + // handle 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 (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: ${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) && request.method === 'POST') { + request.method = 'GET'; + request.body = null; + request.headers.delete('content-length'); + } + + request.counter++; + + resolve(fetch(resolve_url(request.url, res.headers.location), request)); + return; + } + + // normalize location header for manual redirect mode + 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 (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: request.url, + status: res.statusCode, + statusText: res.statusMessage, + headers: headers, + size: request.size, + timeout: request.timeout + }; + + // HTTP-network fetch step 16.1.2 + const codings = headers.get('Content-Encoding'); + + // HTTP-network fetch step 16.1.3: handle content codings + + // in following scenarios we ignore compression support + // 1. compression support is disabled + // 2. HEAD request + // 3. no Content-Encoding header + // 4. no content response (204) + // 5. content not modified response (304) + if (!request.compress || request.method === 'HEAD' || codings === null || res.statusCode === 204 || res.statusCode === 304) { + resolve(new Response(body, response_options)); + return; + } + + // For Node v6+ + // Be less strict when decoding compressed responses, since sometimes + // servers send slightly invalid responses that are still accepted + // by common browsers. + // Always using Z_SYNC_FLUSH is what cURL does. + const zlibOptions = { + flush: zlib.Z_SYNC_FLUSH, + finishFlush: zlib.Z_SYNC_FLUSH + }; + + // for gzip + if (codings == 'gzip' || codings == 'x-gzip') { + body = body.pipe(zlib.createGunzip(zlibOptions)); + resolve(new Response(body, response_options)); + return; + } + + // for deflate + if (codings == 'deflate' || codings == 'x-deflate') { + // handle the infamous raw deflate response from old servers + // a hack for old IIS and Apache servers + const raw = res.pipe(new PassThrough()); + raw.once('data', function (chunk) { + // see http://stackoverflow.com/questions/37519828 + if ((chunk[0] & 0x0F) === 0x08) { + body = body.pipe(zlib.createInflate()); + } else { + body = body.pipe(zlib.createInflateRaw()); + } + resolve(new Response(body, response_options)); + }); + return; + } + + // otherwise, use response as-is + resolve(new Response(body, response_options)); + }); + + writeToStream(req, request); + }); +} + +/** + * Redirect code matching + * + * @param Number code Status code + * @return Boolean + */ +fetch.isRedirect = function (code) { + return code === 301 || code === 302 || code === 303 || code === 307 || code === 308; +}; + +// expose Promise +fetch.Promise = global.Promise; + +module.exports = exports = fetch; +exports.Headers = Headers; +exports.Request = Request; +exports.Response = Response; +exports.FetchError = FetchError; diff --git a/utilities/hash_script/node_modules/node-fetch/lib/request.js b/utilities/hash_script/node_modules/node-fetch/lib/request.js new file mode 100644 index 0000000..1a29c29 --- /dev/null +++ b/utilities/hash_script/node_modules/node-fetch/lib/request.js @@ -0,0 +1,75 @@ + +/** + * request.js + * + * Request class contains server only options + */ + +var parse_url = require('url').parse; +var Headers = require('./headers'); +var Body = require('./body'); + +module.exports = Request; + +/** + * Request class + * + * @param Mixed input Url or Request instance + * @param Object init Custom options + * @return Void + */ +function Request(input, init) { + var 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 init + init = init || {}; + + // fetch spec options + this.method = init.method || input.method || 'GET'; + this.redirect = init.redirect || input.redirect || 'follow'; + this.headers = new Headers(init.headers || input.headers || {}); + this.url = url; + + // 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; +} + +Request.prototype = Object.create(Body.prototype); + +/** + * Clone this request + * + * @return Request + */ +Request.prototype.clone = function() { + return new Request(this); +}; diff --git a/utilities/hash_script/node_modules/node-fetch/lib/response.js b/utilities/hash_script/node_modules/node-fetch/lib/response.js new file mode 100644 index 0000000..f96aa85 --- /dev/null +++ b/utilities/hash_script/node_modules/node-fetch/lib/response.js @@ -0,0 +1,50 @@ + +/** + * response.js + * + * Response class provides content decoding + */ + +var http = require('http'); +var Headers = require('./headers'); +var Body = require('./body'); + +module.exports = Response; + +/** + * Response class + * + * @param Stream body Readable stream + * @param Object opts Response options + * @return Void + */ +function Response(body, opts) { + + opts = opts || {}; + + 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; + + Body.call(this, body, opts); + +} + +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/utilities/hash_script/node_modules/node-fetch/package.json b/utilities/hash_script/node_modules/node-fetch/package.json new file mode 100644 index 0000000..dcb8f7b --- /dev/null +++ b/utilities/hash_script/node_modules/node-fetch/package.json @@ -0,0 +1,110 @@ +{ + "_args": [ + [ + { + "raw": "node-fetch@^1.7.2", + "scope": null, + "escapedName": "node-fetch", + "name": "node-fetch", + "rawSpec": "^1.7.2", + "spec": ">=1.7.2 <2.0.0", + "type": "range" + }, + "/home/nate/Desktop/tar_JDkx6AzcE" + ] + ], + "_from": "node-fetch@>=1.7.2 <2.0.0", + "_id": "node-fetch@1.7.2", + "_inCache": true, + "_location": "/node-fetch", + "_nodeVersion": "8.2.1", + "_npmOperationalInternal": { + "host": "s3://npm-registry-packages", + "tmp": "tmp/node-fetch-1.7.2.tgz_1502191381219_0.664517188211903" + }, + "_npmUser": { + "name": "bitinn", + "email": "bitinn@gmail.com" + }, + "_npmVersion": "5.3.0", + "_phantomChildren": {}, + "_requested": { + "raw": "node-fetch@^1.7.2", + "scope": null, + "escapedName": "node-fetch", + "name": "node-fetch", + "rawSpec": "^1.7.2", + "spec": ">=1.7.2 <2.0.0", + "type": "range" + }, + "_requiredBy": [ + "#USER", + "/" + ], + "_resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-1.7.2.tgz", + "_shasum": "c54e9aac57e432875233525f3c891c4159ffefd7", + "_shrinkwrap": null, + "_spec": "node-fetch@^1.7.2", + "_where": "/home/nate/Desktop/tar_JDkx6AzcE", + "author": { + "name": "David Frank" + }, + "bugs": { + "url": "https://github.com/bitinn/node-fetch/issues" + }, + "dependencies": { + "encoding": "^0.1.11", + "is-stream": "^1.0.1" + }, + "description": "A light-weight module that brings window.fetch to node.js and io.js", + "devDependencies": { + "bluebird": "^3.3.4", + "chai": "^3.5.0", + "chai-as-promised": "^5.2.0", + "codecov": "^1.0.1", + "form-data": ">=1.0.0", + "istanbul": "^0.4.2", + "mocha": "^2.1.0", + "parted": "^0.1.1", + "promise": "^7.1.1", + "resumer": "0.0.0" + }, + "directories": {}, + "dist": { + "integrity": "sha512-xZZUq2yDhKMIn/UgG5q//IZSNLJIwW2QxS14CNH5spuiXkITM2pUitjdq58yLSaU7m4M0wBNaM2Gh/ggY4YJig==", + "shasum": "c54e9aac57e432875233525f3c891c4159ffefd7", + "tarball": "https://registry.npmjs.org/node-fetch/-/node-fetch-1.7.2.tgz" + }, + "gitHead": "3c9b64caf5c84f96052078f18d91c9e12bd5f5c6", + "homepage": "https://github.com/bitinn/node-fetch", + "keywords": [ + "fetch", + "http", + "promise" + ], + "license": "MIT", + "main": "index.js", + "maintainers": [ + { + "name": "bitinn", + "email": "bitinn@gmail.com" + }, + { + "name": "timothygu", + "email": "timothygu99@gmail.com" + } + ], + "name": "node-fetch", + "optionalDependencies": {}, + "readme": "ERROR: No README data found!", + "repository": { + "type": "git", + "url": "git+https://github.com/bitinn/node-fetch.git" + }, + "scripts": { + "coverage": "istanbul cover _mocha --report lcovonly -- -R spec test/test.js && codecov", + "report": "istanbul cover _mocha -- -R spec test/test.js", + "test": "mocha test/test.js" + }, + "version": "1.7.2" +} diff --git a/utilities/hash_script/node_modules/node-fetch/test/dummy.txt b/utilities/hash_script/node_modules/node-fetch/test/dummy.txt new file mode 100644 index 0000000..5ca5191 --- /dev/null +++ b/utilities/hash_script/node_modules/node-fetch/test/dummy.txt @@ -0,0 +1 @@ +i am a dummy \ No newline at end of file diff --git a/utilities/hash_script/node_modules/node-fetch/test/server.js b/utilities/hash_script/node_modules/node-fetch/test/server.js new file mode 100644 index 0000000..5b1b3b9 --- /dev/null +++ b/utilities/hash_script/node_modules/node-fetch/test/server.js @@ -0,0 +1,340 @@ + +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'; + // node 8 default keepalive timeout is 5000ms + // make it shorter here as we want to close server quickly at the end of tests + this.server.keepAliveTimeout = 1000; + 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); + }); + } + + 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'); + setTimeout(function() { + res.end('test'); + }, 1000); + } + + if (p === '/cookie') { + res.statusCode = 200; + res.setHeader('Set-Cookie', ['a=1', 'b=1']); + res.end('cookie'); + } + + 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 === '/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); + } + 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); + } + res.end(convert('中文', 'gbk')); + } + + if (p === '/redirect/301') { + res.statusCode = 301; + res.setHeader('Location', '/inspect'); + res.end(); + } + + if (p === '/redirect/302') { + res.statusCode = 302; + res.setHeader('Location', '/inspect'); + res.end(); + } + + if (p === '/redirect/303') { + res.statusCode = 303; + res.setHeader('Location', '/inspect'); + res.end(); + } + + if (p === '/redirect/307') { + res.statusCode = 307; + res.setHeader('Location', '/inspect'); + res.end(); + } + + if (p === '/redirect/308') { + res.statusCode = 308; + res.setHeader('Location', '/inspect'); + res.end(); + } + + if (p === '/redirect/chain') { + res.statusCode = 301; + res.setHeader('Location', '/redirect/301'); + res.end(); + } + + 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'); + 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 === '/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); + } +} diff --git a/utilities/hash_script/node_modules/node-fetch/test/test.js b/utilities/hash_script/node_modules/node-fetch/test/test.js new file mode 100644 index 0000000..284b263 --- /dev/null +++ b/utilities/hash_script/node_modules/node-fetch/test/test.js @@ -0,0 +1,1487 @@ + +// 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'); + +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'); +// test with native promise on node 0.11, and bluebird for node 0.10 +fetch.Promise = fetch.Promise || bluebird; + +var url, opts, local, base; + +describe('node-fetch', function() { + + before(function(done) { + local = new TestServer(); + base = 'http://' + local.hostname + ':' + local.port; + local.start(done); + }); + + after(function(done) { + local.stop(done); + }); + + it('should return a promise', function() { + url = 'http://example.com/'; + var 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; + fetch.Promise = then; + expect(fetch(url)).to.be.an.instanceof(then); + expect(fetch(url)).to.not.be.an.instanceof(bluebird); + fetch.Promise = old; + }); + + it('should throw error when no promise implementation are found', function() { + url = 'http://example.com/'; + var old = fetch.Promise; + fetch.Promise = undefined; + expect(function() { + fetch(url) + }).to.throw(Error); + fetch.Promise = old; + }); + + 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); + }); + + it('should reject with error if url is protocol relative', function() { + url = '//example.com/'; + return expect(fetch(url)).to.eventually.be.rejectedWith(Error); + }); + + it('should reject with error if url is relative path', function() { + url = '/some/path'; + return expect(fetch(url)).to.eventually.be.rejectedWith(Error); + }); + + it('should reject with error if protocol is unsupported', function() { + url = 'ftp://example.com/'; + return expect(fetch(url)).to.eventually.be.rejectedWith(Error); + }); + + it('should reject with error on network failure', function() { + url = 'http://localhost:50000/'; + return expect(fetch(url)).to.eventually.be.rejected + .and.be.an.instanceOf(FetchError) + .and.include({ type: 'system', code: 'ECONNREFUSED', errno: 'ECONNREFUSED' }); + }); + + it('should resolve into response', function() { + url = base + '/hello'; + return fetch(url).then(function(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); + expect(res.bodyUsed).to.be.false; + + expect(res.url).to.equal(url); + expect(res.ok).to.be.true; + expect(res.status).to.equal(200); + expect(res.statusText).to.equal('OK'); + }); + }); + + it('should accept plain text response', function() { + url = base + '/plain'; + return fetch(url).then(function(res) { + expect(res.headers.get('content-type')).to.equal('text/plain'); + return res.text().then(function(result) { + expect(res.bodyUsed).to.be.true; + expect(result).to.be.a('string'); + expect(result).to.equal('text'); + }); + }); + }); + + it('should accept html response (like plain text)', function() { + url = base + '/html'; + return fetch(url).then(function(res) { + expect(res.headers.get('content-type')).to.equal('text/html'); + return res.text().then(function(result) { + expect(res.bodyUsed).to.be.true; + expect(result).to.be.a('string'); + expect(result).to.equal(''); + }); + }); + }); + + it('should accept json response', function() { + url = base + '/json'; + return fetch(url).then(function(res) { + expect(res.headers.get('content-type')).to.equal('application/json'); + return res.json().then(function(result) { + expect(res.bodyUsed).to.be.true; + expect(result).to.be.an('object'); + expect(result).to.deep.equal({ name: 'value' }); + }); + }); + }); + + it('should send request with custom headers', function() { + url = base + '/inspect'; + opts = { + headers: { 'x-custom-header': 'abc' } + }; + return fetch(url, opts).then(function(res) { + return res.json(); + }).then(function(res) { + expect(res.headers['x-custom-header']).to.equal('abc'); + }); + }); + + it('should accept headers instance', function() { + url = base + '/inspect'; + opts = { + headers: new Headers({ 'x-custom-header': 'abc' }) + }; + return fetch(url, opts).then(function(res) { + return res.json(); + }).then(function(res) { + expect(res.headers['x-custom-header']).to.equal('abc'); + }); + }); + + it('should accept custom host header', function() { + url = base + '/inspect'; + opts = { + headers: { + host: 'example.com' + } + }; + return fetch(url, opts).then(function(res) { + return res.json(); + }).then(function(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'); + 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'); + 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'); + 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'); + 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'); + 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'); + expect(res.status).to.equal(200); + }); + }); + + it('should follow POST request redirect code 301 with GET', function() { + url = base + '/redirect/301'; + opts = { + method: 'POST' + , body: 'a=1' + }; + return fetch(url, opts).then(function(res) { + expect(res.url).to.equal(base + '/inspect'); + expect(res.status).to.equal(200); + return res.json().then(function(result) { + expect(result.method).to.equal('GET'); + expect(result.body).to.equal(''); + }); + }); + }); + + it('should follow POST request redirect code 302 with GET', function() { + url = base + '/redirect/302'; + opts = { + method: 'POST' + , body: 'a=1' + }; + return fetch(url, opts).then(function(res) { + expect(res.url).to.equal(base + '/inspect'); + expect(res.status).to.equal(200); + return res.json().then(function(result) { + expect(result.method).to.equal('GET'); + expect(result.body).to.equal(''); + }); + }); + }); + + it('should follow redirect code 303 with GET', function() { + url = base + '/redirect/303'; + opts = { + method: 'PUT' + , body: 'a=1' + }; + return fetch(url, opts).then(function(res) { + expect(res.url).to.equal(base + '/inspect'); + expect(res.status).to.equal(200); + return res.json().then(function(result) { + expect(result.method).to.equal('GET'); + expect(result.body).to.equal(''); + }); + }); + }); + + it('should obey maximum redirect, reject case', function() { + url = base + '/redirect/chain'; + opts = { + follow: 1 + } + return expect(fetch(url, opts)).to.eventually.be.rejected + .and.be.an.instanceOf(FetchError) + .and.have.property('type', 'max-redirect'); + }); + + it('should obey redirect chain, resolve case', function() { + url = base + '/redirect/chain'; + opts = { + follow: 2 + } + return fetch(url, opts).then(function(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'; + opts = { + follow: 0 + } + return expect(fetch(url, opts)).to.eventually.be.rejected + .and.be.an.instanceOf(FetchError) + .and.have.property('type', 'max-redirect'); + }); + + it('should support redirect mode, manual flag', function() { + url = base + '/redirect/301'; + opts = { + redirect: 'manual' + }; + return fetch(url, opts).then(function(res) { + expect(res.url).to.equal(url); + expect(res.status).to.equal(301); + expect(res.headers.get('location')).to.equal(base + '/inspect'); + }); + }); + + it('should support redirect mode, error flag', function() { + url = base + '/redirect/301'; + opts = { + redirect: 'error' + }; + return expect(fetch(url, opts)).to.eventually.be.rejected + .and.be.an.instanceOf(FetchError) + .and.have.property('type', 'no-redirect'); + }); + + it('should support redirect mode, manual flag when there is no redirect', function() { + url = base + '/hello'; + opts = { + redirect: 'manual' + }; + return fetch(url, opts).then(function(res) { + expect(res.url).to.equal(url); + expect(res.status).to.equal(200); + expect(res.headers.get('location')).to.be.null; + }); + }); + + it('should follow redirect code 301 and keep existing headers', function() { + 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 res.json(); + }).then(function(res) { + expect(res.headers['x-custom-header']).to.equal('abc'); + }); + }); + + it('should reject broken redirect', function() { + 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'; + opts = { + redirect: 'manual' + }; + return fetch(url, opts).then(function(res) { + expect(res.url).to.equal(url); + expect(res.status).to.equal(301); + expect(res.headers.get('location')).to.be.null; + }); + }); + + it('should handle client-error response', function() { + url = base + '/error/400'; + return fetch(url).then(function(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) { + expect(res.bodyUsed).to.be.true; + expect(result).to.be.a('string'); + expect(result).to.equal('client error'); + }); + }); + }); + + it('should handle server-error response', function() { + url = base + '/error/500'; + return fetch(url).then(function(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) { + expect(res.bodyUsed).to.be.true; + expect(result).to.be.a('string'); + expect(result).to.equal('server error'); + }); + }); + }); + + it('should handle network-error response', function() { + url = base + '/error/reset'; + return expect(fetch(url)).to.eventually.be.rejected + .and.be.an.instanceOf(FetchError) + .and.have.property('code', 'ECONNRESET'); + }); + + it('should handle DNS-error response', function() { + url = 'http://domain.invalid'; + return expect(fetch(url)).to.eventually.be.rejected + .and.be.an.instanceOf(FetchError) + .and.have.property('code', 'ENOTFOUND'); + }); + + it('should reject invalid json response', function() { + url = base + '/error/json'; + return fetch(url).then(function(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) { + 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) { + expect(result).to.be.a('string'); + expect(result).to.be.empty; + }); + }); + }); + + it('should throw on no-content json response', function() { + url = base + '/no-content'; + return fetch(url).then(function(res) { + return expect(res.json()).to.eventually.be.rejectedWith(FetchError); + }); + }); + + it('should handle no content response with gzip encoding', function() { + url = base + '/no-content/gzip'; + return fetch(url).then(function(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) { + expect(result).to.be.a('string'); + expect(result).to.be.empty; + }); + }); + }); + + it('should handle not modified response', function() { + url = base + '/not-modified'; + return fetch(url).then(function(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) { + expect(result).to.be.a('string'); + expect(result).to.be.empty; + }); + }); + }); + + it('should handle not modified response with gzip encoding', function() { + url = base + '/not-modified/gzip'; + return fetch(url).then(function(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) { + expect(result).to.be.a('string'); + expect(result).to.be.empty; + }); + }); + }); + + it('should decompress gzip response', function() { + url = base + '/gzip'; + return fetch(url).then(function(res) { + expect(res.headers.get('content-type')).to.equal('text/plain'); + return res.text().then(function(result) { + expect(result).to.be.a('string'); + expect(result).to.equal('hello world'); + }); + }); + }); + + it('should decompress deflate response', function() { + url = base + '/deflate'; + return fetch(url).then(function(res) { + expect(res.headers.get('content-type')).to.equal('text/plain'); + return res.text().then(function(result) { + expect(result).to.be.a('string'); + expect(result).to.equal('hello world'); + }); + }); + }); + + it('should decompress deflate raw response from old apache server', function() { + url = base + '/deflate-raw'; + return fetch(url).then(function(res) { + expect(res.headers.get('content-type')).to.equal('text/plain'); + return res.text().then(function(result) { + expect(result).to.be.a('string'); + expect(result).to.equal('hello world'); + }); + }); + }); + + it('should skip decompression if unsupported', function() { + url = base + '/sdch'; + return fetch(url).then(function(res) { + expect(res.headers.get('content-type')).to.equal('text/plain'); + return res.text().then(function(result) { + expect(result).to.be.a('string'); + expect(result).to.equal('fake sdch string'); + }); + }); + }); + + it('should reject if response compression is invalid', function() { + url = base + '/invalid-content-encoding'; + return fetch(url).then(function(res) { + expect(res.headers.get('content-type')).to.equal('text/plain'); + return expect(res.text()).to.eventually.be.rejected + .and.be.an.instanceOf(FetchError) + .and.have.property('code', 'Z_DATA_ERROR'); + }); + }); + + it('should allow disabling auto decompression', function() { + url = base + '/gzip'; + opts = { + compress: false + }; + return fetch(url, opts).then(function(res) { + expect(res.headers.get('content-type')).to.equal('text/plain'); + return res.text().then(function(result) { + expect(result).to.be.a('string'); + expect(result).to.not.equal('hello world'); + }); + }); + }); + + it('should allow custom timeout', function() { + this.timeout(500); + url = base + '/timeout'; + opts = { + timeout: 100 + }; + return expect(fetch(url, opts)).to.eventually.be.rejected + .and.be.an.instanceOf(FetchError) + .and.have.property('type', 'request-timeout'); + }); + + it('should allow custom timeout on response body', function() { + this.timeout(500); + url = base + '/slow'; + opts = { + timeout: 100 + }; + return fetch(url, opts).then(function(res) { + expect(res.ok).to.be.true; + return expect(res.text()).to.eventually.be.rejected + .and.be.an.instanceOf(FetchError) + .and.have.property('type', 'body-timeout'); + }); + }); + + it('should clear internal timeout on fetch response', function (done) { + this.timeout(1000); + spawn('node', ['-e', 'require("./")("' + base + '/hello", { timeout: 5000 })']) + .on('exit', function () { + 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 () { + 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 () { + done(); + }); + }); + + it('should allow POST request', function() { + url = base + '/inspect'; + opts = { + method: 'POST' + }; + return fetch(url, opts).then(function(res) { + return res.json(); + }).then(function(res) { + expect(res.method).to.equal('POST'); + expect(res.headers['transfer-encoding']).to.be.undefined; + expect(res.headers['content-length']).to.equal('0'); + }); + }); + + it('should allow POST request with string body', function() { + url = base + '/inspect'; + opts = { + method: 'POST' + , body: 'a=1' + }; + return fetch(url, opts).then(function(res) { + return res.json(); + }).then(function(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-length']).to.equal('3'); + }); + }); + + it('should allow POST request with buffer body', function() { + url = base + '/inspect'; + opts = { + method: 'POST' + , body: new Buffer('a=1', 'utf-8') + }; + return fetch(url, opts).then(function(res) { + return res.json(); + }).then(function(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['content-length']).to.be.undefined; + }); + }); + + it('should allow POST request with readable stream as body', function() { + var body = resumer().queue('a=1').end(); + body = body.pipe(new stream.PassThrough()); + + url = base + '/inspect'; + opts = { + method: 'POST' + , body: body + }; + return fetch(url, opts).then(function(res) { + return res.json(); + }).then(function(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['content-length']).to.be.undefined; + }); + }); + + it('should allow POST request with form-data as body', function() { + var form = new FormData(); + form.append('a','1'); + + url = base + '/multipart'; + opts = { + method: 'POST' + , body: form + }; + return fetch(url, opts).then(function(res) { + return res.json(); + }).then(function(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'); + expect(res.body).to.equal('a=1'); + }); + }); + + it('should allow POST request with form-data using stream as body', function() { + var form = new FormData(); + form.append('my_field', fs.createReadStream('test/dummy.txt')); + + url = base + '/multipart'; + opts = { + method: 'POST' + , body: form + }; + + return fetch(url, opts).then(function(res) { + return res.json(); + }).then(function(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; + expect(res.body).to.contain('my_field='); + }); + }); + + it('should allow POST request with form-data as body and custom headers', function() { + var form = new FormData(); + form.append('a','1'); + + var headers = form.getHeaders(); + headers['b'] = '2'; + + url = base + '/multipart'; + opts = { + method: 'POST' + , body: form + , headers: headers + }; + return fetch(url, opts).then(function(res) { + return res.json(); + }).then(function(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'); + expect(res.headers.b).to.equal('2'); + expect(res.body).to.equal('a=1'); + }); + }); + + it('should allow POST request with object body', function() { + 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 res.json(); + }).then(function(res) { + expect(res.method).to.equal('POST'); + expect(res.body).to.equal('[object Object]'); + }); + }); + + it('should allow PUT request', function() { + url = base + '/inspect'; + opts = { + method: 'PUT' + , body: 'a=1' + }; + return fetch(url, opts).then(function(res) { + return res.json(); + }).then(function(res) { + expect(res.method).to.equal('PUT'); + expect(res.body).to.equal('a=1'); + }); + }); + + it('should allow DELETE request', function() { + url = base + '/inspect'; + opts = { + method: 'DELETE' + }; + return fetch(url, opts).then(function(res) { + return res.json(); + }).then(function(res) { + expect(res.method).to.equal('DELETE'); + }); + }); + + it('should allow POST request with string body', function() { + url = base + '/inspect'; + opts = { + method: 'POST' + , body: 'a=1' + }; + return fetch(url, opts).then(function(res) { + return res.json(); + }).then(function(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-length']).to.equal('3'); + }); + }); + + it('should allow DELETE request with string body', function() { + url = base + '/inspect'; + opts = { + method: 'DELETE' + , body: 'a=1' + }; + return fetch(url, opts).then(function(res) { + return res.json(); + }).then(function(res) { + expect(res.method).to.equal('DELETE'); + expect(res.body).to.equal('a=1'); + expect(res.headers['transfer-encoding']).to.be.undefined; + expect(res.headers['content-length']).to.equal('3'); + }); + }); + + it('should allow PATCH request', function() { + url = base + '/inspect'; + opts = { + method: 'PATCH' + , body: 'a=1' + }; + return fetch(url, opts).then(function(res) { + return res.json(); + }).then(function(res) { + expect(res.method).to.equal('PATCH'); + expect(res.body).to.equal('a=1'); + }); + }); + + it('should allow HEAD request', function() { + url = base + '/hello'; + opts = { + method: 'HEAD' + }; + return fetch(url, opts).then(function(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) { + expect(text).to.equal(''); + }); + }); + + it('should allow HEAD request with content-encoding header', function() { + url = base + '/error/404'; + opts = { + method: 'HEAD' + }; + return fetch(url, opts).then(function(res) { + expect(res.status).to.equal(404); + expect(res.headers.get('content-encoding')).to.equal('gzip'); + return res.text(); + }).then(function(text) { + expect(text).to.equal(''); + }); + }); + + it('should allow OPTIONS request', function() { + url = base + '/options'; + opts = { + method: 'OPTIONS' + }; + return fetch(url, opts).then(function(res) { + expect(res.status).to.equal(200); + expect(res.statusText).to.equal('OK'); + expect(res.headers.get('allow')).to.equal('GET, HEAD, OPTIONS'); + expect(res.body).to.be.an.instanceof(stream.Transform); + }); + }); + + it('should reject decoding body twice', function() { + url = base + '/plain'; + return fetch(url).then(function(res) { + expect(res.headers.get('content-type')).to.equal('text/plain'); + return res.text().then(function(result) { + expect(res.bodyUsed).to.be.true; + return expect(res.text()).to.eventually.be.rejectedWith(Error); + }); + }); + }); + + it('should support maximum response size, multiple chunk', function() { + url = base + '/size/chunk'; + opts = { + size: 5 + }; + return fetch(url, opts).then(function(res) { + expect(res.status).to.equal(200); + expect(res.headers.get('content-type')).to.equal('text/plain'); + return expect(res.text()).to.eventually.be.rejected + .and.be.an.instanceOf(FetchError) + .and.have.property('type', 'max-size'); + }); + }); + + it('should support maximum response size, single chunk', function() { + url = base + '/size/long'; + opts = { + size: 5 + }; + return fetch(url, opts).then(function(res) { + expect(res.status).to.equal(200); + expect(res.headers.get('content-type')).to.equal('text/plain'); + return expect(res.text()).to.eventually.be.rejected + .and.be.an.instanceOf(FetchError) + .and.have.property('type', 'max-size'); + }); + }); + + it('should support encoding decode, xml dtd detect', function() { + url = base + '/encoding/euc-jp'; + return fetch(url).then(function(res) { + expect(res.status).to.equal(200); + return res.text().then(function(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) { + expect(res.status).to.equal(200); + return res.text().then(function(result) { + expect(result).to.equal('
日本語
'); + }); + }); + }); + + it('should support encoding decode, html5 detect', function() { + url = base + '/encoding/gbk'; + return fetch(url).then(function(res) { + expect(res.status).to.equal(200); + return res.text().then(function(result) { + expect(result).to.equal('
中文
'); + }); + }); + }); + + it('should support encoding decode, html4 detect', function() { + url = base + '/encoding/gb2312'; + return fetch(url).then(function(res) { + expect(res.status).to.equal(200); + return res.text().then(function(result) { + expect(result).to.equal('
中文
'); + }); + }); + }); + + it('should default to utf8 encoding', function() { + url = base + '/encoding/utf8'; + return fetch(url).then(function(res) { + expect(res.status).to.equal(200); + expect(res.headers.get('content-type')).to.be.null; + return res.text().then(function(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) { + expect(res.status).to.equal(200); + return res.text().then(function(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) { + expect(res.status).to.equal(200); + return res.text().then(function(result) { + expect(result).to.equal('中文'); + }); + }); + }); + + it('should support chunked encoding, html4 detect', function() { + url = base + '/encoding/chunked'; + return fetch(url).then(function(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 + '
日本語
'); + }); + }); + }); + + it('should only do encoding detection up to 1024 bytes', function() { + url = base + '/encoding/invalid'; + return fetch(url).then(function(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 + '中文'); + }); + }); + }); + + it('should allow piping response body as stream', function(done) { + url = base + '/hello'; + fetch(url).then(function(res) { + expect(res.body).to.be.an.instanceof(stream.Transform); + res.body.on('data', function(chunk) { + if (chunk === null) { + return; + } + expect(chunk.toString()).to.equal('world'); + }); + res.body.on('end', function() { + 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(); + expect(res.body).to.be.an.instanceof(stream.Transform); + expect(r1.body).to.be.an.instanceof(stream.Transform); + res.body.on('data', function(chunk) { + if (chunk === null) { + return; + } + expect(chunk.toString()).to.equal('world'); + }); + res.body.on('end', function() { + counter++; + if (counter == 2) { + done(); + } + }); + r1.body.on('data', function(chunk) { + if (chunk === null) { + return; + } + expect(chunk.toString()).to.equal('world'); + }); + r1.body.on('end', function() { + counter++; + if (counter == 2) { + done(); + } + }); + }); + }); + + 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) { + expect(results[0]).to.deep.equal({name: 'value'}); + expect(results[1]).to.equal('{"name":"value"}'); + }); + }); + }); + + 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) { + expect(result).to.deep.equal({name: 'value'}); + return r1.text().then(function(result) { + expect(result).to.equal('{"name":"value"}'); + }); + }); + }); + }); + + 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) { + expect(result).to.equal('{"name":"value"}'); + return res.json().then(function(result) { + expect(result).to.deep.equal({name: 'value'}); + }); + }); + }); + }); + + 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(); + }).to.throw(Error); + }); + }) + }); + + it('should allow get all responses of a header', function() { + url = base + '/cookie'; + return fetch(url).then(function(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']); + expect(res.headers.getAll('Set-Cookie')).to.deep.equal(['a=1', 'b=1']); + }); + }); + + it('should allow iterating through all headers', function() { + var headers = new Headers({ + a: 1 + , b: [2, 3] + , c: [4] + }); + expect(headers).to.have.property('forEach'); + + var result = []; + headers.forEach(function(val, key) { + result.push([key, val]); + }); + + expected = [ + ["a", "1"] + , ["b", "2"] + , ["b", "3"] + , ["c", "4"] + ]; + expect(result).to.deep.equal(expected); + }); + + it('should allow deleting header', function() { + url = base + '/cookie'; + return fetch(url).then(function(res) { + res.headers.delete('set-cookie'); + expect(res.headers.get('set-cookie')).to.be.null; + expect(res.headers.getAll('set-cookie')).to.be.empty; + }); + }); + + it('should send request with connection keep-alive if agent is provided', function() { + url = base + '/inspect'; + opts = { + agent: new http.Agent({ + keepAlive: true + }) + }; + return fetch(url, opts).then(function(res) { + return res.json(); + }).then(function(res) { + expect(res.headers['connection']).to.equal('keep-alive'); + }); + }); + + it('should ignore unsupported attributes while reading headers', function() { + var FakeHeader = function() {}; + // prototypes are ignored + FakeHeader.prototype.z = 'fake'; + + var 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; + res.j = NaN; + res.k = true; + res.l = false; + res.m = new Buffer('test'); + + var h1 = new Headers(res); + + 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(h1._headers['e']).to.include('1'); + expect(h1._headers['f']).to.include('1'); + expect(h1._headers['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(h1._headers['z']).to.be.undefined; + }); + + it('should wrap headers', function() { + var h1 = new Headers({ + a: '1' + }); + + var h2 = new Headers(h1); + h2.set('b', '1'); + + var h3 = new Headers(h2); + h3.append('a', '2'); + + expect(h1._headers['a']).to.include('1'); + expect(h1._headers['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(h3._headers['a']).to.include('1'); + expect(h3._headers['a']).to.include('2'); + expect(h3._headers['b']).to.include('1'); + }); + + it('should support fetch with Request instance', function() { + url = base + '/hello'; + var req = new Request(url); + return fetch(req).then(function(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'; + + var form = new FormData(); + form.append('a', '1'); + + var r1 = new Request(url, { + method: 'POST' + , follow: 1 + , body: form + }); + var r2 = new Request(r1, { + follow: 2 + }); + + expect(r2.url).to.equal(url); + expect(r2.method).to.equal('POST'); + // note that we didn't clone the body + expect(r2.body).to.equal(form); + expect(r1.follow).to.equal(1); + expect(r2.follow).to.equal(2); + expect(r1.counter).to.equal(0); + expect(r2.counter).to.equal(0); + }); + + it('should support overwrite Request instance', function() { + url = base + '/inspect'; + var req = new Request(url, { + method: 'POST' + , headers: { + a: '1' + } + }); + return fetch(req, { + method: 'GET' + , headers: { + a: '2' + } + }).then(function(res) { + return res.json(); + }).then(function(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(); + body = body.pipe(new stream.PassThrough()); + var res = new Response(body); + return res.text().then(function(result) { + expect(result).to.equal('a=1'); + }); + }); + + it('should support parsing headers in Response constructor', function() { + var res = new Response(null, { + headers: { + a: '1' + } + }); + expect(res.headers.get('a')).to.equal('1'); + }); + + it('should support text() method in Response constructor', function() { + var res = new Response('a=1'); + return res.text().then(function(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) { + 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) { + expect(result.toString()).to.equal('a=1'); + }); + }); + + it('should support clone() method in Response constructor', function() { + var body = resumer().queue('a=1').end(); + body = body.pipe(new stream.PassThrough()); + var res = new Response(body, { + headers: { + a: '1' + } + , url: base + , status: 346 + , statusText: 'production' + }); + var cl = res.clone(); + expect(cl.headers.get('a')).to.equal('1'); + expect(cl.url).to.equal(base); + expect(cl.status).to.equal(346); + expect(cl.statusText).to.equal('production'); + 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) { + expect(result).to.equal('a=1'); + }); + }); + + it('should support stream as body in Response constructor', function() { + var body = resumer().queue('a=1').end(); + body = body.pipe(new stream.PassThrough()); + var res = new Response(body); + return res.text().then(function(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) { + 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) { + expect(result).to.equal('a=1'); + }); + }); + + it('should default to 200 as status code', function() { + var 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, { + headers: { + a: '1' + } + }); + expect(req.url).to.equal(url); + expect(req.headers.get('a')).to.equal('1'); + }); + + it('should support text() method in Request constructor', function() { + url = base; + var req = new Request(url, { + body: 'a=1' + }); + expect(req.url).to.equal(url); + return req.text().then(function(result) { + expect(result).to.equal('a=1'); + }); + }); + + it('should support json() method in Request constructor', function() { + url = base; + var req = new Request(url, { + body: '{"a":1}' + }); + expect(req.url).to.equal(url); + return req.json().then(function(result) { + expect(result.a).to.equal(1); + }); + }); + + it('should support buffer() method in Request constructor', function() { + url = base; + var req = new Request(url, { + body: 'a=1' + }); + expect(req.url).to.equal(url); + return req.buffer().then(function(result) { + expect(result.toString()).to.equal('a=1'); + }); + }); + + it('should support arbitrary url in Request constructor', function() { + url = 'anything'; + var 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(); + body = body.pipe(new stream.PassThrough()); + var agent = new http.Agent(); + var req = new Request(url, { + body: body + , method: 'POST' + , redirect: 'manual' + , headers: { + b: '2' + } + , follow: 3 + , compress: false + , agent: agent + }); + var cl = req.clone(); + expect(cl.url).to.equal(url); + expect(cl.method).to.equal('POST'); + expect(cl.redirect).to.equal('manual'); + expect(cl.headers.get('b')).to.equal('2'); + expect(cl.follow).to.equal(3); + expect(cl.compress).to.equal(false); + expect(cl.method).to.equal('POST'); + expect(cl.counter).to.equal(0); + 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) { + 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'); + 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'); + systemError.code = 'ESOMEERROR'; + + var err = new FetchError('test message', 'test-error', systemError); + expect(err).to.be.an.instanceof(Error); + expect(err).to.be.an.instanceof(FetchError); + expect(err.name).to.equal('FetchError'); + expect(err.message).to.equal('test message'); + expect(err.type).to.equal('test-error'); + expect(err.code).to.equal('ESOMEERROR'); + expect(err.errno).to.equal('ESOMEERROR'); + }); + + it('should support https request', function() { + this.timeout(5000); + url = 'https://github.com/'; + opts = { + method: 'HEAD' + }; + return fetch(url, opts).then(function(res) { + expect(res.status).to.equal(200); + expect(res.ok).to.be.true; + }); + }); + +}); -- cgit v1.2.3