From 0959ca9739850bbd24e0721cc1296e7a0aa5c2bd Mon Sep 17 00:00:00 2001 From: David Frank Date: Fri, 13 Mar 2020 23:06:25 +0800 Subject: [PATCH] merge 3.x into master branch (#745) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: Migrate TypeScript types (#669) * style: Introduce linting via XO * fix: Fix tests * chore!: Drop support for nodejs 4 and 6 * chore: Fix Travis CI yml * Use old Babel (needs migration) * chore: lint everything * chore: Migrate to microbundle * Default response.statusText should be blank (#578) * fix: Use correct AbortionError message Signed-off-by: Richie Bendall * chore: Use modern @babel/register Signed-off-by: Richie Bendall * chore: Remove redundant packages Signed-off-by: Richie Bendall * chore: Readd form-data Signed-off-by: Richie Bendall * fix: Fix tests and force utf8-encoded urls Signed-off-by: Richie Bendall * lint index.js * Update devDependencies & ignore `test` directory in linter options * Remove unnecessary eslint-ignore comment * Update the `lint` script to run linter on every file * Remove unused const & unnecessary import * TypeScript: Fix Body.blob() wrong type (DefinitelyTyped/DefinitelyTyped#33721) * chore: Lint as part of the build process * fix: Convert Content-Encoding to lowercase (#672) * fix: Better object checks (#673) * Fix stream piping (#670) * chore: Remove useless check Signed-off-by: Richie Bendall * style: Fix lint Signed-off-by: Richie Bendall * style: Fix lint Signed-off-by: Richie Bendall * refactor: Modernise code Signed-off-by: Richie Bendall * chore: Ensure all files are properly included Signed-off-by: Richie Bendall * chore: Update deps and utf8 should be in dependencies Signed-off-by: Richie Bendall * test: Drop Node v4 from tests Signed-off-by: Richie Bendall * test: Modernise code Signed-off-by: Richie Bendall * chore: Move errors to seperate directory Signed-off-by: Richie Bendall * refactor: Add fetch-blob (#678) * feat: Migrate data uri integration Signed-off-by: Richie Bendall * Allow setting custom highWaterMark via node-fetch options (#386) (#671) * Expose highWaterMark option to body clone function * Add highWaterMark to responseOptions * Add highWaterMark as node-fetch-only option * a way to silently pass highWaterMark to clone * Chai helper * Server helper * Tests * Remove debug comments * Document highWaterMark option * Add TypeScript types for the new highWaterMark option * feat: Include system error in FetchError if one occurs (#654) * style: Add editorconfig Signed-off-by: Richie Bendall * chore!: Drop NodeJS v8 Signed-off-by: Richie Bendall * chore: Remove legacy code for node < 8 Signed-off-by: Richie Bendall * chore: Use proper checks for ArrayBuffer and AbortError Signed-off-by: Richie Bendall * chore: Use explicitly set error name in checks Signed-off-by: Richie Bendall * Propagate size and timeout to cloned response (#664) * Remove --save option as it isn't required anymore (#581) * Propagate size and timeout to cloned response Co-authored-by: Steve Moser Co-authored-by: Antoni Kepinski * Update Response types * Update devDependencies * feat: Fallback to blob type (Closes: #607) Signed-off-by: Richie Bendall * style: Update formatting Signed-off-by: Richie Bendall * style: Fix linting issues Signed-off-by: Richie Bendall * docs: Add info on patching the global object * docs: Added non-globalThis polyfill * Replace deprecated `url.resolve` with the new WHATWG URL * Update devDependencies * Format code in examples to use `xo` style * Verify examples with RunKit and edit them if necessary * Add information about TypeScript support * Document the new `highWaterMark` option * Add Discord badge & information about Open Collective * Style change * Edit acknowledgement & add "Team" section * fix table * Format example code to use xo style * chore: v3 release changelog * Add the recommended way to fix `highWaterMark` issues * docs: Add simple Runkit example * fix: Properly set the name of the errors. Signed-off-by: Richie Bendall * docs: Add AbortError to documented types Signed-off-by: Richie Bendall * docs: AbortError proper typing parameters Signed-off-by: Richie Bendall * docs: Add example code for Runkit Signed-off-by: Richie Bendall * Replace microbundle with @pika/pack (#689) * gitignore the pkg/ directory * Move TypeScript types to the root of the project * Replace microbundle with @pika/pack * chore: Remove @pika/plugin-build-web and revert ./dist output directory Signed-off-by: Richie Bendall Co-authored-by: Richie Bendall * fix incorrect statement in changelog * chore: v3.x upgrade guide * Change the Open Collective button * docs: Encode support button as Markdown instead of HTML * chore: Ignore proper directory in xo * Add an "Upgrading" section to readme * Split the upgrade guide into 2 files & add the missing changes about v3.x * style: Lint test and example files Signed-off-by: Richie Bendall * Move *.md files to the `docs` folder (except README.md) * Update references to files * Split LIMITS.md into 2 files (as of v2.x and v3.x) * chore: Remove logging statement Signed-off-by: Richie Bendall * style: Fix lint * docs: Correct typings for systemError in FetchError (Fixes #697) * refactor: Replace `encoding` with `fetch-charset-detection`. (#694) * refactor: Replace `encoding` with `fetch-charset-detection`. Signed-off-by: Richie Bendall * refactor: Move writing to stream back to body.js Signed-off-by: Richie Bendall * refactor: Only put convertBody in fetch-charset-detection and refactor others. Signed-off-by: Richie Bendall * test: Readd tests for getTotalBytes and extractContentType Signed-off-by: Richie Bendall * chore: Revert package.json indention Signed-off-by: Richie Bendall * chore: Remove optional dependency * docs: Replace code for fetch-charset-detection with documentation. Signed-off-by: Richie Bendall * chore: Remove iconv-lite * fix: Use default export instead of named export for convertBody Signed-off-by: Richie Bendall * chore: Remove unneeded installation of fetch-charset-detection in the build * docs: Fix typo * fix: Throw SyntaxError instead of FetchError in case of invalid… (#700) * fix: Throw SyntaxError instead of FetchError in case of invalid JSON Signed-off-by: Richie Bendall * docs: Add to upgrade guide Signed-off-by: Richie Bendall * Remove deprecated url.parse from test * Remove deprecated url.parse from server * fix: Proper data uri to buffer conversion (#703) Signed-off-by: Richie Bendall * chore: Add funding info * fix: Flawed property existence test (#706) Fix a problem where not all prototype methods are copied from the Body via the mixin method due to a failure to properly detect properties in the target. The current code uses the `in` operator, which may return properties lower down the inheritance chain, thus causing them to fail the copy. The new code properly calls the `.hasOwnProperty()` method to make the determination. * fix: Properly handle stream pipeline double-fire Signed-off-by: Richie Bendall * docs: Fix spelling Signed-off-by: Richie Bendall * chore: Add `funding` field to package.json (#708) * Fix: Do not set ContentLength to NaN (#709) * do not set ContentLength to NaN * lint * docs: Add logo Signed-off-by: Richie Bendall * chore: Update repository name from bitinn/node-fetch to node-fetch/node-fetch. Signed-off-by: Richie Bendall * chore: Fix unit tests Signed-off-by: Richie Bendall * chore(deps): Bump @pika/plugin-copy-assets from 0.7.1 to 0.8.1 (#713) Bumps [@pika/plugin-copy-assets](https://github.com/pikapkg/builders) from 0.7.1 to 0.8.1. - [Release notes](https://github.com/pikapkg/builders/releases) - [Commits](https://github.com/pikapkg/builders/compare/v0.7.1...v0.8.1) Signed-off-by: dependabot-preview[bot] * chore(deps): Bump @pika/plugin-build-types from 0.7.1 to 0.8.1 (#710) Bumps [@pika/plugin-build-types](https://github.com/pikapkg/builders) from 0.7.1 to 0.8.1. - [Release notes](https://github.com/pikapkg/builders/releases) - [Commits](https://github.com/pikapkg/builders/compare/v0.7.1...v0.8.1) Signed-off-by: dependabot-preview[bot] * Bump nyc from 14.1.1 to 15.0.0 (#714) Bumps [nyc](https://github.com/istanbuljs/nyc) from 14.1.1 to 15.0.0. - [Release notes](https://github.com/istanbuljs/nyc/releases) - [Changelog](https://github.com/istanbuljs/nyc/blob/master/CHANGELOG.md) - [Commits](https://github.com/istanbuljs/nyc/compare/v14.1.1...v15.0.0) Signed-off-by: dependabot-preview[bot] * chore: Update travis ci url Signed-off-by: Richie Bendall * chore(deps): Bump mocha from 6.2.2 to 7.0.0 (#711) Bumps [mocha](https://github.com/mochajs/mocha) from 6.2.2 to 7.0.0. - [Release notes](https://github.com/mochajs/mocha/releases) - [Changelog](https://github.com/mochajs/mocha/blob/master/CHANGELOG.md) - [Commits](https://github.com/mochajs/mocha/compare/v6.2.2...v7.0.0) Signed-off-by: dependabot-preview[bot] * feat: Allow excluding a user agent in a fetch request by setting… (#715) Signed-off-by: Richie Bendall * Bump @pika/plugin-build-node from 0.7.1 to 0.8.1 (#717) Bumps [@pika/plugin-build-node](https://github.com/pikapkg/builders) from 0.7.1 to 0.8.1. - [Release notes](https://github.com/pikapkg/builders/releases) - [Commits](https://github.com/pikapkg/builders/compare/v0.7.1...v0.8.1) Signed-off-by: dependabot-preview[bot] * Bump @pika/plugin-standard-pkg from 0.7.1 to 0.8.1 (#716) Bumps [@pika/plugin-standard-pkg](https://github.com/pikapkg/builders) from 0.7.1 to 0.8.1. - [Release notes](https://github.com/pikapkg/builders/releases) - [Commits](https://github.com/pikapkg/builders/compare/v0.7.1...v0.8.1) Signed-off-by: dependabot-preview[bot] * Bump form-data from 2.5.1 to 3.0.0 (#712) Bumps [form-data](https://github.com/form-data/form-data) from 2.5.1 to 3.0.0. - [Release notes](https://github.com/form-data/form-data/releases) - [Commits](https://github.com/form-data/form-data/compare/v2.5.1...v3.0.0) Signed-off-by: dependabot-preview[bot] * fix: typo * update suggestion * feat: Added missing redirect function (#718) * added missing redirect function * chore: Add types Co-authored-by: Richie Bendall * fix: Use req.setTimeout for timeout (#719) * chore: Update typings comment Signed-off-by: Richie Bendall * chore: Update deps Signed-off-by: Richie Bendall * docs: center badges & Open Collective button * docs: add missing comma * Remove current stable & LTS node version numbers from the comments I don't think we really want to update them * Bump xo from 0.25.4 to 0.26.1 (#730) Bumps [xo](https://github.com/xojs/xo) from 0.25.4 to 0.26.1. - [Release notes](https://github.com/xojs/xo/releases) - [Commits](https://github.com/xojs/xo/compare/v0.25.4...v0.26.1) Signed-off-by: dependabot-preview[bot] * Bump @pika/plugin-build-types from 0.8.3 to 0.9.2 (#729) Bumps [@pika/plugin-build-types](https://github.com/pikapkg/builders) from 0.8.3 to 0.9.2. - [Release notes](https://github.com/pikapkg/builders/releases) - [Commits](https://github.com/pikapkg/builders/compare/v0.8.3...v0.9.2) Signed-off-by: dependabot-preview[bot] * Bump @pika/plugin-standard-pkg from 0.8.3 to 0.9.2 (#726) Bumps [@pika/plugin-standard-pkg](https://github.com/pikapkg/builders) from 0.8.3 to 0.9.2. - [Release notes](https://github.com/pikapkg/builders/releases) - [Commits](https://github.com/pikapkg/builders/compare/v0.8.3...v0.9.2) Signed-off-by: dependabot-preview[bot] * docs: Update information about `req.body` type in v3.x release * Add information about removed body type to the v3 upgrade guide * add awesome badge * Show 2 ways of importing node-fetch (CommonJS & ES module) * update dependencies * lint * refactor: Replace `url.parse` with `new URL()` (#701) * chore: replace `url.parse` with `new URL()` * lint * handle relative URLs * Change error message * detect whether the url is absolute or not * update tests * drop relative url support * lint * fix tests * typo * Add information about dropped arbitrary URL support in v3.x upgrade guide * set xo linting rule (node/no-deprecated-api) to on * remove the `utf8` dependency * fix * refactor: split tests into several files, create the `utils` directory * Update package.json scripts & remove unnecessary xo linting rules * refactor: turn on some xo linting rules to improve code quality * fix tests * Remove invalid urls * fix merge conflict * update the upgrade guide * test if URLs are encoded as UTF-8 * update xo to 0.28.0 * chore: Build before publishing * v3.0.0-beta.1 * fix lint on test/main.js Co-authored-by: Richie Bendall Co-authored-by: Antoni Kepinski Co-authored-by: aeb-sia <50743092+aeb-sia@users.noreply.github.com> Co-authored-by: Nazar Mokrynskyi Co-authored-by: Steve Moser Co-authored-by: Erick Calder Co-authored-by: Yaacov Rydzinski Co-authored-by: dependabot-preview[bot] <27856297+dependabot-preview[bot]@users.noreply.github.com> Co-authored-by: Jimmy Wärting --- .babelrc | 52 - .editorconfig | 13 + .gitignore | 6 + .nycrc | 7 - .travis.yml | 28 +- README.md | 533 +++-- browser.js | 25 - build/babel-plugin.js | 61 - build/rollup-plugin.js | 18 - CHANGELOG.md => docs/CHANGELOG.md | 37 +- ERROR-HANDLING.md => docs/ERROR-HANDLING.md | 16 +- docs/media/Banner.svg | 21 + docs/media/Logo.svg | 21 + docs/media/NodeFetch.sketch | Bin 0 -> 33025 bytes LIMITS.md => docs/v2-LIMITS.md | 4 +- UPGRADE-GUIDE.md => docs/v2-UPGRADE-GUIDE.md | 8 +- docs/v3-LIMITS.md | 31 + docs/v3-UPGRADE-GUIDE.md | 110 ++ example.js | 27 + externals.d.ts | 21 + index.d.ts | 220 +++ package.json | 214 +- rollup.config.js | 27 - src/abort-error.js | 25 - src/blob.js | 119 -- src/body.js | 380 ++-- src/errors/abort-error.js | 27 + src/errors/fetch-error.js | 34 + src/fetch-error.js | 33 - src/headers.js | 83 +- src/index.js | 200 +- src/request.js | 148 +- src/response.js | 71 +- src/utils/is.js | 78 + test/external-encoding.js | 34 + test/headers.js | 232 +++ test/{test.js => main.js} | 1820 ++++++------------ test/request.js | 266 +++ test/response.js | 200 ++ test/utils/chai-timeout.js | 18 + test/{ => utils}/dummy.txt | 0 test/{ => utils}/server.js | 190 +- 42 files changed, 2974 insertions(+), 2484 deletions(-) delete mode 100644 .babelrc create mode 100644 .editorconfig delete mode 100644 .nycrc delete mode 100644 browser.js delete mode 100644 build/babel-plugin.js delete mode 100644 build/rollup-plugin.js rename CHANGELOG.md => docs/CHANGELOG.md (85%) rename ERROR-HANDLING.md => docs/ERROR-HANDLING.md (61%) create mode 100644 docs/media/Banner.svg create mode 100644 docs/media/Logo.svg create mode 100644 docs/media/NodeFetch.sketch rename LIMITS.md => docs/v2-LIMITS.md (90%) rename UPGRADE-GUIDE.md => docs/v2-UPGRADE-GUIDE.md (95%) create mode 100644 docs/v3-LIMITS.md create mode 100644 docs/v3-UPGRADE-GUIDE.md create mode 100644 example.js create mode 100644 externals.d.ts create mode 100644 index.d.ts delete mode 100644 rollup.config.js delete mode 100644 src/abort-error.js delete mode 100644 src/blob.js create mode 100644 src/errors/abort-error.js create mode 100644 src/errors/fetch-error.js delete mode 100644 src/fetch-error.js create mode 100644 src/utils/is.js create mode 100644 test/external-encoding.js create mode 100644 test/headers.js rename test/{test.js => main.js} (51%) create mode 100644 test/request.js create mode 100644 test/response.js create mode 100644 test/utils/chai-timeout.js rename test/{ => utils}/dummy.txt (100%) rename test/{ => utils}/server.js (64%) diff --git a/.babelrc b/.babelrc deleted file mode 100644 index 6a95c25..0000000 --- a/.babelrc +++ /dev/null @@ -1,52 +0,0 @@ -{ - env: { - test: { - presets: [ - [ 'env', { - loose: true, - targets: { node: 4 }, - exclude: [ - // skip some almost-compliant features on Node.js v4.x - 'transform-es2015-block-scoping', - 'transform-es2015-classes', - 'transform-es2015-for-of', - ] - } ] - ], - plugins: [ - './build/babel-plugin' - ] - }, - coverage: { - presets: [ - [ 'env', { - loose: true, - targets: { node: 4 }, - exclude: [ - 'transform-es2015-block-scoping', - 'transform-es2015-classes', - 'transform-es2015-for-of' - ] - } ] - ], - plugins: [ - [ 'istanbul', { exclude: [ 'src/blob.js', 'build', 'test' ] } ], - './build/babel-plugin' - ] - }, - rollup: { - presets: [ - [ 'env', { - loose: true, - targets: { node: 4 }, - exclude: [ - 'transform-es2015-block-scoping', - 'transform-es2015-classes', - 'transform-es2015-for-of' - ], - modules: false - } ] - ] - } - } -} diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..991f40f --- /dev/null +++ b/.editorconfig @@ -0,0 +1,13 @@ +# editorconfig.org + +root = true + +[*] +end_of_line = lf +charset = utf-8 +trim_trailing_whitespace = true +insert_final_newline = true +indent_style = tab + +[*.md] +trim_trailing_whitespace = false diff --git a/.gitignore b/.gitignore index 839eff4..a73d7bf 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,9 @@ +# Sketch temporary file +~*.sketch + +# Generated files +dist/ + # Logs logs *.log diff --git a/.nycrc b/.nycrc deleted file mode 100644 index d8d9c14..0000000 --- a/.nycrc +++ /dev/null @@ -1,7 +0,0 @@ -{ - "require": [ - "babel-register" - ], - "sourceMap": false, - "instrument": false -} diff --git a/.travis.yml b/.travis.yml index 3bb109e..20b2669 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,20 +1,16 @@ language: node_js + node_js: - - "4" - - "6" - - "8" - - "10" - - "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' + - "lts/*" # Latest LTS + - "node" # Latest Stable + +matrix: + include: + - # Linting stage + node_js: "lts/*" # Latest LTS + script: npm run lint + +cache: npm + script: - - npm uninstall encoding - npm run coverage - - npm install encoding - - npm run coverage -cache: - directories: - - node_modules diff --git a/README.md b/README.md index f5bc474..08dbdcc 100644 --- a/README.md +++ b/README.md @@ -1,15 +1,22 @@ -node-fetch -========== +
+ Node Fetch +
+

A light-weight module that brings window.fetch to Node.js.

+ Build status + Coverage status + Current version + Install size + Mentioned in Awesome Node.js + Discord +
+
+ Consider supporting us on our Open Collective: +
+
+ Open Collective +
-[![npm version][npm-image]][npm-url] -[![build status][travis-image]][travis-url] -[![coverage status][codecov-image]][codecov-url] -[![install size][install-size-image]][install-size-url] -[![Discord][discord-image]][discord-url] - -A light-weight module that brings `window.fetch` to Node.js - -(We are looking for [v2 maintainers and collaborators](https://github.com/bitinn/node-fetch/issues/567)) +--- [![Backers][opencollective-image]][opencollective-url] @@ -20,6 +27,7 @@ A light-weight module that brings `window.fetch` to Node.js - [Difference from client-side fetch](#difference-from-client-side-fetch) - [Installation](#installation) - [Loading and configuring the module](#loading-and-configuring-the-module) +- [Upgrading](#upgrading) - [Common Usage](#common-usage) - [Plain text or HTML](#plain-text-or-html) - [JSON](#json) @@ -39,13 +47,32 @@ A light-weight module that brings `window.fetch` to Node.js - [API](#api) - [fetch(url[, options])](#fetchurl-options) - [Options](#options) + - [Default Headers](#default-headers) + - [Custom Agent](#custom-agent) + - [Custom highWaterMark](#custom-highwatermark) - [Class: Request](#class-request) + - [new Request(input[, options])](#new-requestinput-options) - [Class: Response](#class-response) + - [new Response([body[, options]])](#new-responsebody-options) + - [response.ok](#responseok) + - [response.redirected](#responseredirected) - [Class: Headers](#class-headers) + - [new Headers([init])](#new-headersinit) - [Interface: Body](#interface-body) + - [body.body](#bodybody) + - [body.bodyUsed](#bodybodyused) + - [body.arrayBuffer()](#bodyarraybuffer) + - [body.blob()](#bodyblob) + - [body.json()](#bodyjson) + - [body.text()](#bodytext) + - [body.buffer()](#bodybuffer) - [Class: FetchError](#class-fetcherror) -- [License](#license) + - [Class: AbortError](#class-aborterror) +- [TypeScript](#typescript) - [Acknowledgement](#acknowledgement) +- [Team](#team) + - [Former](#former) +- [License](#license) @@ -59,247 +86,314 @@ See Matt Andrews' [isomorphic-fetch](https://github.com/matthew-andrews/isomorph - Stay consistent with `window.fetch` API. - Make conscious trade-off when following [WHATWG fetch spec][whatwg-fetch] and [stream spec](https://streams.spec.whatwg.org/) implementation details, document known differences. -- Use native promise but allow substituting it with [insert your favorite promise library]. -- Use native Node streams 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](ERROR-HANDLING.md) for troubleshooting. +- Use native promise, but allow substituting it with [insert your favorite promise library]. +- Use native Node streams 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][error-handling.md] for troubleshooting. ## Difference from client-side fetch -- See [Known Differences](LIMITS.md) for details. +- See known differences: + - [As of v3.x](docs/v3-LIMITS.md) + - [As of v2.x](docs/v2-LIMITS.md) - If you happen to use a missing feature that `window.fetch` offers, feel free to open an issue. - Pull requests are welcomed too! ## Installation -Current stable release (`2.x`) +Current stable release (`3.x`) ```sh $ npm install node-fetch ``` ## Loading and configuring the module -We suggest you load the module via `require` until the stabilization of ES modules in node: + ```js +// CommonJS const fetch = require('node-fetch'); + +// ES Module +import fetch from 'node-fetch'; ``` If you are using a Promise library other than native, set it through `fetch.Promise`: + ```js +const fetch = require('node-fetch'); const Bluebird = require('bluebird'); fetch.Promise = Bluebird; ``` +If you want to patch the global object in node: + +```js +const fetch = require('node-fetch'); + +if (!globalThis.fetch) { + globalThis.fetch = fetch; +} +``` + +For versions of node earlier than 12.x, use this `globalThis` [polyfill](https://mathiasbynens.be/notes/globalthis): + +```js +(function() { + if (typeof globalThis === 'object') return; + Object.defineProperty(Object.prototype, '__magic__', { + get: function() { + return this; + }, + configurable: true + }); + __magic__.globalThis = __magic__; + delete Object.prototype.__magic__; +}()); +``` + +## Upgrading + +Using an old version of node-fetch? Check out the following files: + +- [2.x to 3.x upgrade guide](docs/v3-UPGRADE-GUIDE.md) +- [1.x to 2.x upgrade guide](docs/v2-UPGRADE-GUIDE.md) +- [Changelog](docs/CHANGELOG.md) + ## Common Usage -NOTE: The documentation below is up-to-date with `2.x` releases; see the [`1.x` readme](https://github.com/bitinn/node-fetch/blob/1.x/README.md), [changelog](https://github.com/bitinn/node-fetch/blob/1.x/CHANGELOG.md) and [2.x upgrade guide](UPGRADE-GUIDE.md) for the differences. +NOTE: The documentation below is up-to-date with `3.x` releases, if you are using an older version, please check how to [upgrade](#upgrading). + +### Plain text or HTML -#### Plain text or HTML ```js +const fetch = require('node-fetch'); + fetch('https://github.com/') - .then(res => res.text()) - .then(body => console.log(body)); + .then(res => res.text()) + .then(body => console.log(body)); ``` -#### JSON +### JSON ```js +const fetch = require('node-fetch'); fetch('https://api.github.com/users/github') - .then(res => res.json()) - .then(json => console.log(json)); + .then(res => res.json()) + .then(json => console.log(json)); ``` -#### Simple Post +### Simple Post + ```js -fetch('https://httpbin.org/post', { method: 'POST', body: 'a=1' }) - .then(res => res.json()) // expecting a json response - .then(json => console.log(json)); +const fetch = require('node-fetch'); + +fetch('https://httpbin.org/post', {method: 'POST', body: 'a=1'}) + .then(res => res.json()) // expecting a json response + .then(json => console.log(json)); ``` -#### Post with JSON +### Post with JSON ```js -const body = { a: 1 }; +const fetch = require('node-fetch'); + +const body = {a: 1}; fetch('https://httpbin.org/post', { - method: 'post', - body: JSON.stringify(body), - headers: { 'Content-Type': 'application/json' }, - }) - .then(res => res.json()) - .then(json => console.log(json)); + method: 'post', + body: JSON.stringify(body), + headers: {'Content-Type': 'application/json'} +}) + .then(res => res.json()) + .then(json => console.log(json)); ``` -#### Post with form parameters -`URLSearchParams` is available in Node.js as of v7.5.0. See [official documentation](https://nodejs.org/api/url.html#url_class_urlsearchparams) for more usage methods. +### Post with form parameters + +`URLSearchParams` is available on the global object in Node.js as of v10.0.0. See [official documentation](https://nodejs.org/api/url.html#url_class_urlsearchparams) for more usage methods. NOTE: The `Content-Type` header is only set automatically to `x-www-form-urlencoded` when an instance of `URLSearchParams` is given as such: ```js -const { URLSearchParams } = require('url'); +const fetch = require('node-fetch'); const params = new URLSearchParams(); params.append('a', 1); -fetch('https://httpbin.org/post', { method: 'POST', body: params }) - .then(res => res.json()) - .then(json => console.log(json)); +fetch('https://httpbin.org/post', {method: 'POST', body: params}) + .then(res => res.json()) + .then(json => console.log(json)); ``` -#### Handling exceptions -NOTE: 3xx-5xx responses are *NOT* exceptions and should be handled in `then()`; see the next section for more information. +### Handling exceptions -Adding a catch to the fetch promise chain will catch *all* exceptions, such as errors originating from node core libraries, network errors and operational errors, which are instances of FetchError. See the [error handling document](ERROR-HANDLING.md) for more details. +NOTE: 3xx-5xx responses are _NOT_ exceptions, and should be handled in `then()`, see the next section. + +Adding a catch to the fetch promise chain will catch _all_ exceptions, such as errors originating from node core libraries, like network errors, and operational errors which are instances of FetchError. See the [error handling document][error-handling.md] for more details. ```js -fetch('https://domain.invalid/') - .catch(err => console.error(err)); +const fetch = require('node-fetch'); + +fetch('https://domain.invalid/').catch(err => console.error(err)); ``` -#### Handling client and server errors +### Handling client and server errors + It is common to create a helper function to check that the response contains no client (4xx) or server (5xx) error responses: ```js +const fetch = require('node-fetch'); + function checkStatus(res) { - if (res.ok) { // res.status >= 200 && res.status < 300 - return res; - } else { - throw MyCustomError(res.statusText); - } + if (res.ok) { + // res.status >= 200 && res.status < 300 + return res; + } else { + throw MyCustomError(res.statusText); + } } fetch('https://httpbin.org/status/400') - .then(checkStatus) - .then(res => console.log('will not get here...')) + .then(checkStatus) + .then(res => console.log('will not get here...')); ``` ## Advanced Usage -#### Streams +### Streams + The "Node.js way" is to use streams when possible: ```js -fetch('https://assets-cdn.github.com/images/modules/logos_page/Octocat.png') - .then(res => { - const dest = fs.createWriteStream('./octocat.png'); - res.body.pipe(dest); - }); +const {createWriteStream} = require('fs'); +const fetch = require('node-fetch'); + +fetch( + 'https://octodex.github.com/images/Fintechtocat.png' +).then(res => { + const dest = fs.createWriteStream('./octocat.png'); + res.body.pipe(dest); +}); ``` -#### Buffer -If you prefer to cache binary data in full, use buffer(). (NOTE: `buffer()` is a `node-fetch`-only API) +### Buffer + +If you prefer to cache binary data in full, use buffer(). (NOTE: buffer() is a `node-fetch` only API) ```js +const fetch = require('node-fetch'); const fileType = require('file-type'); -fetch('https://assets-cdn.github.com/images/modules/logos_page/Octocat.png') - .then(res => res.buffer()) - .then(buffer => fileType(buffer)) - .then(type => { /* ... */ }); +fetch('https://octodex.github.com/images/Fintechtocat.png') + .then(res => res.buffer()) + .then(buffer => fileType(buffer)) + .then(type => { + console.log(type); + }); ``` -#### Accessing Headers and other Meta data +### Accessing Headers and other Meta data + ```js -fetch('https://github.com/') - .then(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')); - }); +const fetch = require('node-fetch'); + +fetch('https://github.com/').then(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')); +}); ``` -#### Extract Set-Cookie Header +### Extract Set-Cookie Header Unlike browsers, you can access raw `Set-Cookie` headers manually using `Headers.raw()`. This is a `node-fetch` only API. ```js -fetch(url).then(res => { - // returns an array of values, instead of a string of comma-separated values - console.log(res.headers.raw()['set-cookie']); +const fetch = require('node-fetch'); + +fetch('https://example.com').then(res => { + // returns an array of values, instead of a string of comma-separated values + console.log(res.headers.raw()['set-cookie']); }); ``` -#### Post data using a file stream +### Post data using a file stream ```js -const { createReadStream } = require('fs'); +const {createReadStream} = require('fs'); +const fetch = require('node-fetch'); const stream = createReadStream('input.txt'); -fetch('https://httpbin.org/post', { method: 'POST', body: stream }) - .then(res => res.json()) - .then(json => console.log(json)); +fetch('https://httpbin.org/post', {method: 'POST', body: stream}) + .then(res => res.json()) + .then(json => console.log(json)); ``` -#### Post with form-data (detect multipart) +### Post with form-data (detect multipart) ```js +const fetch = require('node-fetch'); const FormData = require('form-data'); const form = new FormData(); form.append('a', 1); -fetch('https://httpbin.org/post', { method: 'POST', body: form }) - .then(res => res.json()) - .then(json => console.log(json)); +fetch('https://httpbin.org/post', {method: 'POST', body: form}) + .then(res => res.json()) + .then(json => console.log(json)); // OR, using custom headers // NOTE: getHeaders() is non-standard API -const form = new FormData(); -form.append('a', 1); - const options = { - method: 'POST', - body: form, - headers: form.getHeaders() -} + method: 'POST', + body: form, + headers: form.getHeaders() +}; fetch('https://httpbin.org/post', options) - .then(res => res.json()) - .then(json => console.log(json)); + .then(res => res.json()) + .then(json => console.log(json)); ``` -#### Request cancellation with AbortSignal - -> NOTE: You may cancel streamed requests only on Node >= v8.0.0 +### Request cancellation with AbortSignal You may cancel requests with `AbortController`. A suggested implementation is [`abort-controller`](https://www.npmjs.com/package/abort-controller). An example of timing out a request after 150ms could be achieved as the following: ```js -import AbortController from 'abort-controller'; +const fetch = require('node-fetch'); +const AbortController = require('abort-controller'); const controller = new AbortController(); -const timeout = setTimeout( - () => { controller.abort(); }, - 150, -); +const timeout = setTimeout(() => { + controller.abort(); +}, 150); -fetch(url, { signal: controller.signal }) - .then(res => res.json()) - .then( - data => { - useData(data) - }, - err => { - if (err.name === 'AbortError') { - // request was aborted - } - }, - ) - .finally(() => { - clearTimeout(timeout); - }); +fetch('https://example.com', {signal: controller.signal}) + .then(res => res.json()) + .then( + data => { + useData(data); + }, + err => { + if (err.name === 'AbortError') { + console.log('request was aborted'); + } + } + ) + .finally(() => { + clearTimeout(timeout); + }); ``` -See [test cases](https://github.com/bitinn/node-fetch/blob/master/test/test.js) for more examples. - +See [test cases](https://github.com/node-fetch/node-fetch/blob/master/test/test.js) for more examples. ## API @@ -314,6 +408,7 @@ Perform an HTTP(S) fetch. `url` should be an absolute url, such as `https://example.com/`. A path-relative URL (`/file/under/root`) or protocol-relative URL (`//can-be-http-or-https.com/`) will result in a rejected `Promise`. + ### Options The default values are shown after each option key. @@ -322,36 +417,37 @@ The default values are shown after each option key. { // These properties are part of the Fetch Standard method: 'GET', - headers: {}, // request headers. format is the identical to that accepted by the Headers constructor (see below) - body: null, // request body. can be null, a string, a Buffer, a Blob, or a Node.js Readable stream - redirect: 'follow', // set to `manual` to extract redirect headers, `error` to reject redirect - signal: null, // pass an instance of AbortSignal to optionally abort requests + headers: {}, // request headers. format is the identical to that accepted by the Headers constructor (see below) + body: null, // request body. can be null, a string, a Buffer, a Blob, or a Node.js Readable stream + redirect: 'follow', // set to `manual` to extract redirect headers, `error` to reject redirect + signal: null, // pass an instance of AbortSignal to optionally abort requests // The following properties are node-fetch extensions - 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). Signal is recommended instead. - compress: true, // support gzip/deflate content encoding. false to disable - size: 0, // maximum response body size in bytes. 0 to disable - agent: null // http(s).Agent instance or function that returns an instance (see below) + 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). Signal is recommended instead. + compress: true, // support gzip/deflate content encoding. false to disable + size: 0, // maximum response body size in bytes. 0 to disable + agent: null, // http(s).Agent instance or function that returns an instance (see below) + highWaterMark: 16384 // the maximum number of bytes to store in the internal buffer before ceasing to read from the underlying resource. } ``` -##### Default Headers +#### Default Headers If no values are set, the following request headers will be sent automatically: -Header | Value -------------------- | -------------------------------------------------------- -`Accept-Encoding` | `gzip,deflate` _(when `options.compress === true`)_ -`Accept` | `*/*` -`Connection` | `close` _(when no `options.agent` is present)_ -`Content-Length` | _(automatically calculated, if possible)_ -`Transfer-Encoding` | `chunked` _(when `req.body` is a stream)_ -`User-Agent` | `node-fetch/1.0 (+https://github.com/bitinn/node-fetch)` +| Header | Value | +| ------------------- | -------------------------------------------------------- | +| `Accept-Encoding` | `gzip,deflate` _(when `options.compress === true`)_ | +| `Accept` | `*/*` | +| `Connection` | `close` _(when no `options.agent` is present)_ | +| `Content-Length` | _(automatically calculated, if possible)_ | +| `Transfer-Encoding` | `chunked` _(when `req.body` is a stream)_ | +| `User-Agent` | `node-fetch (+https://github.com/node-fetch/node-fetch)` | Note: when `body` is a `Stream`, `Content-Length` is not set automatically. -##### Custom Agent +#### Custom Agent The `agent` option allows you to specify networking related options which are out of the scope of Fetch, including and not limited to the following: @@ -364,25 +460,58 @@ See [`http.Agent`](https://nodejs.org/api/http.html#http_new_agent_options) for In addition, the `agent` option accepts a function that returns `http`(s)`.Agent` instance given current [URL](https://nodejs.org/api/url.html), this is useful during a redirection chain across HTTP and HTTPS protocol. ```js +const http = require('http'); +const https = require('https'); + const httpAgent = new http.Agent({ - keepAlive: true + keepAlive: true }); const httpsAgent = new https.Agent({ - keepAlive: true + keepAlive: true }); const options = { - agent: function (_parsedURL) { - if (_parsedURL.protocol == 'http:') { - return httpAgent; - } else { - return httpsAgent; - } - } -} + agent: function(_parsedURL) { + if (_parsedURL.protocol == 'http:') { + return httpAgent; + } else { + return httpsAgent; + } + } +}; +``` + + + +#### Custom highWaterMark + +Stream on Node.js have a smaller internal buffer size (16Kb, aka `highWaterMark`) from client-side browsers (>1Mb, not consistent across browsers). Because of that, when you are writing an isomorphic app and using `res.clone()`, it will hang with large response in Node. + +The recommended way to fix this problem is to resolve cloned response in parallel: + +```js +const fetch = require('node-fetch'); + +fetch('https://example.com').then(res => { + const r1 = res.clone(); + + return Promise.all([res.json(), r1.text()]).then(results => { + console.log(results[0]); + console.log(results[1]); + }); +}); +``` + +If for some reason you don't like the solution above, since `3.x` you are able to modify the `highWaterMark` option: + +```js +const fetch = require('node-fetch'); + +fetch('https://example.com', {highWaterMark: 10}).then(res => res.clone().buffer()); ``` + ### Class: Request An HTTP(S) request containing information about URL, method, headers, and the body. This class implements the [Body](#iface-body) interface. @@ -405,12 +534,13 @@ The following node-fetch extension properties are provided: - `compress` - `counter` - `agent` +- `highWaterMark` See [options](#fetch-options) for exact meaning of these extensions. #### new Request(input[, options]) -*(spec-compliant)* +_(spec-compliant)_ - `input` A string representing a URL, or another `Request` (which will be cloned) - `options` [Options][#fetch-options] for the HTTP(S) request @@ -420,6 +550,7 @@ Constructs a new `Request` object. The constructor is identical to that in the [ In most cases, directly `fetch(url, options)` is simpler than creating a `Request` object. + ### Class: Response An HTTP(S) response. This class implements the [Body](#iface-body) interface. @@ -433,7 +564,7 @@ The following properties are not implemented in node-fetch at this moment: #### new Response([body[, options]]) -*(spec-compliant)* +_(spec-compliant)_ - `body` A `String` or [`Readable` stream][node-readable] - `options` A [`ResponseInit`][response-init] options dictionary @@ -444,24 +575,25 @@ Because Node.js does not implement service workers (for which this class was des #### response.ok -*(spec-compliant)* +_(spec-compliant)_ Convenience property representing if the request ended normally. Will evaluate to true if the response status was greater than or equal to 200 but smaller than 300. #### response.redirected -*(spec-compliant)* +_(spec-compliant)_ Convenience property representing if the request has been redirected at least once. Will evaluate to true if the internal redirect counter is greater than 0. + ### Class: Headers This class allows manipulating and iterating over a set of HTTP headers. All methods specified in the [Fetch Standard][whatwg-fetch] are implemented. #### new Headers([init]) -*(spec-compliant)* +_(spec-compliant)_ - `init` Optional argument to pre-fill the `Headers` object @@ -469,18 +601,16 @@ Construct a new `Headers` object. `init` can be either `null`, a `Headers` objec ```js // Example adapted from https://fetch.spec.whatwg.org/#example-headers-class +const Headers = require('node-fetch'); const meta = { - 'Content-Type': 'text/xml', - 'Breaking-Bad': '<3' + 'Content-Type': 'text/xml', + 'Breaking-Bad': '<3' }; const headers = new Headers(meta); // The above is equivalent to -const meta = [ - [ 'Content-Type', 'text/xml' ], - [ 'Breaking-Bad', '<3' ] -]; +const meta = [['Content-Type', 'text/xml'], ['Breaking-Bad', '<3']]; const headers = new Headers(meta); // You can in fact use any iterable objects, like a Map or even another Headers @@ -492,6 +622,7 @@ const copyOfHeaders = new Headers(headers); ``` + ### Interface: Body `Body` is an abstract interface with methods that are applicable to both `Request` and `Response` classes. @@ -502,89 +633,89 @@ The following methods are not yet implemented in node-fetch at this moment: #### body.body -*(deviation from spec)* +_(deviation from spec)_ -* Node.js [`Readable` stream][node-readable] +- Node.js [`Readable` stream][node-readable] Data are encapsulated in the `Body` object. Note that while the [Fetch Standard][whatwg-fetch] requires the property to always be a WHATWG `ReadableStream`, in node-fetch it is a Node.js [`Readable` stream][node-readable]. #### body.bodyUsed -*(spec-compliant)* +_(spec-compliant)_ -* `Boolean` +- `Boolean` A boolean property for if this body has been consumed. Per the specs, a consumed body cannot be used again. #### body.arrayBuffer() + #### body.blob() + #### body.json() + #### body.text() -*(spec-compliant)* +_(spec-compliant)_ -* Returns: Promise +- Returns: `Promise` Consume the body and return a promise that will resolve to one of these formats. #### body.buffer() -*(node-fetch extension)* +_(node-fetch extension)_ -* Returns: Promise<Buffer> +- Returns: `Promise` Consume the body and return a promise that will resolve to a Buffer. -#### body.textConverted() - -*(node-fetch extension)* - -* Returns: Promise<String> - -Identical to `body.text()`, except instead of always converting to UTF-8, encoding sniffing will be performed and text converted to UTF-8 if possible. - -(This API requires an optional dependency of the npm package [encoding](https://www.npmjs.com/package/encoding), which you need to install manually. `webpack` users may see [a warning message](https://github.com/bitinn/node-fetch/issues/412#issuecomment-379007792) due to this optional dependency.) - + ### Class: FetchError -*(node-fetch extension)* +_(node-fetch extension)_ An operational error in the fetching process. See [ERROR-HANDLING.md][] for more info. + ### Class: AbortError -*(node-fetch extension)* +_(node-fetch extension)_ An Error thrown when the request is aborted in response to an `AbortSignal`'s `abort` event. It has a `name` property of `AbortError`. See [ERROR-HANDLING.MD][] for more info. +## TypeScript + +Since `3.x` types are bundled with `node-fetch`, so you don't need to install any additional packages. + +For older versions please use the type definitions from [DefinitelyTyped](https://github.com/DefinitelyTyped/DefinitelyTyped): + +```sh +$ npm install --save-dev @types/node-fetch +``` + ## Acknowledgement Thanks to [github/fetch](https://github.com/github/fetch) for providing a solid implementation reference. -`node-fetch` v1 was maintained by [@bitinn](https://github.com/bitinn); v2 was maintained by [@TimothyGu](https://github.com/timothygu), [@bitinn](https://github.com/bitinn) and [@jimmywarting](https://github.com/jimmywarting); v2 readme is written by [@jkantr](https://github.com/jkantr). +## Team + +[![David Frank](https://github.com/bitinn.png?size=100)](https://github.com/bitinn) | [![Jimmy Wärting](https://github.com/jimmywarting.png?size=100)](https://github.com/jimmywarting) | [![Antoni Kepinski](https://github.com/xxczaki.png?size=100)](https://github.com/xxczaki) | [![Richie Bendall](https://github.com/Richienb.png?size=100)](https://github.com/Richienb) | [![Gregor Martynus](https://github.com/gr2m.png?size=100)](https://github.com/gr2m) +---|---|---|---|--- +[David Frank](https://bitinn.net/) | [Jimmy Wärting](https://jimmy.warting.se/) | [Antoni Kepinski](https://kepinski.me) | [Richie Bendall](https://www.richie-bendall.ml/) | [Gregor Martynus](https://twitter.com/gr2m) + +###### Former + +- [Timothy Gu](https://github.com/timothygu) +- [Jared Kantrowitz](https://github.com/jkantr) ## License MIT -[npm-image]: https://flat.badgen.net/npm/v/node-fetch -[npm-url]: https://www.npmjs.com/package/node-fetch -[travis-image]: https://flat.badgen.net/travis/bitinn/node-fetch -[travis-url]: https://travis-ci.org/bitinn/node-fetch -[codecov-image]: https://img.shields.io/codecov/c/gh/node-fetch/node-fetch/master?style=flat-square -[codecov-url]: https://codecov.io/gh/node-fetch/node-fetch -[install-size-image]: https://flat.badgen.net/packagephobia/install/node-fetch -[install-size-url]: https://packagephobia.now.sh/result?p=node-fetch -[discord-image]: https://img.shields.io/discord/619915844268326952?color=%237289DA&label=Discord&style=flat-square -[discord-url]: https://discord.gg/Zxbndcm -[opencollective-image]: https://opencollective.com/node-fetch/backers.svg -[opencollective-url]: https://opencollective.com/node-fetch [whatwg-fetch]: https://fetch.spec.whatwg.org/ [response-init]: https://fetch.spec.whatwg.org/#responseinit [node-readable]: https://nodejs.org/api/stream.html#stream_readable_streams [mdn-headers]: https://developer.mozilla.org/en-US/docs/Web/API/Headers -[LIMITS.md]: https://github.com/bitinn/node-fetch/blob/master/LIMITS.md -[ERROR-HANDLING.md]: https://github.com/bitinn/node-fetch/blob/master/ERROR-HANDLING.md -[UPGRADE-GUIDE.md]: https://github.com/bitinn/node-fetch/blob/master/UPGRADE-GUIDE.md +[error-handling.md]: https://github.com/node-fetch/node-fetch/blob/master/docs/ERROR-HANDLING.md diff --git a/browser.js b/browser.js deleted file mode 100644 index 83c54c5..0000000 --- a/browser.js +++ /dev/null @@ -1,25 +0,0 @@ -"use strict"; - -// ref: https://github.com/tc39/proposal-global -var getGlobal = function () { - // the only reliable means to get the global object is - // `Function('return this')()` - // However, this causes CSP violations in Chrome apps. - if (typeof self !== 'undefined') { return self; } - if (typeof window !== 'undefined') { return window; } - if (typeof global !== 'undefined') { return global; } - throw new Error('unable to locate global object'); -} - -var global = getGlobal(); - -module.exports = exports = global.fetch; - -// Needed for TypeScript and Webpack. -if (global.fetch) { - exports.default = global.fetch.bind(global); -} - -exports.Headers = global.Headers; -exports.Request = global.Request; -exports.Response = global.Response; \ No newline at end of file diff --git a/build/babel-plugin.js b/build/babel-plugin.js deleted file mode 100644 index 8cddae9..0000000 --- a/build/babel-plugin.js +++ /dev/null @@ -1,61 +0,0 @@ -// This Babel plugin makes it possible to do CommonJS-style function exports - -const walked = Symbol('walked'); - -module.exports = ({ types: t }) => ({ - visitor: { - Program: { - exit(program) { - if (program[walked]) { - return; - } - - for (let path of program.get('body')) { - if (path.isExpressionStatement()) { - const expr = path.get('expression'); - if (expr.isAssignmentExpression() && - expr.get('left').matchesPattern('exports.*')) { - const prop = expr.get('left').get('property'); - if (prop.isIdentifier({ name: 'default' })) { - program.unshiftContainer('body', [ - t.expressionStatement( - t.assignmentExpression('=', - t.identifier('exports'), - t.assignmentExpression('=', - t.memberExpression( - t.identifier('module'), t.identifier('exports') - ), - expr.node.right - ) - ) - ), - t.expressionStatement( - t.callExpression( - t.memberExpression( - t.identifier('Object'), t.identifier('defineProperty')), - [ - t.identifier('exports'), - t.stringLiteral('__esModule'), - t.objectExpression([ - t.objectProperty(t.identifier('value'), t.booleanLiteral(true)) - ]) - ] - ) - ), - t.expressionStatement( - t.assignmentExpression('=', - expr.node.left, t.identifier('exports') - ) - ) - ]); - path.remove(); - } - } - } - } - - program[walked] = true; - } - } - } -}); diff --git a/build/rollup-plugin.js b/build/rollup-plugin.js deleted file mode 100644 index 36ebdc8..0000000 --- a/build/rollup-plugin.js +++ /dev/null @@ -1,18 +0,0 @@ -export default function tweakDefault() { - return { - transformBundle: function (source) { - var lines = source.split('\n'); - for (var i = 0; i < lines.length; i++) { - var line = lines[i]; - var matches = /^(exports(?:\['default']|\.default)) = (.*);$/.exec(line); - if (matches) { - lines[i] = 'module.exports = exports = ' + matches[2] + ';\n' + - 'Object.defineProperty(exports, "__esModule", { value: true });\n' + - matches[1] + ' = exports;'; - break; - } - } - return lines.join('\n'); - } - }; -} diff --git a/CHANGELOG.md b/docs/CHANGELOG.md similarity index 85% rename from CHANGELOG.md rename to docs/CHANGELOG.md index 188fcd3..2d5c4ba 100644 --- a/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -1,7 +1,32 @@ - Changelog ========= +# 3.x release + +## v3.0.0 + + + +- **Breaking:** minimum supported Node.js version is now 10. +- Enhance: added new node-fetch-only option: `highWaterMark`. +- Enhance: `AbortError` now uses a w3c defined message. +- Enhance: data URI support. +- Enhance: drop existing blob implementation code and use fetch-blob as dependency instead. +- Enhance: modernise the code behind `FetchError` and `AbortError`. +- Enhance: replace deprecated `url.parse()` and `url.replace()` with the new WHATWG's `new URL()` +- Enhance: allow excluding a `user-agent` in a fetch request by setting it's header to null. +- Fix: `Response.statusText` no longer sets a default message derived from the HTTP status code. +- Fix: missing response stream error events. +- Fix: do not use constructor.name to check object. +- Fix: convert `Content-Encoding` to lowercase. +- Fix: propagate size and timeout to cloned response. +- Other: bundle TypeScript types. +- Other: replace Rollup with @pika/pack. +- Other: introduce linting to the project. +- Other: simplify Travis CI build matrix. +- Other: dev dependency update. +- Other: readme update. + # 2.x release @@ -40,7 +65,7 @@ Changelog ## v2.2.1 - Fix: `compress` flag shouldn't overwrite existing `Accept-Encoding` header. -- Fix: multiple `import` rules, where `PassThrough` etc. doesn't have a named export when using node <10 and `--exerimental-modules` flag. +- Fix: multiple `import` rules, where `PassThrough` etc. doesn't have a named export when using node <10 and `--experimental-modules` flag. - Other: Better README. ## v2.2.0 @@ -74,7 +99,7 @@ Fix packaging errors in v2.1.0. ## v2.0.0 -This is a major release. Check [our upgrade guide](https://github.com/bitinn/node-fetch/blob/master/UPGRADE-GUIDE.md) for an overview on some key differences between v1 and v2. +This is a major release. Check [our upgrade guide](https://github.com/node-fetch/node-fetch/blob/master/UPGRADE-GUIDE.md) for an overview on some key differences between v1 and v2. ### General changes @@ -99,7 +124,7 @@ This is a major release. Check [our upgrade guide](https://github.com/bitinn/nod ### Response and Request classes - Major: `response.text()` no longer attempts to detect encoding, instead always opting for UTF-8 (per spec); use `response.textConverted()` for the v1 behavior -- Major: make `response.json()` throw error instead of returning an empty object on 204 no-content respose (per spec; reverts behavior changed in v1.6.2) +- Major: make `response.json()` throw error instead of returning an empty object on 204 no-content response (per spec; reverts behavior changed in v1.6.2) - Major: internal methods are no longer exposed - Major: throw error when a `GET` or `HEAD` Request is constructed with a non-null body (per spec) - Enhance: add `response.arrayBuffer()` (also applies to Requests) @@ -124,9 +149,9 @@ This is a major release. Check [our upgrade guide](https://github.com/bitinn/nod # 1.x release -## backport releases (v1.7.0 and beyond) +## Backport releases (v1.7.0 and beyond) -See [changelog on 1.x branch](https://github.com/bitinn/node-fetch/blob/1.x/CHANGELOG.md) for details. +See [changelog on 1.x branch](https://github.com/node-fetch/node-fetch/blob/1.x/CHANGELOG.md) for details. ## v1.6.3 diff --git a/ERROR-HANDLING.md b/docs/ERROR-HANDLING.md similarity index 61% rename from ERROR-HANDLING.md rename to docs/ERROR-HANDLING.md index 89d5691..bda35d1 100644 --- a/ERROR-HANDLING.md +++ b/docs/ERROR-HANDLING.md @@ -6,17 +6,19 @@ Because `window.fetch` isn't designed to be transparent about the cause of reque The basics: -- A cancelled request is rejected with an [`AbortError`](https://github.com/bitinn/node-fetch/blob/master/README.md#class-aborterror). You can check if the reason for rejection was that the request was aborted by checking the `Error`'s `name` is `AbortError`. +- A cancelled request is rejected with an [`AbortError`](https://github.com/node-fetch/node-fetch/blob/master/README.md#class-aborterror). You can check if the reason for rejection was that the request was aborted by checking the `Error`'s `name` is `AbortError`. ```js -fetch(url, { signal }).catch(err => { - if (err.name === 'AbortError') { - // request was aborted +const fetch = required('node-fetch'); + +fetch(url, {signal}).catch(error => { + if (error.name === 'AbortError') { + console.log('request was aborted'); } -}) +}); ``` -- All [operational errors][joyent-guide] *other than aborted requests* are rejected with a [FetchError](https://github.com/bitinn/node-fetch/blob/master/README.md#class-fetcherror). You can handle them all through the promise `catch` clause. +- All [operational errors][joyent-guide] *other than aborted requests* are rejected with a [FetchError](https://github.com/node-fetch/node-fetch/blob/master/README.md#class-fetcherror). You can handle them all through the promise `catch` clause. - All errors come with an `err.message` detailing the cause of errors. @@ -28,6 +30,6 @@ fetch(url, { signal }).catch(err => { 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 +- Because we maintain 100% coverage, see [test.js](https://github.com/node-fetch/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 [joyent-guide]: https://www.joyent.com/node-js/production/design/errors#operational-errors-vs-programmer-errors diff --git a/docs/media/Banner.svg b/docs/media/Banner.svg new file mode 100644 index 0000000..b9c0797 --- /dev/null +++ b/docs/media/Banner.svg @@ -0,0 +1,21 @@ + + + Created with Lunacy + + + + + + + + \ No newline at end of file diff --git a/docs/media/Logo.svg b/docs/media/Logo.svg new file mode 100644 index 0000000..8d1a2c9 --- /dev/null +++ b/docs/media/Logo.svg @@ -0,0 +1,21 @@ + + + Created with Lunacy + + + + + + + + \ No newline at end of file diff --git a/docs/media/NodeFetch.sketch b/docs/media/NodeFetch.sketch new file mode 100644 index 0000000000000000000000000000000000000000..ad858e7bff071e6514c10dd72a9bb4cc25eaffbe GIT binary patch literal 33025 zcmY(pLy#^?*EHI;cH7!*yPvjg+qP}nwr$(oZQHiZ|HKz}aNkkYpe7l)B63w`%1eQQ zp#lLxK>_)DlgP_c3+VO%0Rce*0|7z*ce*&4IMP`<+1c7mH`oUA!-37|adeED_q4jL zI@7>P{-MR(J^zM)U(gjmrcACDyD$_ycN!KZD(py}@?1?}`--l!OLNiCrF&%Y69mPH zT0tiP!w^oc;B0WH5sYPV-!2IJ?DXx=@k8xvdS|3g}ns3RA| zfEn@&yJ}TjQeERl8)7_K2e_STWLS}rE+{pMm>l$bMaxi)xgX^Ml$~;u&lxpWTX$@V zoX&5ke2zBjump^iXz^rJW?3+xN+bpwMlzV9DpYVNRwSBd8kUp_lR_^dRle*9eYI|7 zYIL#D&wFr3sV7zR?L;QkYoD)x}3P!VPlAyj4u z3hWI7&(CqujV)(4xSVu>Hxjgg0^71*bh~Z(S$j45ZM)b<>yCLW-Dyob9(eO`t1pu& z;^kwZHtVx|Jt={W+hD+{-rz~ysfPUPWTUoHbI)-7t(Eh3yN^ODjkrNDU_XKQ5ccl% z@9_;K@M`(X(AP<4X=N)TCV*N2E1b4x9Zbwrrs;Mfh1G7=qKEI-oVVni5|ehBFqTj5TRuhVh7N&hzPRN0vOrYX*n1G01gfj1|eYq zw*TYpTHVI(kPZ2VI^d?tB;3=iSEA85tIN8{eBnCGRbdX<=#e-IM1<7=^!F7fve9~d z=aTKyQR^>s{2KF8_w3y~H%{L%sRj1oLixWyY?PwCzWaASbGkZHemEVsP{Hg>3TVMa znZtg#1cLN67@=q->I@NHZkE&T;^NMV3c|$uiFTIx;e0t(jE7QH)VaIvMb;B*IaBTG zaIhbG0y2oIheld+2bmRRZ8V-1ND{?AU}Q? z1UU%-^=Dc85_zd}{47kV33Mq7ZBF!XAL9`1m%Kbq5vw=YMvbHfa?P##*JxfUtf9w# zN)4UG19*%pL|q9*{Kp-oxy(4gDz{($2>w2g)mk?gWU;x$?E|Zo@rQB}46$Qdt%aH5 za)(8C(6A1nkYC@z3;Y3?%^E?qCW+SSfd20KJfuP3B(KX|i4Eb&GLM?T)Vt+`^3kBh zx+;ktbs<%1aGJLNk6w&lam2^>G6P$djijN#zagy!ZW5_E&b7sH7mk^jQhs>lk?0d` zj5+<4NQq;fFJ`)gi8d0!T)^u5F7iG92tQ~9WU`nl+TK%KO(SqXjno1O=_J8TYG7N` zG24mHy^e~+zs-ZP&ZA^;n^9hwMfcK+Ik8~~dTbpuT> zE&Wq9^Cbb|y3nt4UZ2aaF{z*TPzt19L&;S^N$q7`c%76-HOMg_*-Syxau(>xlFOKvKB8OE=-HF%rVmE*YHzDrQ}mY>kX zLgVEK>Z3z6JILQE>r)eI(dgKGYbk1w((CGJ3-A^=^4yvV_6~nO96;Lv)r>+5#iuGU zxRpRGq-2D3+cOBAxXwrxokAtHT}86~WxFeS;F44-`@aD!lL|{HU2~ppGL9sxZHT;8 zubkW>pzCr;Uj)x6amj&`n&O;)c6MR6BedS-=vZsqbE1Pc0n9}*bn3JnF!WdvfyDzJ zW1IWI1m>{sU7$Eq>1iTvY6R3*w$F>$k2iymA|^c~dnYV#dmykjV@(ZF@@po#pM6Nl z7pdq}+f+h9kDJ7?lC9;c^1#2PmvmXIe~KrU9gYyxCqjP=C`6CPZ^Hy#(6dM>-wqiF zsiiFG+Ii^6GkQ)Uj?=1{jg>qBV;oxy$2>w06Czi$#_l$5ZWR*JRB-<&f1>?EGPlm^gLJd* zO{Uv3ybbT>-y!`j2+8YtFhJK{YY4YUjV{7^@!E4kgeC`ID9(ys zPQ5}@QI0zFMmwr7=~%O~v3xDo7)Ip7y*QmWM}yCMA9>o`T>X9arf6!hIGekC}3yGY{XO-hp z4uuS`S!p3r%g7qi|459!Ip$M&W%ep)sX8x8k(PutK|a(~K1m#o^7$!k??lkI9%~Yu&~AJ?16(9ql~9>VUdSS=ei=)ddg{ULe+1d`<0c zZgHrdsY-RGfuXj@b2?&DDfW1pmF`+;NvrltDha%Sj7ha4P3Y0OR&=eMS)8p!i=K+4 zAa?J4aHmX7k47ubO&OUOW4RY)7VJ|ECgY0Erc&VodbAdZxR$gyXYYSay+YF|#6LPM z+}BG7T>i*5^#k+gyvx4=ScT~MOPQO#3B?7;#kU?eSk@>oC-9A`(_qEbdal=Z-vbAH zbr`a@0|#1r@vB95u|OrKC^-BHmK-tX^T(63c}*nU~~zWewGQO`%@b);K5SKrJL-@7)xKfA5SFH83{ zlMe~U?t7ig9SjLIsmPe0N(Aa`{<5ozaGRrb)Df#G!x1UNJ3IS}uE?BhyUUV;zh{yM zLuuv3vo%5V*>}@^!2hd+PoN}%I3WM2AlUz32{{;rMVT0xS!qRtnS}mRLSYtK4p!Fx zgc_n4DC3OC$^3Juw1&<>y z+d|1+?4E!gSSV#e(qH`!drCW7{=Ba#5fcX4$!H%iOFTvSEojIA?bLo|3qMM!#^7NexF^!?1JLR7@??L(pwu z(~0(oqS}EMB`e0MdQ)|h#^VeWZ;3?v;cD%b{6|o`jfgQCj5%_=zaDwl7d;;{1vmJ$ zvctpD2paE0Fyuir=!v7{11ymg4R_kl_9RF>1?d}M%`M*l&e>-1-iV6>5YTf35YT^r z03dru6ITlpHz)f4??Pv9Yj)KYqNbwt$_E!78JxU8L>N;XT)aT4v#^l3fG5dLA(`eM zo-FR>Ce>ztb`!h1LHPRlgLr$`Zlnz`yKPy`w3?BdYJpBp`XeMn#vd9KB$R}Nq3R#h z6$c3!HW%iM=$&Yq_y_JM%$%B{AQd{nI8ZCdb$3ek&_F(B|t7+-CgMAY| zE^Z!)k@IQV_M#M`I|FMwrOFRzdzK$1oj4IYUk{65e)~HmaVx_42{|a0X>eAaA;H&e ze*%T~35gnI`< z@VmfX?p&r5>wh=ybRj0DD#$W*GvohKv+1gJ_xY%^bGJ_atF^r16G;h z%Ioa;l@omk-pu-H@6y_WgcA>!j27jT7tY+Gce@_;D@=6!8()B^Pkg+(;o|!K(ZUs_ zrNUoV4Z@E-hxpsLn*y?kCaHvh5Y~_#D@&wVtLOZu^2b1W`!&qd>Rxr^cE=e7YQnba z9>DOSXWi$#JY`qPQWATe;qxh>*1zF@icFN%q{@+Ud`eY!x9t%XkGj|Vd5xTH5Y|5k zzZhv=tBfriSlvYoY+oL!{_#j;B~F$7`5G_OByV9sJ>T&~4a7#ycl-PMgjym>+s^PA zt9dHRSTmr)XR>wqyx&|5_@|Req-)v?N^4W^#Ad3cxW^Y4me9WVJ=|R-kh8!Mslt16 z-4%gXE^B^-ME!uuRvdZtr%aG+Qy&L3<2p5nQO`l`!|(4;x7r>$Hq7RFl?^|x9C?gK zNd*OkMT04Ov#vip%;$Ql;@nUJvp!1V%YW@%ZdeE?Uv}jo!fA1QjEvr-B4RgewQ89a zz1{3?-hu!J=b)DE+p1)lwge*^F>i)yf*g@#oGxXe0J>~cQav}gVySe@D zivGDgaQy6YzL~%Y=0Z*?S%%9}oaC;Do4?~N9`sNER6?B^vw@hCJUlpf(5KKZ-EyL0 zC6^*3{?>kYaPX6LPA=U&!AlmGqt7iXCG%JT^AaTX8|ryiK=^jxT+Ak|kQ(O$IGr}b zILflS()Fg`mwxEx9`G_Sl5g1*_}x`1Y`7~v8H-L5sxhFQcA6fDY?Cu6*8kafl_62L zds^KLd~Jdciez^y7AFdkM?gg2)s%E>;m#YXLF?1W71GYx;HjvC3qY)|$b1@l@7>|` z`JN90u}JZ@IbfNe%RhSbxY#UPX>U85x}(Dq8!rv&1=?QuY}d0?c#b&m`UA%pVEJ?D(#RtQ{n&d3k8M_HJ@4`zl@2 zhxvA&NEMX&1y31nCC#-qn{CqvQYTK`))IO(rRbuwy*@u@KC4H2CWRNV+K|*1Njfv9 zDO4hY2kI|?1Uy(2PVefC{kHunNuHwmBY*yMhbL31(zUiL>-R}*2>XD4zKlSb++aCU zI`Qix6q4_!)^qi(r7VaLS_fyxyFGfA4K1*%)-!mc6X*i_Gh^xc<7aUqF;o3e#ev!O ztYb=yptRh&cdd?@4)rmOoq~XF+eE<$JjZQ|0t`yu)k~U;l&ZgpQts4&x5)(B z%*ujv=qJt>!Ka*Lv^{QglfiqlSu*WByso7H+PxVK#6fQIOVtdw5lXn_i3L_9n1 z$NHwp|K3xM;@ZnpjAw1h4dbWUY<$vAX&BA2YFpC*MG zq6=G4d&m8)RwZi)^^xs0X|YdBC>R`!dqVguMv!aH^uX)sjrV7fdZjiAzM<2mX|dgI0PYx6KI`)CT`V*oH4_^~*w$(3P= zV!LQKQD)oxnIQ{bM}SQvd+oryX!B5lWVqMi=`!ZjYAS4nlK*%;xqVZDT<+r6B3QIa zR1ET!hQX}n-DYvmTxz@2RbE_t9Ok{TwI7^v)i?26SY*uW&2jhba;K?FdPvLg9bJyo zV|2SWha5P|)`n?E`nbD{Iu+JgsrfRo&`_N$ud^d7flL`}x6oip<M8e)6aB)`s_s zpFiK9D(=#{hXG#s^V<6i`C>REqzl;t;H67Lnx391xx)*R5$Tb(;aWkC8d4F-LC5>k zCkDj}#bLIgb%pT4ZTAe^nB9njRU^II6}kE)o$mRz&Z ziBuyLNN4}^!*(H8k%| zd!jhWIjb^RF8!B=-%?**ferO6^Vbyu11FA+L5Qjo9O)@yaS&tqH&oCA33>UMTnMI> zj}Rxe>RQ;f3m6U9S}tm?dmn^I}M0JoJb+FwY8OyvO-*Y71maWlPZAUVD0x5 z5q+5PhlGx$O^Cz08ik;#smbbLWO>vn+m)?%yWyFffY>i9aJyi(&XDyY)=OGiI=jXB zal1}fcs{m!P>>Di6_A~D8)9q6RQ7{p5wF&v;lRzS7}V6z;DA#XKF;Y7QYBPHjXNTp zAKgP|dT;>hUUjz($Xk$3U&`c~zFc2Kqs&i!;-sf~$*;>8|14 z*v&PV$$xdFJfqF9Lua~CC#y}8IyR3i7!Q&bw7hxDRX`}ut*iM@y0_HV<}`%9Z5OvG zz)ls{YPOjWFX5;$^LdxYjaL`~4=CEUBdRK*p>64t03XU9I^9{6Ib@dYqfIgR@n=lY zc#2Bp&h3VEV3$17{*THWMGKtCyq!g$<&Uc}#ttGlJcbf8hUOglw$MnFeS$JjK;W=E z)*M`0R^JvPAo1~gm$P=6z`_Yh38?0ZxClr+U6K=`Xs-OTHBd>j;qHvQk9YA6BDwyV z0Q$mI+dnv{)fxV8OBy62%|~I?8~g0Zr*at^Y%#LO!*a;)1Ucns|7_zIy=(6^C{Nuv zz{Jjnq1dwu>{DNyD2}#_>gto6Jc5C-N(Uj(9IcN%AUo1ok&Q;{9lwr!6?Kh|4gJ&Z zo_=WHViM?xB@QD?=lZoX4=*(EA=nvAbw`evp}W#~>ygIM(TA3{MoUjX0{+HBx^AJF zGuelgX~3@o#rFi)@EM;$VeuY8x`AEa+q>IlfNb*ZGu5lt8XPrSodL%8@J4lOQ6;vOoCa)u>yc5u*cZeeCNws4vRr8toCy7 zWN`4cS9VgKKJe|_Z9STCY2hxebfRV9+YvdpXP8*6C5yZHY8>!Na*4w?nYWqr)+frt zSw;*K#KAOd*%rv#ZQja*09}?^JdX|yHGMk}_2n)w{*A~kQu)VNPd4Pnpx*i`CRusm zB@*k(%3fRy?7_|D;bdadtrGQO};E4Dx|B6Yq^3&NJQioicUJ}195Rrq9`TrA%Mp!!7z4Ag&YTHD zG-1EVa_aqyrGY_ip7f{h-$2e5Ll5bUet>uPVEpu_CRY61W`JwAzHD6E*%Eg%+>{m^ z0|=pyG3}q&N25N+#;CEy4BM||womGl(r}W?uz`t<1~i9;o?#{+L^;u|Vf^VF%FDVk z%pv8s3BZ&X&r}`-`@JKqgaz==1E4k84IMDVsXZel!? z=)&fdEbz}=#u^^EM~es{&k#4Ow_t=`xowxICOD$@n-^A^Ey}>SAG92XKX}1s89lnB|v18q7^JpTnB zm*xieB?u*yTT$)$!88IceTCWeQ|$PmB#s+!nnn0~$p=be36~tIKDS?)3J%yWY4P`#eEP*LE{?o6^85G6;4jSXkI1kKpQKMv*_GBC-ch zub}vCITFpXCR;|vyXmL5sGSEKHB@CaqRqHMEmbD;M9mbriQyA#$=^l?4|$boG$ z8n=M==%2VR4>f+z-WLm=`4puK_~#U^YyKc3`O8Q?L-$f1@~OM8_xA+@P5wwsI#qp8 zM9)=5l3tA%PL&D2%M+T2qxMNbf*c#39n_Yjyw#}~n?djgxk71Plp4{_8miH>o=P;I zX7RQ1b=i2m+YgY%cvsc-60QVLNwQ_^%3qU>{r$RF5+;c#U}0um27SG5p7i|6Wmo-y zgNaAGXr-a2=W2;1{1ws=qcTkqhZy7a((BhZ$v-f7%rn8_$_aY{BH))$fY)*YItgYy zh4>l?adsNtwI-{X&uPEMaT(EP`zT_T8AkX9kH;!>YN+JgNB8W4Ffxb#J4~cHyZbE> zhRPJ&g>jiEqTk{Uf^Q3lO?rvW8B#-1tFXe}^00ul^sMw}JKi6Y&Z-AVPCKszh4&-anbF3P44nE(K%d(6Qh1>Xsx7oXKkfrn{9Feo;He#e_6HgHqtx3;41zy=+zLE8AR;3}|L;2Z(dzlU zYGYJ!%(}Sl8A5f7O9R)BWgpnq+EyA>aE@CMw0SrmDi|6XS_!$0QXBHRBl6EG@FBXX6z(U?nB zCd&AO7P!nK5QXn%AHhvqbsXAYZC6&ZMkVLXm4t2Ab&&OM-T(p{8K7^HED1>ME{|p8 z<34lNYVpl(8xlQUV)F8mNdE9aud4Wd#PtP471|+@2O;Mm)iuY(K_We_73ly?S{NY|%=@UUxX+3R5iu zN1dqSCwW=Kl^hi>>3G-~+IkwxrFFK*t+&M${DP)SzK!3#BN%8LTh%wS%OziG5Bf%D z3Lzq9GcX@ljj^zl=M#XQ^dQ$}*;Ddqa45#|^CH3LSRV$Vg|L|LS5;d*yI1-9vR=dH zpe?c%@gcFb_ec)tA>Jl9Q|FQGcy*EPyt#Ty{@rrYfFt7dYMprqgTlVE)-|rZufoUb zTlMj{{&dcGI@@=!gANWQuC=?c5TueJTy@;;XnvBuA8ACWx_i;)c%bz%`^p~Wndo22 z5^e?ttODcL4PB6s9{TOj?pM<-JfgLR~@Qd*1yajiMB8R90z!iAJ zB?F8)j6Q=c{hb=$e>C|$g(LNN*;GO7If49o%m@dOPMQAiu^0tv7c4Ez-tkC&Oz?m? z1u7o?KhqYHOIocm@Tx5 zeovRz9d!#X{?47BpY4W&)|fEhuwhFNuQ2bsXJWsx4;S=n=1Vt}I!3rs8)!}~Ud!$_ zK+3^j)+^{#{hFjJlCOmLX(Q+CeK(oy&p5S*!wndP&%ydFN?^#nHN=*{X-KtCWb;@n zyG0V?ukMw3M#~RX&4Xs}+)%$iRs&}UZ0XU?3KSDjH8s{7!5|^+s0SIUS6Y|=Tafmo zcwm^PXQ4BEEwkAo+vGtWnja`@Xkvgzc)u!>bN8mtl3Q5od4>8$>-M_1dsoXHPlk~$DEAN0cf<|)i; zE85Qo_U^>Vzs>F|1A}T!k{(!AoD=94w4<8DpPO)0w|b0gKUcWt%j_g6R}F(*ADHQ^ zq2XuvRY9INqL;`2;@~le2Z8RXkot|%8uO$Q#1%Bi$cZnvuA!#Jj={nDi21vEmw4|SEzb%p^CM)Ze~ z(7u3az)nmpY2X$V7{9K-chXSikQb@zNwd|Ay<~BJ079DP^e{sonl#V~OXKU*jco{< zk+XgF$)(B{=V0aI_DoKJH=--@>$fk|h2r?NeR1p(1bAT(E2%h6f*yWJ^u%>_ZOwTc zMvoEjI#czsv#{Q#%ccHA3{|K8>QkC8@YDZ((YMz zH!xqlAglKThY~@j_C#-C*&nI}qy*;LT1-+Df>hXTEJ^CCUH(KUdryS;zju<>i=w7J zEXQjw$do6X&ja+F<-xc*5vv=gz>48`6YHWCGcFf;_{%WbWk06Z%cQHK^FZ=y?Dwz4 zf97TK_Beko;kmNwVIXT$E>8p*oNA-K16T;6@&H~kpj)X8-gGE1o09xoGG`B( zzuT?nW`%YhrZhopW&hV2`tHadUkYVR&r>k7aRPl4Jfuw@0m>Xuc>n<^QHJUG3d zu-z%>;kr{0*r@e7w2FOJO2Sk}$tugtLT71c2xq&@BsJHr9gSO`(H6%`#9F}|VOskR(?@>@dI9FS3f z3U8iuWjSZswGjIl(F10fYn#M0q*uxJ+Gq2A6K0mrp=d^MDrEtgv-H*&Svw|Vbed)Y zYBJ(yA5gfzJ@o!+=1X{Be+J-P+HF>yfCX1}Gpu?o{!$hA^k=WgxbndrtD z#Gj79;GqWj@b1i_65f1$NMC#ak%b^o8gF7Q3D6qn%x1aFh}bf{nlHGPEj?B@1Nq;^ z0E(q5g&IA(w$d!O!$;RiM*DC)c&>2d!Ju9JNPg<##4#Z0q(Si^B*RpV{OL>zjo!&RTOh6IVey_9MMP^SV5<+(sXu=kJ$SZ7)6^=S(RUM05Ee|3ZqYAIKZ zgB4*p3%dRo=~mC}V=wW%0(saw8Iz3#QgD96=j42M(g@qPT{?JfWsPOWuV+0s3J_z_ZTJ1OCeZ z|8?`W#f9oJ=#|`nFTbE z99RfkQ8DBVMT?$9g&0)mndh2s(MMVDN-kBpRH#>RT1kAEz`3YcOST$4;MBCSw&u^# zs4yR$JJH|T2vJoBSTH>8fG)uX?P#U%Ruw29;^w8#LCTi*n-(0p4^2GMNav@4igVn} zjcM^$CnO1_dJ~-pB;($xcMM~H7)8N?dxUCo6sa6-sBWlFd#>DT@k>?9b>Ta#<3T4R z&8qBrV5`_{Ns$ix+z%o~BrHwvGU1Z28(aV<8& z(c*(^=}adBJ80TmEMyPf*0{NNkb#`}nk^bpn{Q1Mf3XBGYSsf8$@mnr(ydz8cBEHe zMH{x5(F~b)j@kBHAJ?c|u)FwdrtFgPp(&fVR=D9i@&wEj7kGB?5(GG7AtbT=Ik(8` z+c|!RhLsNpYPI15YO6b5P0Zh80NWB7un1KCoXCUo$Q0Y%_7}{Fj~9Kpu}|evXJojD%N*L;&f%zJAx>QYhECHk^VZjTo}(p#9xD z2R_DSXMb(m+A`D2aHf+Qq$~@V3$}#Bt}PAy#izm##f)eIJ_>Oo9@@@q)`SZi8@)SM zaU{gK2HPWsr(mk*8Hv9E4u`FvIOB(JuU0%2MiLN*;*X&uoLuM!LoCBJi@m&g)I*dy z;wI3rXSv4iIdFI5*|WTAU-(xzZ*6AFjjh^wA|bAqiPGr4Mmy`D6uK|J3|!*lIK1@Vkx zS9AY3r+vySzpY6Ve7fXOzM&h;PJkQB&@=gGmsF8Kr6$mgJ-lu?maG(;yY00wmYOzn z7?f1}sm0*2NNElzf91Agn+MDS?BL=GZ%t8uk&oOu)ojsdhfOb5`6#cByD}>Fs92k! z8(9cu)x*AS9OECikbNc3`EP6|a%R#}H=^lzL)bdQn$nlZX>+Ze&Y0#>yr1-2P}7mK zs?Xs#K-m@QEF>5mYE|rMV8H?Nm&x4ExqEX80eSZC($Pi{l1$rhL)Ns32^o0GCS8m1 z=_(R`ow+}W#xAe?KNY|1n#4;chJ>qDuVT06^aqDM9hrcyqr&19$C^D^H)oq zAnA>LO#UfUj(QV-?VpUfUp@%UDg|R_AQrIfsJd?+?G!12mF1K6Net3oCN{e!y96++ z4zyg;hV5b>ijST3&lv2*;Uam3hI9xqV&qSo0jM!f=^MM<;FmWd&GJ& z1;X+)tr;3FtFPod3l?wasnc%bM%k`|ndgY_2eyYFxJL@8r){qp(+ci* zvf}h3`EZgR)(m7*%;>U%(#A1;R6Nz%(D|0F&wF+v+-Kfm|NgW?U0uz5Yxsy*){B)@ z&BqPQX3TkT-+thC z6BbTJU_2{a`mzwCy?r7DB_*7mgj5W4QEqL5O)Z{9F&+*M0YM=YM#ci*#(u=p49`f> zisO4*^2Mu;)oJ;HyX zz0OP`B%aRgHa*{#bwygWZC^y&!c)!1iqyXC-Z0mbh`T+wMS$OG^eAQtC%Q2XDmGzL zmv`gf-lQILB?gkBB65b%=b4GNv$8{iIvZwU!Ua^Rz~2R{OEWR^t|zh25rUx&zz0a!EAfC`rc{MV9(IO?9R z%kq75&!2x}>4d|O^hki>I08PVO*+ZXw>RrBXrhF7{VgkV^Rv$i>AZfE82fs*M&axV zN>b|0P8BaS2w&1E7u6M992^{4&#-9&#@OP>CtTV z5K5smDFc9o2^;LVN9HjBu;wMGqegD+i3F~K(B1R%abt#<;~M6x&6C(JY}aGks<}ny zMoYhgR2Hl~cyn7n<+B>cW^9ubr0`EYI~p5u20VMI?`+fEoN0M?WZ{RBsF;+S^Z92_ zh?%4aAE+{$gi>;!_x4ZEX^|;^ykDulkK(_0o@}&?-5y~$SP|huEN>Pwi-~9XYYg4{&O?cbcjy!Jgx9#=m%pyEdq- z+4-xAl+{xA=G1{5Hv0UFX3%NP^fC1Ga9bWS zv%FHcU&Lx$Z$L?NnO}Da+hYtd48)&dq~1vcP@78k2xZ3*c)o0WLPUo#79OLAD9oeKj!jchHYLa(p+ z%`O74X`R=W8ybkfIT^2 zcxSXnoyHvoE!fe_X*W^1J}dG!)|V)bsD$^q*wv`N+X62lNbQ_vhsTH13>ReStq|bo z1DTY_(Ml&CBfnksa~e|_i81gvFeBDQetZ;M@x*W=7X^A}DyO2(+#<=z_(UsCSf~3I zKmmVEb*PS+xs{L0_ZAb%U_o2SDRbEiGy9-zk*1SX-V zO~2PB|Chx=CL|^0xzCt>wW$Y-;j z8=hCv_#ZC4L^wenKJ^(WI5qj~_4DCb{+Lv^k|t^=>UKUG={W*Vd$L&dop_w;E+=Mw zrcw!+u!0D*Y%k&VRn4G-qy548z;o;$!4~v3|!}6Is zv7zFxRex|6h(;`zf@O>FmgG>m#tC@6sGU<|Hx;eq)%?jGy)1x{*bI7|nmRubF{>NwBqcWw3e6#RAg0hjh`&2c$UTcB}m3} zn-15u-hHYZN(}-2kL72LYkxB!bTSjyea$sDzX3SubP_b`HF!@5 zke@%-vQ&>HIZml&S&>4xmvMwy5*7m`M}&T2DwpyCR$*7;1g1y5{`e3=K=mWmV4s-7 zz*ExS-*1%fl1@wB(GR9hVt#8nveqLqb6B-(Y8;J(^a@8fefg)Q!a-I)0S4Jfv-M<0 zxW;2ZV&Rj^gIszyR>`ch09u5RxXUa6AVf+2O}#~WJM}qq;8?T(Opa7HKl!?(<<9FK zw|#t3m#0pR&u@Xka3*>h7QoRUYS_H`>j>a5$J(BXKuM7 zc4+c&l7i)6W*+r%PV_L`t?uRJ=sM;JI?beN#OA@pgFEi!d;9>~MT)+Gksh8c2LOOO zGzfmwsFM2+Om~#pm=*YZ;-MAzkN!Fa`GGcC6KBaS4!_djeFwLlxiwgb_}D}wYdqzq zrXk}#iEAM29>LNbu76b}3%3O!a;OvQmrfiff(ZKN#xSrX3qyDe3PrcythZ;q>u;1X zvwJTYG6ck*7qKY&ur#bEW{}sM!!68sx;qn%B&!)-$X2)Lsh||vWFMDDSMTJu&oASr zFZ$DBRIeWeK7nT*aT}PK2><-vmj{@U=(OL0ZX7a9dHP$th=A?1hYQ(QQw~){MMV{C z63b{+-7rVcj-39>=#nob3pPWKq1Y_Ug*7{zZ&M7;BFg2(N@Vmc1l1IUh;{~bb@j>x zLvK;B<>Et*x$*Jw%7x0_nCfSkEy=xhxq1)8i!%>~Tr*B=IW)M8X+8IJ7 zqSWc^PsTp=Y6JGFo5@vYPi-xQMDg6I3SOew&G&p4BcLEedPPRnEPnyYJNdaK1sL;s7)ED4! zfOiJTjw`+~VEez?B3+nEH*NP5W<1RIh-qb#Nu51)3!d94X{VujH=Y8x5kuaWOBTvM z${|%I!))?oY1#pPi{S#KNN~`(4TdsY(1E}=ZRKfaKz$bt96-Nl%@XH zz6Kn3$tqyly@8o|1uNvdD(!;#OF!7R()g4GJ0>4(AX9jk&>%S#80>pZ{8kFYgi8&E3s`kud++wA}P;hq218`#9ZKR1j1ZArg-#ag^B)q#J;1LR{8c;;JifE z=U0s~5stZ*becs?haj{3UauX@<>a_vDBxh4xtDup7O71jV+03bqEL-+(!3{97UqA5 zn&@yTZo(1{WTqazCn4#QSZ{*)SHu5}j|(SaL+UWT`zRF}Ic%)t(6l|fc|W~dDJo*} zX3g)SiTE1rhrGY{W&Fc;21OmrIf`KVNk=X83m^k)q6th!ol`}kqeSbbkoR&92U()R z>^HC(2&8xevyK8G)_>DL;X0w^u7r0wnx6`F zQmmlhZ$}Fb@M*{lEE|9}1-_Bs41iUt(=YkBx!V9vJ0wd`utV0gjexH9`F(Ob_NSvD z8q(1P{$>h;54>&sX8@Fr5mkh>12rSx#ItHAz?xV2b`butet)shLmi6;j;SVZ;TcDQ z(okIOs8`uX3iOX`vRRw39(FZfe4j+r6b%dQddT-^|MQ>0dEkcH+Ynjb%2gL@KJ-MN zu?x#pIlMisrvVhBn%Ux~e!mAeWD&qx76Jrwy$c#(*YF_{=Z!00#&64B*8MO#nP2rU zK61tCSD4aJ61i=I?cwxEUwF*3BhEUrqzaVDuYbM%gfoaTb9RZO*Q;ia7+q8u@nx8UGdi{M`t|JI+p z3NAs#tNtj2`(^VDy!U6%18^I0KriPlD8U%e8V{L6nxw$O%6ogrS zaehKlQgG4}RzKT#LY0$aG-ruLcKTwcy0Sf`{QbJK2+`}d8V*r^l#YYlu%3z^8CjQ5 z#Oh%C6FEbD(Apj5I;Pq6DULR#zJ!5<2&@bgC8}}oU|@oT<>jMIl9uY+NeBQ{Bv3dg zx)m-iu6`o6q#1YMUt)&ZYmia1lJXG_y#MMq(jb9^^YVcj{O*Md6V-IfbCZ?CLVPKo zQZ=TvGaf$Rml*^ohDwtRa0EV_atMBr{GiUi`iSX1IsIts zLqAubcai;4`|i1YniFx~6zNgV6As2)SJ!`0W26Xfau4Kf8ni`xqKq(-K)Ymkpq*Zs zSPm=$;w%RRn-+-ue|3^(yxhMw68vbww2GF!->#$@M3`rHD%~<1;{wrfs9e9G?#nE} zJ>4S0!+qE3{(WgFC|h4;z3Y03jjb3qCG^5kYU`F=cly&vQ1PF~{G9#d@fpRXY3j5* ziAM6lD-06=dxBrU_)9CdHF+T%Rbt48zIFJ@#S`L%L`UNm9>lO;M+JUP`pSTxA9g(t z$sNe~m7gT}kG*`y|G&!KDM*yC>(VTovTfV8t4`T=ow9A)wr$(CZQHgn^-W*=(_hR* zcjisz#a*~6zh-3B2}p=2jR)&S>(Jsy4n6Ycjb|2x zR%qghd-Q4SJx94@CpXBP?fy@bD5xB*g(}khjpZguH4$xliXJ8?yJYq4whPdUjF>!6Eh}j2+mJq7awH|LhB~HspgaJVZdwY6%`d!HA z8thZtz1>*g`TmiSlnDhw%vP~K2!`Gf0Ec^a6AIuMv2tFrJWU(i0tjVEsRwvV@q$xy z0EK3~_11W}??#4ltY>xHL*_YeIOI~WCnO1HG-(8<`~5J)Cm&N|XmTe4j0Xt~LZzi_ za%->rrx@lVqInm>gz;d3riux6{U-hHcjX?Gq8n_7m>)MbNpS8dr1sT(&Y|Gru}D6F ze&8{+F?Wy*axi-+*!Fcc8>8G(g*xU6&5WHW|5ypjm&|Vo8AV!5M-FLOB#H&CHGCYc z4q5F{1;=wR?w{S&U5jNe+xw*V3>FKh%F06NCgJPiows+eRf<7H zM0i0#Ss6&DvW-CAtgNi4o|S;V6AG1y+-?p+t(G&(<=t3W@&b*5TubgpO+3A?m|yZ#rvESw=!%G25jldu^BL=MkN^Oe2FE`Rox9gO>>V+ zsj+D&8ENz#RHSZVvpMJDj1?CX>rKC2(b8_u#**1SzU#=S#`8e+@AnT>uRDv) zc4rQTXKOwgxL1~*2pBZ^jg(qiZhgxTk!Q$u7twjKLTgGIDbc+=hIdO|JU#&>RkA@p zKPLiF6uQWyi?=%w@OY>fX;50cZZZ0);#r@2a8#G`Y|x!2uCt%fF+Jiu@u9gO(0dyt zBBdbP&W+pW+ejtqSWPNJ zB>0R@7v}`@>3EjZ1iw3(gWyeFx3#z9=41)@VCyl)i%R|FKH%Me_^T`{3s(v~*Dw&J zM8bJdgsb0D5kXXP)pJV}Br8A(0_?I(&&U{G|IEXL#A)AIqWtKOUSV7R_G4+QZuCm~9z>#b2SHaT_HjSpOKU6o~1Sv~HX0Lss|bHGJ8 zk;4Yf9>t$P?w#UYRPkY0N5=4a^KT1J=$J;y@gK3nVnp1T^26q1P(D3!Om@GiQM$`; zximTCKZM{4>K_u3`u#P9(V|G}46h2il)}Gm9|2V3?G;d&J{s(8YWdz0aiLf5Sj7JI z9uxw3)qX^z4+rQ?B%B&&9)HI-V=yS1G&o)Bi?HJW0+O{VJso^ki}RoR5k8M z8^CGod;yt-Lwc zP;4t(J=n!Q;9_j#LIV2xxJaBcl|J)c$&2kD@}V!F`g2a>t{=YYqH;;S<6;FVVrZC| z`ifIJ5t<_Dm4BwFFTQy?s!%+s=0ONw5BppM**CD1eFNGe8#c=C9ugZC7PjJshH&m! z1ovG%|7mL3Kd}<0307dq%AD z$@L;<7TgNRl_90tbt=9O1uDY-^XXbLiRnz&j!#KNC0R8d269Su!0X{iNL!PC!XgQ4 zv$&MUmoaeQasZT36}zxrr|aP94jB)qI{K3bF4i`*eQ4Y3VyWT;!rDtia@*$Z^};Fl zdH`Fg{B!?FEXcUnoOL;d)G9GTQ!1dkTbpN{$Oo!>g1$(f%gz@(Tgl2B*c{A1K}xmm%OnkZ4;r)}T%|~5e<{-zcOO!*NmRrA zg4IzVS=B}-|L|&#r!w)y>)zhSmPh8vAnDXM@{COaPv97?o2-9vlvGL%9&1>Fe+X6N+QnH^CTOrWa#K<5j?W&*z|Pm z*#Sg$t-PEeSisnULcL2){c#zf2aq;IqE6R!4w5P-qT>39P;+V%>A`;XExz1TP8iS*N9reNlI|^!Ehw+Uv7;D77jIlF80cd zIb_&V@MFvI7_shi;H5fG$HzbQmSl<)N@{*yZM3b>$8=&y4Lr;as3-e58)~jr2&2uO zpY1Oz`r;fVN!Jwiqp)Q*fSwvB7MgCuVrqM@!UASw1&>_YU@0-Gc3DggLphMV*|}WR z=WhDl^1*4Io}+q9I+RO99i`dWcOfh(+3RG2HdgpZIENQr1_$8~r5r*Z(1$pO0LSS- zW$Xy4**rp2%;(kse+s<%D3;VQs|U<`7Be~r8C+_U|3PJ$j21y|OHb&%6G9Xny`5=v zgZE8xA&pFpxhu>(wDiCpA0K;Tyu`bnlq1#+=Yt+FHa(Vvjivl#qde%cVTlCMYDqjd zZqhne*J``Ba%gX*B@j#L!1+&D@{U^$!q#jg+ycdHJ<8`vGABfrk{Hlza3Wgaj{|Rx zT9*{GePqm}I;bXg3Sn*8YJ0tP^Th~pE36jkfj^}EJw#!1o}5Ur5Nphxv~hC$Ve&WD^oeoYs7=4*vB-HKo2i5$NyM1`+jC19xO=i$gvTVuy;*(TF(tbcN_Mg9 zI}1i!;nIbe8kf|`9xg;E`qdWlFJT}ol%zn>gF9^0^v9RP+@(ZF4BMx0Q1HOhhhL@f zlkTu4pWSLa%o435ciLENDr=IFn!57JSSyQ+n>!FMXAuU@$4N>RovIEWJ5}L%@Wf6R zeR|1s_{eiiTzSO+;d_(4r{=x=-B({2P&n(vzAyw zTbA?{#uu@Iu&3}<-@V6N=5N+{0SL@UP~Ka4l;BozTh{djigp;>`c-|5 zb4e`NBa1QO^5<^fg2yPvzzzoKJbhAWN+WFVIdB=mZn2pQ>jngZ{?J~)2k z?yhP6aWY58(O)MeDyPh7BD*6G6gRRf4pF{ZMNqtR4G|bBEYUr~l z8q}RoNrFPMf*d|8OU2q577WbSw+-QMWJQW0z>)+w?RQp^(9S=k_A4g`=PDh~=;-01 z_~|S{OI@GS-){>gF919!_k5y%T>){TfBts!)vDjTb?}7(@BF){h-qxx3}Cmf zWL*O;O#)0NRdxP|4sb=j`SoC9*1KRO3=IH&Y->}QM!m6tNgM%|4<3qlpUR4tjF>V}Bv#RN@88r{y z0k0y_+C_vr%2O%fa^+`XOAv-yN@kJx5^cG8wW5_g;?wFUSnEhcK;Xi6EL;f0N z1v5QT&w95FGZ|vc3%rV`j+#5Vw+Jx0W8It9C|?tpm_SSQK(Y18N_$yS-r>QI6xO8X z816I({(ebdvo36f^Sm?j^4W@4k=7l?PgZFr3X$9&?U&sMopknknn4$IoHNa@p;rjr zH%8op5d6YMcq*Nqnej=BsRQDu!&-*V&%b!zlim3qDU{JCgU`b+;G}aE+pnpq;R@qW zRFK=mz|!<2AWG&bWK%q#y#faVU_Z2MdoW!`;fX7$UH^jwcCw8oew1@51Cg0Y>LnW) zsaiqeZv6@3JoZR*rOqWn+jss<&Q(UQCwWy(>oY`3>DVn(IVTs zfni)4pR;jJCsA4;5*Xl=lAAfeZ!|HVXo06Uuz0GR%*a9I75lJg#L#nN^6#=My!~*t zVKiVJl@uyd>ANx6e%+;FAQaUT2ozEwG?kX@Lnr-k81mFM)ONbKR>L5iv?(Y5bHmbsKi$MG0-f98bIsZKzUS*7) zY@)^+uyT*ELE{p3Bo@5+sanmbFL2j49)a7@bqFa)D6W!k9VgbW>CQ>PHvkVvTI^=? zc-`06&wS2PljJu!{M@^JT+C;`CiJ4yiaM{Pto(Fjb|;4d=A}3qdug9ba^w&ekB^#I zn7ieEw>jj;v|;y?hc&sD{*wBk1Roa=!nzi^(o!-p9t5n~-^S)VSj83)m}_5Rf7ExE z{l-6Xo5EPo1BHgO#V`kU*hGzTzkuehl?eo8P}%*$8ND?*GXX`r7gUvFkf@`p zmO5vnD%E8_Gjs3lo7U77Y=giLFwL`8QX^| zhg|W%Ix`j4+J`)W;J~7KGH;9?^*x+o$4q+zds%i@x1i4SXSb$^&|7-do&5w(5@-LL zjau76&0PfZ zImm^2@cad&F*ZdV?-c633P7bApAT-ZlM^Y zm`e$B=2P3xwZrsL8yDJ#KA&~B3Hox2_fxmUm!KJv*=hCidsE4 zfBg?OUHgn{ACDnb~e8 zlx2z!Ia@>3-`@z)bh}u^pHv~$NOVz3N8;)|DJnjS$ql81$4No@12DUBzV}A>>jmA; z&hJ>ZAZVzl-dHy(Zv$^iN=m+jIwdb0hM||baPD94_Ti-;n%WGd<6G@dy4*u9CKeaU z_KYm}@8ZC_BtVM}9Ph{0%NW8F!V6o*=u*zw@8056baiBf*WD`HU_6aYZarhY^-$AM zSl*CpwoV~AMaHuX>Xkatmd%_2t?b&wO}#)?yI6j(O+y%D$LYYySW9{Lq(nPzZ>=to zczMA~Umt?5`w4=F2!#4oT@U)c$D}Y@U8b_9EsGw;50NYuQI!3Zvl4be@X;9uUalc| zPUco#UhW1&=cH?1$|vaHr6w+e4Kvfa{d{Mt_JAhC#Q3)|XnrA1{~nN%OG--Kywz~w zl~bNR`vv_qkG7Oc!k^?DeLT6}j7(cPBPQ=E}%>4lYcaMZ@i<`O}$prD=`2m{PC ze%R2`bJ&3wA5Ki=4+ymN1kUY;VZonJrf#TAg)<0JI&F>|MG2R}C}~6{Ar!E0^}f4F zx@mhosPi^-PW9;{7JFxB&tB54+mwHt*ek22PqZBx65XVERSs5H2XrJIT-r4X^f`!# z3gkG=5fKL$g}@?e$M%!@KDWr1PTYeI1qB70LfOwvIqNX&$^+TZ(uCSD@FclA>Q_G; zm@Kn3OBA$m%QWSB;bUrRNMIS?>fw19^z`|51`_(`T-u&ZU;u3v9~>pnlNSEwUBE`I zT50U!At?^o*1lGPNi{xzl)YN@Bx6yKlLyXsNy;>*wO`b-Gc9OY7Z#_LV1z^KUbc9? z{;o=(s0Fj$U|Jb>ZnLhn8xs9GN9`&%N9h?MlXG8-&f6K#*}$c8ec88ghZGHmI2qBt z#-dkE@%rJT!tcC4-f$o{00~S+jjJUCe_?j7EKkSW7L4e`<7+pzqVP4AfzL8{e-A}B z*o2X4cXyv86Kit^44pass2+gw`B(ndkW3M+t;YDysB?d9D%zbA@7gnBRM{>H#Gt9} zMZ#jq5O~h*^V0Q%K<_phCcWr(Ez&31C;Nqq>og(X2j18X3Fc<+5|a%Y<(pJL>bw7Z z-VmrY^}#_{#I}0-wJyhPPna0DXZ8gcXd6?#ra#0-hrfgZkM4SPv>u8_f05jr+kCLF zdfrsg`H6^#XnlQM>&anIh+GZ1r+k5;@1soBIW{N-Wh@H^R|obIqZ(Qyn4AaIz$e*} z{V;PRrCL6EyS2?;oLS6%$fQq!z3_Z3&u#xyf>g`rLnl=%^yeaHhdv^%2TI#_^F0U2 zkd?JrgOm_aA2NJ*U1JU+RUdgnC4c2n30{Di@@<*&ZJb&x&WUk1s8GoZU zFPSZN=yGr*MBHqbr3S>3Wp1BZtgIlOFa({TBN^v ze$O*EsU%}NM{jBC2}B=|5vF4z#lSwLF6wiUZ5MA9lF)$BF+bNxNB1$8nmsEcwSKi^ zi{%pI6Tr}|)QxOD$?A^k31jLS_0Jxr78W}X^yrfd66>wl7&thVN|t%rlaL_;TZd|H z_ADs5H~sE|Nx6L)@FOa*+dgmSTf(T@p77zQf~ADt42xh65fOdW6++N1*PVj#_G{~F zB*Hvr-|tUJ?$-{tMye#zfi~v$T^^IzSUpOJKK~@k<=~<+>K9`nJ>8w0oK_^XTdB(- zW`$auRkF>2Jx{Y?m;F>t4sU_uUJpkzyXI?bxxq2+1VzfxHBp(U4r%JV%XWat+rPxv zm=5?Ndi&jHY|w(6F|x<#szV}nVo+vRR$SEaNFs_;=v7aS4~T@UBPFkC9m!y7LxzUp zj;V1wJlwk-21JsQI6_EAwjI|0%>mWRobRVgNflCabxWg0`^hBhe0-Rco`|5AZIW20 zr{)6NB^zwCP4q%mf$?sEl}eOdX$_Sa;lx8<9}f~Ricdvl3&wNBw5CX@^Il7c1UGZx zaWxSfTRKZ|OiVTz_=#@=RKCRwOZ&JYPH-#S(CpjQBD_6Sd*{5uY=!pUZ9ybL+8Mao%Ub@qRViQhQ*KSE+MS zH0ItyQfYG89=+sdU~BRPsu$}xqw~(tOj*T`=(XN{P??QnLCj2@`DwvO=s0NAGF)F3 zbGUobQ55eZUkjX#7~>RH7P3uglTE3HK%$fp@%QTQ$ix%C!q~p-fa+-3HC1EbE9GjU zxM{p`zkO&f#Q5=9VPHzkD#yp)7<73oT$O-P{~YvsoicB*I6WbxP}qltNCRms6j(-` z1<81gjBi%k)A~a7yJ!tyd^2btH&0J%_PsVLbeZX2+ZS^HNNzT`M5HV)6ae|uRO#wl zc1|Y%wQtfs?4F#o{OP230grve@4+kF3VKfpR8qn@f*P9*0nh`mb2#jF28<2`=i67) z78-y+CuPD60ca^igK`ZkVE9 z*|ztgk56>-^weU}^qchD5l%$~_`!+~3=7a9jt|VqGB%=_b3gWUc6R<77+CJ+d4s3X z$H$#WkXID-M-0AK#aQQ|T6B5&g^hw?@^H@_FUc%z&X<~=ic~-EslDM8y?#@H-Bb!= zmUKPC+Ja3ZM-sm=!XnJoY-klJCxt|iKC+H_LEbZ??uUmSqgpmrEH5DptGu!>qZW>%PhRBnUGd85G)u0EF(eASYjW3(I6~92ykqci9BlAbF2sr%&e1WrA}`K28-Kt*^-DyIGu~_A8~DpDl?z9)ZK3I zB0nS&Jo10Th?<488Q`lfyvMN9k?Hn1wmP;f-{AZ}L)n0Id5fwL2eqGj=V*QTT_p;K5c`*Jr~9KwbFu`j;U&6@r+xDQ^w3(p>P~lg+)WE}pYD z*bSZb=;-L<8U`th0V5b2@0&@!AZw>~m9j;@^3>mX0jTI@<%r$3&N7&WkoZx|yRMl& zc>Hba=;^t;N2h}Xu-7NI8_b2T+Ei8_(;6>ZgN{{0ZEXOY z{H#2tfQMM*vsWXKFp8v|kQDotn#6)JKv3-(x7@3~C>x^|M-~~%uSj|*7;Jz2x}2N) z3F6Yp-Q%}JPG2k(`Q}IYaL;5@W#%^29o-t$EsB}zY#giUUNx_>}DjVGEzH^Ta>UjV_^43k)z>L5;9 zu)O574A_R@imWu9fTN?>xwdZv%dzori~yGHwApq(4qNg!PmOk9^n@B&FcM>aBT%&; zCN%|U#~%oJAIc8^>OHmnd9e&^CZs2eQ0yG5fif^z1=ji&`EZj@t_?S_;(zSL(V~)j z4rg4NU`t2L1iCA1Y~CMNwQ`BP|Gj{{RD*_wybuV$Z!@gbFgfe5l14wZK7ZQ_(1`OB z2nYw4fA2{IXf4+~PtKsXBwqQFz4~G*YVy63efBeraX9L5EaGX)iirVG1_64yxtGs* z`vOI<=m56Ldm^G9ot$Uzlo8HeNl9s4wB6J4@3&Y%r6q0q00_2L|NA^ECq^F98-)pw z4X|Pf1o@hP(^6&5TSQ*_ES@VP^l(kdjazI!KxxF-yL&t712g7ew)hTow4gW&5Y9Rds7ha{z%o=xPPs? zN&#h@d5ltD_spVxy`p}V^kc?gN^9sxWCCHEi}6Mjn}0xKwbXin{%VYHds3)S*lag{ zE90AfmZ-~IeoU-7vsEfE3pR*R$f4My*ZqPKrl&g9!gKwL1mN(uKOMiHn3i_CDV@!9 zDo@kBX|glJL`esho)SJf_fWs(pi7J8VqL7aJtz*VIp@X9)a--u@-a`-f5oNP|I7Z57WFjV_B<)$)XcC#2(3ND!l(=Hof;`*nh!{r z&C1gWg0mYyBARbWlN2>l6o(kC>yV^W=U?_mKzQT;y&Zc!7qS9m+sS&NM~ZAHC+$Cy zrf9r8+Yx)(c5w0yolkmGgUhR?wHq3-1Dq3`M-^@FY5Avy(7Qgl6ZG392uYIucrYug zFbqxHaaGTYwwP$tR#9)w@X~{@aq|XhgyOi5EpKD_8*FHwy#XQI(Y0nzHRrAmgiwHZ zpT4Ahc3o={{;G=dik5=i@<;~un1DOri$U}Lsc_lmjkCO|*yN-l`!|rI<5_S7A=RMf zW{~&+EjJE{u_+oF8azu%OP}x8S_xE7Fq6ZEv!wO)^_l0Mz%M-1pVONVHylphMvVTZ zQ|VJ!7uIK<1FWAXDzjJBCZ>tAHH<%a4yn`6+=K4(B>7S2+>8K;@Q#`0mSP5A2vX~V zhcZU(@rfHJuXS)vsDxUk+pUFv*%mS&kVNDouDis6fm^t*rx^xZW*{m4b;DP}<3Z%x zU75jyDfO}%65}tQ8O(2bO>1RkJoZs(QyC_=WDZ6fN}lq?x9)nDwPp9n<7?(Dgg#u! zVVi2TgR>I#Iofs)M!st{A;9!;oL_huRw3U4*@nywp~7zwn9t= z^f4`-KWUjcDe%xo867z+E2Q&xOvro?^dN+IUIljSN*h=&1FEmLC>uJn5fCeWq={6=)_3 z2^vfjhWMuK9X&IBuK4b(mlp_DPhCq2fW*q5I}>+zcaP8%yWhDnSbt>9%+AW1a*X6O zh#acMtQkH6>*jYDV}`$vw@xqRi>bo*VGf^|EQ}>*@Jf zPxTFVmlm5zwbZ60B^e>3l^n2^TLsqZ0WkdO-TW4Nu{!b0Z*8H+?_US02f@ezt;%y9 zCC0h~x8iy2nnqH${)sTB|4G9pjg0im+$WsMm&0nMD=0hqjv>g)lO_puCP$Id5DA_n zZR-3ac)WAj9R3V>9##lv7$U!>6QxZJ*NvlWxM2%aBqZ@45ZWngX4dP#to9sbS_HB? zb*kNJhV$;c?P?Te;Mn-h~v0`Q*t=6?ZVvekXvVi=tsz8f#LN=lv6d1 zW=gf0a+U^{l6lD+`bXvD{4z8Cly7Tl{v*pTgs#J#v^c*WR-7RLfvsY|5pcUWo}%%c zHw)C!`g(gP@pBN86&;xpuh|;y&YSA<4CkKGa@E4vQxq{izL4`~C%nt6skzxcr}y(N z`tk$Q_)!_EMJX1vEW8 z_!JPSCDqp6imk0R)(vju+Mb)6yHb{EkPBZLJvIFrEAkyQzk+!$GmUsgzZ%5t*1gh) zrp(7FkCl@1;q32z=6fS`5#waq_GF>?zKqf7FwJe~d;?t~I#U%O*9w{OlGJ_z-XOAA z_^@$_qTmQdlX=Yh=Z@m+t=;qEv0d76sI9N2jTaF+cvS+M+1KZ6XkBnUHU7NnH!o*} z-D&w8<*W~1y$0lDzurP*-Jit*M5i4B-=uKEQ6D>l477KIdAG8kLCJ|C9su zF+|sau#@g@{=#Ty~WGnzHo&B9jK3ifsq%E1pqFv>Uwe3a|M}X zrouT34B3lb1JH1q2Us0hPe1Bgfg?c=qH*(?ASTo2X>z?GmQ~_=>{TzVvh*n+u!5rC ztN9#JAjH5lLwMoWN82_k_rP1wPnsJ5sSDUh44%li^#Hz_&h!ti9$+Q{uvtWxq>B9y^#8wupXaW z8Fsc-YPT4GFaZ<**LXgZ1@J;5wucHN^Xd7!mceOjRt!S0>JM!IrZM<@X=BJbVqC?E zhBP5$u&RNABGJmSWm<<(v3rYGE|&A{USVGcAYKG$kS3>xeq3waolU%uuSe#5mw?gG z-P3jiW!DDpL5}W&3RmnO`Je<-IH;)^+S<8;Ad;;VY%8K&^avVD%u}JL>)%K zepLVhZ#ju$FbgXlp7kspZ$X0RMvPL%TR~n=5kABd9q>178G{ReZ*@^v$-46yGjnTi znB-WN&UT*TS$geds%smiPBAR0P(I){%(0m9GT(^@ zzuT20%KPdl<~#<3V6j7=m?+7&^Cc?k2d_6H0s?}CZ)_4#(Gd!b$*o@-Otr2quU&`S zXiL~E)BN4;5uX0kUYN$d^fYQ@zp$Eo_?Y2hE6A&WY*Uh?W8+zOW_(E;7ZtWqqnh+Y za*e_CXZ}gFZ=EkztsBY2bnBgMo-8SEF7=Ph*NqV>C6YO$^A4YwLU=pRY9TXQKzM0j zX4=ONFgtA9SH~dRdyipTDBe^Yd2vLEpuMDQ4Cw4GD1XR9qhBDo{un?Al@*iz4R>z6 zZ|pr1&O0LE7I!p6(*t&n1tMn_i1k$^3hPdASiHF9{w!l)vCcQF`Ipx~WyP3LzJ%jv zLV{lP!XnopD(7x^uuE&A5!c^<$`_sh@On0}O9wHT0bn3`9vK15W7?thkS9-ugRqOa1)tlp>F!a941-aoC#u-NV%0Er-;o1*~+qi zGy$6hT42t!Q(Nx5q9_oEd&2m02$Jn|PJU7DZPFJo=^#3F&z;cvbxpjcKUQPZK(vvJ z%msYi)DWnE6X*jG*J2p~dZ_ zqw1(r>jxkUTj*#R)3}UhllW^rJtGy|6vqOq`Id$s&DUC`>H1Adt;xF62u6u54Q{;} z9Y3h&@u|9c-?Hc1u;Y=gThAD)84<3SdJ{@#r=x^By5u{~^D325<8H;GhjkmQyDKN( z*wxkL_mF|{n^-F*97rE*WT5is9ns@0v8Es#B_}RZ`kVdH`KC9=HqRLMZTeUx7areW zRSl6Ue|^76gMyF*p_YocY(mSvFg?)BkhaGiRN^6Ad~&ryG0+roV)g~T6!87N#>Sm2 zT~m|x0oz4KdmNpQq$p?pKmt924b*2(KpYre6uyB1yQ`Ttjte-5-5&t zRzG{kKu-W56IO5mx-i&nSo%-r%?xUu4roFC_2idhy04yImnT7eOUot^Ar@u% zE>>U1mde5RtG8gWUCa2(J_u^9jj$4nXB9ilLR09EX7!eM=PCdM&1^pNL*sqGXB`A1 zAU%kLcT!eX7nO8mdZ4;pxbybj#KaRl!Aa2Y99?r9pFs~#jZuE2Xu%sLHfaF>3V>sw zqp>q8rAeCaUT-&cLKg36D(W9GAn=`WCM#*u6K{4zw2HX|i+@c_DcA~wqEQpdo%fda zp6;&VoC3U_i1B~Uo&)pd>)*hSlFk7qGpWNU0@7-_0ECt`j{lh$up-)JyfjSIx98U0 z$|Q1jc6Z}S^s(E%%UxU_8w1IAJS0fFaJV4ncD>4l7gtr$r~$)*j6a-&Tm5s_!aKy7vyacKeBoxE9^Mny7L*4!-# z5Lv%=B7yC6Q0J9>Fj?4Gk-MmxTMf9(chdY^=oFWRh*Rj$a6}r2N(X3k+{Y*F_EXJNUF1e99{Je(Q?YK~8%2hma^#yq_W2D{UorVFdlnSH5w|TFIjKXOU_nTKR9YSI1)sFc^&V z;Kb9M)u|NdD(7W?52iUgpiaK{7H8ujBBTnv0Q$w5H0HJh2c4a|rkCnp|CU=oTsfce zcJi!_`hn%L4VXzx02c_?P_s_}$Q)PaYSSrUY7~UW-xZG0e_EG@NqsC8nV9gxp$jBl zv>nKAgsA>`WTQTar^kVFV*1BFfq@;wQQQ&(g0=s#Huyk8zP&yE_D1xj>6)wCl#=?M zjzp2mjZIDv^7I%kP8-d(-grxqtCoaMKykG6 z#sR17|InI+q2sG2G`?%{X;l`5(2XRO!*;SyFP>WF=3R~4*1iVD0{0oH66rg$X0z~W&zf<+28D$s?`(iljCNv@%qMwc1!)X-#CH&E`9AKX zwVMa&2cGwl$~hB|)(|ijQTH6p@Y$@llH`Xq6Aqo;9#0#7!98Fw{ukoUOvmy&cZRBq zvWd%H^QINl5pxa7ZOxiO<3U~5Qfx9DL|BK@52*nt*+9qnZN+(H+L1=NR+>8LilLVH z?t$8*`l|~ERXVJN&rD+^Z%n18)!OOWgrVZ?2ubRD6OWgi;=An=HO<)Gkz{v-^vw`@ zDF;IP7pr_OGVr1=Rys8!)_6Q=tGSe(J6nHnaKCGjiebUAqkfpe;lZipV2OMyj;IC) zmG)OZ>AqDRfI`12lZI*!zJg(2Mnv^H+t(%a z5S;{bv88Icm>?AQ3L7373BCQ(k(k{?m(jDt)L8fPlMyxbdySsqrCeMO5;Ochs!P>* zdx9&+;*STb#2@cQrk|1kOy*<3StD^2G&I$PhRoeJIX~;LmAPrXkp_2t4UKhy_M!V1 z#BN?4G_r)bS^9+c0&6gJ1A_x(`ImY5zc}U9GSq$(vF7)sU}<3@H&`mgq78~Vk-om4 zB{ta1(9zQd6u_8Mj0>kOMzPjOVd*Pcb=bPiCNLub${jp}a`B(|TT!?ZUc4(;a#Zi9 z{D{|Lj!`#P5p)l2iHZ3vyaP{XvrJx4)3^C}0p|WfT=qjP8xQap|h4HV13<*}JmxL{76>dZ&LW>&W4YZtao}R-Y%?ag|T%(|8)ez5u z3sN`>+feN2S+rx~fdXFjdA#;_kZT1}+sVJ?yUaka*raG!gHg-E)id8zfOhQYa}|wf zW2PAEsw96oa;JAnk!ZH%qU}j(Ay(DMD5d{Sz&eNE2l$guAl$K;nJ^nrm)xkz&w>zK z?_&eI!LHPwerAOVKOw>(Ov!!uhZZ1qey}f}a&U#_b$;$0F=A-o5MzR+oN0#^AtYDV zsSe(+WS2!5cU@Yi_L#E4#B<)Pw7=Qm?~660G!Qr$1sgF(ZI@Vot`?dpGT&d68o3YN zP5Kq%Qu(JZ2f{v~^po*Xq-T%|mE)w9gcL|*1yz^i1EW(n+;?p+CR=XTa3RT!&ocW0XZ&Mvtd`S!0Y54JRh}rur=CnKP*{e=x(xFQ z$`vdP^y%YOxeD;+EC`uqPDmlKc9}A43HyksW6&|jnwU8-y&JU0&@U#Srwax}#ME4= zd*_;2O8u0i(Jw|qdiFpA`SDM5(Jn01F5Hd_jM^5q^Hy!)O0a*b8!`~t>Qh@6OpWl3 zhU`tymNQx?+P|wPTh-wP&y})TT+r5Kj&Xm6x(Ez(c`sYwNDKaLoN4LFvPi-OO3T?k zH^kXTz}uM}SjH1isyd@|f)>$NhCT@ewjNx2>sV{6Dy&~KeqTzQak*T`WQHEB${bge zF1X=%hnLss&sPHGtgUaSJb6-D_uE{dNfssv|K6|tlKokK@nm(==TR|#e_xbCV)3=h zn+9xHyNT?lp6`0ggFW|`N$a0+_E;GFJ4hGpzLAG(;+4i|j(zyXeI>4_cPUKp`!|$P zjClUS(2^0-{%H{wp+sl~1_mZeW|?Y(;=Bcj1_xVddY+AyHFRFZvqLe^wXM8Zrvd_o z#s?3t#BW$CF9TkscQTaq2c07z zl&k7BR;Ho;^(sKC4RcJ8wy8qFt?DfqiMC6vvm^f=2vb2+o+!a*Sx>RyXsX8Pa^Z@t zf9R(*fmQY^_D4#TK?AekYhOb6vMk&5&PRt!FKJ7{{b%0&>5lT`$)}}rSGh6~QRL4w zIlmM{l}%zb#iL3DO&0ExCq+tqu!HGeTG67bt1I3NH^l9&VP;mAwwAWhyjy*fP6V_smwHLBBQ2qw^Iw%lgKh6V} z3gS9Hubj(%cQ-HLNl^9d(mtpi_-Ee8+A`_+-`p^sUU4)c0Ql8lk>$s{(-}g=JbAWR zC5%$NvK35!Uha=Gt{jS!8-M}Zm8&oCvL_Bn(MC}L2Q&m83PCA4qIcZT@cGcNFsqn# zWCK<&So6*4=Ma+^)P;h+a6&jq6;&1E?n>04ij#PtVt6A(tr0qELjUf2};~ zs+-G!;M1Fl+0b<RAyA}0{=>mOy4Sk3u#d6_;H%|&TMQ`+Mc@e`oT{v$C@O=I7_%-`{6_ z{E*Vunj5=uA=ndg7o;je z1EuZ0>YAKBA()q-&nclRx98^>iOI-D=GQr)%`LiTGi2a^_@nJ9lj7qM$L$`J`ywpU z89YNw*L4dY`_y2}?I_v?R|Z{T0D6>{vRFd9K#Ijr>e$Pm)OQ;E#auxjB+xPc+}_%D z#aMX=snjA;wdM;kx=}*7g+HjZ#}or^(V1Vbo6{ELpcxQNA5qKyX`GYhBDK4GEU9_e z!$?G@NOfXwmo3R*^zfTHGqKPO+lZO`3y}kXEo^X;e%ds99W0KC@Hqu81ZL>*)y5_j zLQ8EhLL3y3J2j_$%h}=cS+WiszYmFX&aYx7L^1%#h^6!ezUNL1#G(t|Y`KyXrY7m9 zgRrb1)$l)mG*&6B%22aemHP(UVwOFY!Cr2{}D&`9+8)iK!?AD8rW) zXz->Hhi*Q@uksF({w1qT_Kk3K95c=({pBDJ-&pMdabfk6XCK+4(57OF zDKpWxiS`Vz?oR~B*URb8QJl{3DGGJv5OXiI zz0XgY-qP#S!-Y?9t?pQY-1iLgZix2}&-=}sPt3Q+<4(a3?th9(M9CguOVk4x_!6=O z`FP{xlWF4_-M)JM3s+7^pCKOpiz`3)MS}hZ%5G)ksQ14>pK$^)8~E@+KN3S6e|39} zJqLzW;FMXzmhcBgM!5ab@!c|U9pPl6wa*t=-nBbr&>BQ`-%K||c+GA7Ml>JDi zprHRs_y#X(9GTfL7DU-?oZiuBvRCqKsLD5?*LVQ>hb?mDSIe@ zsCEEDo6hkb1;{_ULl@fj+R0^#he`V13jh6p{EtGC*Z=2nk&^`Z#mfT#fd1}9zl{;``On$^1Kl$S AUH||9 literal 0 HcmV?d00001 diff --git a/LIMITS.md b/docs/v2-LIMITS.md similarity index 90% rename from LIMITS.md rename to docs/v2-LIMITS.md index 9c4b8c0..849a155 100644 --- a/LIMITS.md +++ b/docs/v2-LIMITS.md @@ -26,7 +26,7 @@ Known differences - 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). -- Because node.js stream doesn't expose a [*disturbed*](https://fetch.spec.whatwg.org/#concept-readablestream-disturbed) property like Stream spec, using a consumed stream for `new Response(body)` will not set `bodyUsed` flag correctly. +- Because Node.js stream doesn't expose a [*disturbed*](https://fetch.spec.whatwg.org/#concept-readablestream-disturbed) property like Stream spec, using a consumed stream for `new Response(body)` will not set `bodyUsed` flag correctly. [readable-stream]: https://nodejs.org/api/stream.html#stream_readable_streams -[ERROR-HANDLING.md]: https://github.com/bitinn/node-fetch/blob/master/ERROR-HANDLING.md +[ERROR-HANDLING.md]: https://github.com/node-fetch/node-fetch/blob/master/docs/ERROR-HANDLING.md diff --git a/UPGRADE-GUIDE.md b/docs/v2-UPGRADE-GUIDE.md similarity index 95% rename from UPGRADE-GUIDE.md rename to docs/v2-UPGRADE-GUIDE.md index 22aab74..3660dfb 100644 --- a/UPGRADE-GUIDE.md +++ b/docs/v2-UPGRADE-GUIDE.md @@ -45,7 +45,7 @@ spec-compliant. These changes are done in conjunction with GitHub's const headers = new Headers({ 'Abc': 'string', - 'Multi': [ 'header1', 'header2' ] + 'Multi': ['header1', 'header2'] }); // before after @@ -63,14 +63,14 @@ headers.get('Multi') => headers.get('Multi') => const headers = new Headers({ 'Abc': 'string', - 'Multi': [ 'header1', 'header2' ] + 'Multi': ['header1', 'header2'] }); // before after headers.getAll('Multi') => headers.getAll('Multi') => [ 'header1', 'header2' ]; throws ReferenceError headers.get('Multi').split(',') => - [ 'header1', 'header2' ]; + ['header1', 'header2']; ////////////////////////////////////////////////////////////////////////////// @@ -91,7 +91,7 @@ headers.get(undefined) headers.get(undefined) const headers = new Headers(); headers.set('Héy', 'ok'); // now throws headers.get('Héy'); // now throws -new Headers({ 'Héy': 'ok' }); // now throws +new Headers({'Héy': 'ok'}); // now throws ``` ## Node.js v0.x support dropped diff --git a/docs/v3-LIMITS.md b/docs/v3-LIMITS.md new file mode 100644 index 0000000..3e630e9 --- /dev/null +++ b/docs/v3-LIMITS.md @@ -0,0 +1,31 @@ + +Known differences +================= + +*As of 3.x release* + +- Topics such as Cross-Origin, Content Security Policy, Mixed Content, Service Workers are ignored, given our server-side context. + +- On the upside, there are no forbidden headers. + +- `res.url` contains the final url when following redirects. + +- For convenience, `res.body` is a Node.js [Readable stream][readable-stream], so decoding can be handled independently. + +- Similarly, `req.body` can either be `null`, a buffer or a Readable stream. + +- Also, you can handle rejected fetch requests through checking `err.type` and `err.code`. See [ERROR-HANDLING.md][] for more info. + +- Only support `res.text()`, `res.json()`, `res.blob()`, `res.arraybuffer()`, `res.buffer()` + +- There is currently no built-in caching, as server-side caching varies by use-cases. + +- Current implementation lacks server-side cookie store, you will need to extract `Set-Cookie` headers manually. + +- If you are using `res.clone()` and writing an isomorphic app, note that stream on Node.js have a smaller internal buffer size (16Kb, aka `highWaterMark`) from client-side browsers (>1Mb, not consistent across browsers). Learn [how to get around this][highwatermark-fix]. + +- Because Node.js stream doesn't expose a [*disturbed*](https://fetch.spec.whatwg.org/#concept-readablestream-disturbed) property like Stream spec, using a consumed stream for `new Response(body)` will not set `bodyUsed` flag correctly. + +[readable-stream]: https://nodejs.org/api/stream.html#stream_readable_streams +[ERROR-HANDLING.md]: https://github.com/node-fetch/node-fetch/blob/master/docs/ERROR-HANDLING.md +[highwatermark-fix]: https://github.com/node-fetch/node-fetch/blob/master/README.md#custom-highwatermark diff --git a/docs/v3-UPGRADE-GUIDE.md b/docs/v3-UPGRADE-GUIDE.md new file mode 100644 index 0000000..f49ce92 --- /dev/null +++ b/docs/v3-UPGRADE-GUIDE.md @@ -0,0 +1,110 @@ +# Upgrade to node-fetch v3.x + +node-fetch v3.x brings about many changes that increase the compliance of +WHATWG's [Fetch Standard][whatwg-fetch]. However, many of these changes mean +that apps written for node-fetch v2.x needs to be updated to work with +node-fetch v3.x and be conformant with the Fetch Standard. This document helps +you make this transition. + +Note that this document is not an exhaustive list of all changes made in v3.x, +but rather that of the most important breaking changes. See our [changelog] for +other comparatively minor modifications. + +- [Breaking Changes](#breaking) +- [Enhancements](#enhancements) + +--- + + + +# Breaking Changes + +## Minimum supported Node.js version is now 10 + +Since Node.js will deprecate version 8 at the end of 2019, we decided that node-fetch v3.x will not only drop support for Node.js 4 and 6 (which were supported in v2.x), but also for Node.js 8. We strongly encourage you to upgrade, if you still haven't done so. Check out Node.js' official [LTS plan] for more information on Node.js' support lifetime. + +## `Response.statusText` no longer sets a default message derived from the HTTP status code + +If the server didn't respond with status text, node-fetch would set a default message derived from the HTTP status code. This behavior was not spec-compliant and now the `statusText` will remain blank instead. + +## Dropped the `browser` field in package.json + +Prior to v3.x, we included a `browser` field in the package.json file. Since node-fetch is intended to be used on the server, we have removed this field. If you are using node-fetch client-side, consider switching to something like [cross-fetch]. + +## Dropped the `res.textConverted()` function + +If you want charset encoding detection, please use the [fetch-charset-detection] package ([documentation][fetch-charset-detection-docs]). + +```js +const fetch = require("node-fetch"); +const convertBody = require("fetch-charset-detection"); + +fetch("https://somewebsite.com").then(res => { + const text = convertBody(res.buffer(), res.headers); +}); +``` + +## JSON parsing errors from `res.json()` are of type `SyntaxError` instead of `FetchError` + +When attempting to parse invalid json via `res.json()`, a `SyntaxError` will now be thrown instead of a `FetchError` to align better with the spec. + +```js +const fetch = require("node-fetch"); + +fetch("https://somewebsitereturninginvalidjson.com").then(res => res.json()) +// Throws 'Uncaught SyntaxError: Unexpected end of JSON input' or similar. +``` + +## A stream pipeline is now used to forward errors + +If you are listening for errors via `res.body.on('error', () => ...)`, replace it with `res.body.once('error', () => ...)` so that your callback is not [fired twice](https://github.com/node-fetch/node-fetch/issues/668#issuecomment-569386115) in NodeJS >=13.5. + +## `req.body` can no longer be a string + +We are working towards changing body to become either null or a stream. + +## Changed default user agent + +The default user agent has been changed from `node-fetch/1.0 (+https://github.com/node-fetch/node-fetch)` to `node-fetch (+https://github.com/node-fetch/node-fetch)`. + +## Arbitrary URLs are no longer supported + +Since in 3.x we are using the WHATWG's `new URL()`, arbitrary URL parsing will fail due to lack of base. + +# Enhancements + +## Data URI support + +Previously, node-fetch only supported http url scheme. However, the Fetch Standard recently introduced the `data:` URI support. Following the specification, we implemented this feature in v3.x. Read more about `data:` URLs [here][data-url]. + +## New & exposed Blob implementation + +Blob implementation is now [fetch-blob] and hence is exposed, unlikely previously, where Blob type was only internal and not exported. + +## Better UTF-8 URL handling + +We now use the new Node.js [WHATWG-compliant URL API][whatwg-nodejs-url], so UTF-8 URLs are handled properly. + +## Request errors are now piped using `stream.pipeline` + +Since the v3.x required at least Node.js 10, we can utilise the new API. + +## Creating Request/Response objects with relative URLs is no longer supported + +We introduced Node.js `new URL()` API in 3.x, because it offers better UTF-8 support and is WHATWG URL compatible. The drawback is, given current limit of the API (nodejs/node#12682), it's not possible to support relative URL parsing without hacks. +Due to the lack of a browsing context in Node.js, we opted to drop support for relative URLs on Request/Response object, and it will now throw errors if you do so. +The main `fetch()` function will support absolute URLs and data url. + +## Bundled TypeScript types + +Since v3.x you no longer need to install `@types/node-fetch` package in order to use `node-fetch` with TypeScript. + +[whatwg-fetch]: https://fetch.spec.whatwg.org/ +[data-url]: https://fetch.spec.whatwg.org/#data-url-processor +[LTS plan]: https://github.com/nodejs/LTS#lts-plan +[cross-fetch]: https://github.com/lquixada/cross-fetch +[fetch-charset-detection]: https://github.com/Richienb/fetch-charset-detection +[fetch-charset-detection-docs]: https://richienb.github.io/fetch-charset-detection/globals.html#convertbody +[fetch-blob]: https://github.com/bitinn/fetch-blob#readme +[whatwg-nodejs-url]: https://nodejs.org/api/url.html#url_the_whatwg_url_api +[changelog]: CHANGELOG.md diff --git a/example.js b/example.js new file mode 100644 index 0000000..ba41eda --- /dev/null +++ b/example.js @@ -0,0 +1,27 @@ +const fetch = require('node-fetch'); + +// Plain text or HTML +fetch('https://github.com/') + .then(res => res.text()) + .then(body => console.log(body)); + +// JSON +fetch('https://api.github.com/users/github') + .then(res => res.json()) + .then(json => console.log(json)); + +// Simple Post +fetch('https://httpbin.org/post', {method: 'POST', body: 'a=1'}) + .then(res => res.json()) + .then(json => console.log(json)); + +// Post with JSON +const body = {a: 1}; + +fetch('https://httpbin.org/post', { + method: 'post', + body: JSON.stringify(body), + headers: {'Content-Type': 'application/json'} +}) + .then(res => res.json()) + .then(json => console.log(json)); diff --git a/externals.d.ts b/externals.d.ts new file mode 100644 index 0000000..6162660 --- /dev/null +++ b/externals.d.ts @@ -0,0 +1,21 @@ +// `AbortSignal` is defined here to prevent a dependency on a particular +// implementation like the `abort-controller` package, and to avoid requiring +// the `dom` library in `tsconfig.json`. + +export interface AbortSignal { + aborted: boolean; + + addEventListener: (type: 'abort', listener: ((this: AbortSignal, event: any) => any), options?: boolean | { + capture?: boolean; + once?: boolean; + passive?: boolean; + }) => void; + + removeEventListener: (type: 'abort', listener: ((this: AbortSignal, event: any) => any), options?: boolean | { + capture?: boolean; + }) => void; + + dispatchEvent: (event: any) => boolean; + + onabort?: null | ((this: AbortSignal, event: any) => void); +} diff --git a/index.d.ts b/index.d.ts new file mode 100644 index 0000000..236316e --- /dev/null +++ b/index.d.ts @@ -0,0 +1,220 @@ +// Prior contributors: Torsten Werner +// Niklas Lindgren +// Vinay Bedre +// Antonio Román +// Andrew Leedham +// Jason Li +// Brandon Wilson +// Steve Faulkner + +/// + +import {Agent} from 'http'; +import {AbortSignal} from '../externals'; + +export class Request extends Body { + method: string; + redirect: RequestRedirect; + referrer: string; + url: string; + + // Node-fetch extensions to the whatwg/fetch spec + agent?: Agent | ((parsedUrl: URL) => Agent); + compress: boolean; + counter: number; + follow: number; + hostname: string; + port?: number; + protocol: string; + size: number; + timeout: number; + highWaterMark?: number; + + context: RequestContext; + headers: Headers; + constructor(input: string | { href: string } | Request, init?: RequestInit); + static redirect(url: string, status?: number): Response; + clone(): Request; +} + +export interface RequestInit { + // Whatwg/fetch standard options + body?: BodyInit; + headers?: HeadersInit; + method?: string; + redirect?: RequestRedirect; + signal?: AbortSignal | null; + + // Node-fetch extensions + agent?: Agent | ((parsedUrl: URL) => Agent); // =null http.Agent instance, allows custom proxy, certificate etc. + compress?: boolean; // =true support gzip/deflate content encoding. false to disable + follow?: number; // =20 maximum redirect count. 0 to not follow redirect + size?: number; // =0 maximum response body size in bytes. 0 to disable + timeout?: number; // =0 req/res timeout in ms, it resets on redirect. 0 to disable (OS limit applies) + highWaterMark?: number; // =16384 the maximum number of bytes to store in the internal buffer before ceasing to read from the underlying resource. + + // node-fetch does not support mode, cache or credentials options +} + +export type RequestContext = + 'audio' + | 'beacon' + | 'cspreport' + | 'download' + | 'embed' + | 'eventsource' + | 'favicon' + | 'fetch' + | 'font' + | 'form' + | 'frame' + | 'hyperlink' + | 'iframe' + | 'image' + | 'imageset' + | 'import' + | 'internal' + | 'location' + | 'manifest' + | 'object' + | 'ping' + | 'plugin' + | 'prefetch' + | 'script' + | 'serviceworker' + | 'sharedworker' + | 'style' + | 'subresource' + | 'track' + | 'video' + | 'worker' + | 'xmlhttprequest' + | 'xslt'; +export type RequestMode = 'cors' | 'no-cors' | 'same-origin'; +export type RequestRedirect = 'error' | 'follow' | 'manual'; +export type RequestCredentials = 'omit' | 'include' | 'same-origin'; + +export type RequestCache = + 'default' + | 'force-cache' + | 'no-cache' + | 'no-store' + | 'only-if-cached' + | 'reload'; + +export class Headers implements Iterable<[string, string]> { + constructor(init?: HeadersInit); + forEach(callback: (value: string, name: string) => void): void; + append(name: string, value: string): void; + delete(name: string): void; + get(name: string): string | null; + getAll(name: string): string[]; + has(name: string): boolean; + raw(): { [k: string]: string[] }; + set(name: string, value: string): void; + + // Iterator methods + entries(): Iterator<[string, string]>; + keys(): Iterator; + values(): Iterator<[string]>; + [Symbol.iterator](): Iterator<[string, string]>; +} + +type BlobPart = ArrayBuffer | ArrayBufferView | Blob | string; + +interface BlobOptions { + type?: string; + endings?: 'transparent' | 'native'; +} + +export class Blob { + readonly type: string; + readonly size: number; + constructor(blobParts?: BlobPart[], options?: BlobOptions); + slice(start?: number, end?: number): Blob; +} + +export class Body { + body: NodeJS.ReadableStream; + bodyUsed: boolean; + size: number; + timeout: number; + constructor(body?: any, opts?: { size?: number; timeout?: number }); + arrayBuffer(): Promise; + blob(): Promise; + buffer(): Promise; + json(): Promise; + text(): Promise; +} + +export class FetchError extends Error { + name: 'FetchError'; + [Symbol.toStringTag]: 'FetchError'; + type: string; + code?: string; + errno?: string; + constructor(message: string, type: string, systemError?: object); +} + +export class AbortError extends Error { + type: string; + message: string; + name: 'AbortError'; + [Symbol.toStringTag]: 'AbortError'; + constructor(message: string); +} + +export class Response extends Body { + headers: Headers; + ok: boolean; + redirected: boolean; + status: number; + statusText: string; + type: ResponseType; + url: string; + size: number; + timeout: number; + constructor(body?: BodyInit, init?: ResponseInit); + static error(): Response; + static redirect(url: string, status: number): Response; + clone(): Response; +} + +export type ResponseType = + 'basic' + | 'cors' + | 'default' + | 'error' + | 'opaque' + | 'opaqueredirect'; + +export interface ResponseInit { + headers?: HeadersInit; + size?: number; + status?: number; + statusText?: string; + timeout?: number; + url?: string; +} + +export type HeadersInit = Headers | string[][] | { [key: string]: string }; +// HeaderInit is exported to support backwards compatibility. See PR #34382 +export type HeaderInit = HeadersInit; +export type BodyInit = + ArrayBuffer + | ArrayBufferView + | NodeJS.ReadableStream + | string + | URLSearchParams; +export type RequestInfo = string | Request; + +declare function fetch( + url: RequestInfo, + init?: RequestInit +): Promise; + +declare namespace fetch { + function isRedirect(code: number): boolean; +} + +export default fetch; diff --git a/package.json b/package.json index 8e5c883..f66547e 100644 --- a/package.json +++ b/package.json @@ -1,66 +1,152 @@ { - "name": "node-fetch", - "version": "2.6.0", - "description": "A light-weight module that brings window.fetch to node.js", - "main": "lib/index", - "browser": "./browser.js", - "module": "lib/index.mjs", - "files": [ - "lib/index.js", - "lib/index.mjs", - "lib/index.es.js", - "browser.js" - ], - "engines": { - "node": "4.x || >=6.0.0" - }, - "scripts": { - "build": "cross-env BABEL_ENV=rollup rollup -c", - "prepare": "npm run build", - "test": "cross-env BABEL_ENV=test mocha --require babel-register --throw-deprecation test/test.js", - "report": "cross-env BABEL_ENV=coverage nyc --reporter lcov --reporter text mocha -R spec test/test.js", - "coverage": "cross-env BABEL_ENV=coverage nyc --reporter json --reporter text mocha -R spec test/test.js && codecov -f coverage/coverage-final.json" - }, - "repository": { - "type": "git", - "url": "https://github.com/bitinn/node-fetch.git" - }, - "keywords": [ - "fetch", - "http", - "promise" - ], - "author": "David Frank", - "license": "MIT", - "bugs": { - "url": "https://github.com/bitinn/node-fetch/issues" - }, - "homepage": "https://github.com/bitinn/node-fetch", - "devDependencies": { - "@ungap/url-search-params": "^0.1.2", - "abort-controller": "^1.1.0", - "abortcontroller-polyfill": "^1.3.0", - "babel-core": "^6.26.3", - "babel-plugin-istanbul": "^4.1.6", - "babel-preset-env": "^1.6.1", - "babel-register": "^6.16.3", - "chai": "^3.5.0", - "chai-as-promised": "^7.1.1", - "chai-iterator": "^1.1.1", - "chai-string": "~1.3.0", - "codecov": "^3.3.0", - "cross-env": "^5.2.0", - "form-data": "^2.3.3", - "is-builtin-module": "^1.0.0", - "mocha": "^5.0.0", - "nyc": "11.9.0", - "parted": "^0.1.1", - "promise": "^8.0.3", - "resumer": "0.0.0", - "rollup": "^0.63.4", - "rollup-plugin-babel": "^3.0.7", - "string-to-arraybuffer": "^1.0.2", - "whatwg-url": "^5.0.0" - }, - "dependencies": {} + "name": "node-fetch", + "version": "3.0.0-beta.1", + "description": "A light-weight module that brings window.fetch to node.js", + "main": "dist/index.js", + "module": "dist/index.mjs", + "types": "types/index.d.ts", + "files": [ + "src/**/*", + "dist/**/*", + "types/**/*.d.ts" + ], + "engines": { + "node": ">=10.0.0" + }, + "scripts": { + "build": "pika-pack --out dist/", + "prepare": "npm run build", + "prepublishOnly": "npm run build", + "test": "cross-env BABEL_ENV=test mocha --require @babel/register --throw-deprecation test/*.js", + "report": "cross-env BABEL_ENV=coverage nyc --reporter lcov --reporter text mocha -R spec test/*.js", + "coverage": "cross-env BABEL_ENV=coverage nyc --reporter json --reporter text mocha -R spec test/*.js && codecov -f coverage/coverage-final.json", + "lint": "xo" + }, + "repository": { + "type": "git", + "url": "https://github.com/node-fetch/node-fetch.git" + }, + "keywords": [ + "fetch", + "http", + "promise" + ], + "author": "David Frank", + "license": "MIT", + "bugs": { + "url": "https://github.com/node-fetch/node-fetch/issues" + }, + "homepage": "https://github.com/node-fetch/node-fetch", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/node-fetch" + }, + "devDependencies": { + "@babel/core": "^7.8.7", + "@babel/preset-env": "^7.8.7", + "@babel/register": "^7.8.6", + "@pika/pack": "^0.5.0", + "@pika/plugin-build-node": "^0.9.2", + "@pika/plugin-build-types": "^0.9.2", + "@pika/plugin-copy-assets": "^0.9.2", + "@pika/plugin-standard-pkg": "^0.9.2", + "abort-controller": "^3.0.0", + "abortcontroller-polyfill": "^1.4.0", + "chai": "^4.2.0", + "chai-as-promised": "^7.1.1", + "chai-iterator": "^3.0.2", + "chai-string": "^1.5.0", + "codecov": "^3.6.5", + "cross-env": "^7.0.2", + "form-data": "^3.0.0", + "mocha": "^7.1.0", + "nyc": "^15.0.0", + "parted": "^0.1.1", + "promise": "^8.1.0", + "resumer": "0.0.0", + "string-to-arraybuffer": "^1.0.2", + "xo": "^0.28.0" + }, + "dependencies": { + "data-uri-to-buffer": "^3.0.0", + "fetch-blob": "^1.0.5" + }, + "@pika/pack": { + "pipeline": [ + [ + "@pika/plugin-standard-pkg" + ], + [ + "@pika/plugin-build-node" + ], + [ + "@pika/plugin-build-types" + ], + [ + "@pika/plugin-copy-assets", + { + "files": [ + "externals.d.ts" + ] + } + ] + ] + }, + "xo": { + "envs": [ + "node", + "browser" + ], + "rules": { + "complexity": 0, + "promise/prefer-await-to-then": 0, + "no-mixed-operators": 0, + "no-negated-condition": 0, + "unicorn/prevent-abbreviations": 0 + }, + "ignores": [ + "dist" + ], + "overrides": [ + { + "files": "test/**/*.js", + "envs": [ + "node", + "mocha" + ], + "rules": { + "max-nested-callbacks": 0, + "no-unused-expressions": 0, + "new-cap": 0, + "guard-for-in": 0 + } + }, + { + "files": "example.js", + "rules": { + "import/no-extraneous-dependencies": 0 + } + } + ] + }, + "babel": { + "presets": [ + [ + "@babel/preset-env", + { + "targets": { + "node": true + } + } + ] + ] + }, + "nyc": { + "require": [ + "@babel/register" + ], + "sourceMap": false, + "instrument": false + }, + "runkitExampleFilename": "example.js" } diff --git a/rollup.config.js b/rollup.config.js deleted file mode 100644 index a201ee4..0000000 --- a/rollup.config.js +++ /dev/null @@ -1,27 +0,0 @@ -import isBuiltin from 'is-builtin-module'; -import babel from 'rollup-plugin-babel'; -import tweakDefault from './build/rollup-plugin'; - -process.env.BABEL_ENV = 'rollup'; - -export default { - input: 'src/index.js', - output: [ - { file: 'lib/index.js', format: 'cjs', exports: 'named' }, - { file: 'lib/index.es.js', format: 'es', exports: 'named', intro: 'process.emitWarning("The .es.js file is deprecated. Use .mjs instead.");' }, - { file: 'lib/index.mjs', format: 'es', exports: 'named' }, - ], - plugins: [ - babel({ - runtimeHelpers: true - }), - tweakDefault() - ], - external: function (id) { - if (isBuiltin(id)) { - return true; - } - id = id.split('/').slice(0, id[0] === '@' ? 2 : 1).join('/'); - return !!require('./package.json').dependencies[id]; - } -}; diff --git a/src/abort-error.js b/src/abort-error.js deleted file mode 100644 index cbb13ca..0000000 --- a/src/abort-error.js +++ /dev/null @@ -1,25 +0,0 @@ -/** - * abort-error.js - * - * AbortError interface for cancelled requests - */ - -/** - * Create AbortError instance - * - * @param String message Error message for human - * @return AbortError - */ -export default function AbortError(message) { - Error.call(this, message); - - this.type = 'aborted'; - this.message = message; - - // hide custom error implementation details from end-users - Error.captureStackTrace(this, this.constructor); -} - -AbortError.prototype = Object.create(Error.prototype); -AbortError.prototype.constructor = AbortError; -AbortError.prototype.name = 'AbortError'; diff --git a/src/blob.js b/src/blob.js deleted file mode 100644 index e1151a9..0000000 --- a/src/blob.js +++ /dev/null @@ -1,119 +0,0 @@ -// Based on https://github.com/tmpvar/jsdom/blob/aa85b2abf07766ff7bf5c1f6daafb3726f2f2db5/lib/jsdom/living/blob.js -// (MIT licensed) - -import Stream from 'stream'; - -// fix for "Readable" isn't a named export issue -const Readable = Stream.Readable; - -export const BUFFER = Symbol('buffer'); -const TYPE = Symbol('type'); - -export default class Blob { - constructor() { - this[TYPE] = ''; - - const blobParts = arguments[0]; - const options = arguments[1]; - - const buffers = []; - let size = 0; - - if (blobParts) { - const a = blobParts; - const length = Number(a.length); - for (let i = 0; i < length; i++) { - const element = a[i]; - let buffer; - if (element instanceof Buffer) { - buffer = element; - } else if (ArrayBuffer.isView(element)) { - buffer = Buffer.from(element.buffer, element.byteOffset, element.byteLength); - } else if (element instanceof ArrayBuffer) { - buffer = Buffer.from(element); - } else if (element instanceof Blob) { - buffer = element[BUFFER]; - } else { - buffer = Buffer.from(typeof element === 'string' ? element : String(element)); - } - size += buffer.length; - buffers.push(buffer); - } - } - - this[BUFFER] = Buffer.concat(buffers); - - let type = options && options.type !== undefined && String(options.type).toLowerCase(); - if (type && !/[^\u0020-\u007E]/.test(type)) { - this[TYPE] = type; - } - } - get size() { - return this[BUFFER].length; - } - get type() { - return this[TYPE]; - } - text() { - return Promise.resolve(this[BUFFER].toString()) - } - arrayBuffer() { - const buf = this[BUFFER]; - const ab = buf.buffer.slice(buf.byteOffset, buf.byteOffset + buf.byteLength); - return Promise.resolve(ab); - } - stream() { - const readable = new Readable(); - readable._read = () => {}; - readable.push(this[BUFFER]); - readable.push(null); - return readable; - } - toString() { - return '[object Blob]' - } - slice() { - const size = this.size; - - const start = arguments[0]; - const end = arguments[1]; - let relativeStart, relativeEnd; - if (start === undefined) { - relativeStart = 0; - } else if (start < 0) { - relativeStart = Math.max(size + start, 0); - } else { - relativeStart = Math.min(start, size); - } - if (end === undefined) { - relativeEnd = size; - } else if (end < 0) { - relativeEnd = Math.max(size + end, 0); - } else { - relativeEnd = Math.min(end, size); - } - const span = Math.max(relativeEnd - relativeStart, 0); - - const buffer = this[BUFFER]; - const slicedBuffer = buffer.slice( - relativeStart, - relativeStart + span - ); - const blob = new Blob([], { type: arguments[2] }); - blob[BUFFER] = slicedBuffer; - return blob; - } -} - -Object.defineProperties(Blob.prototype, { - size: { enumerable: true }, - type: { enumerable: true }, - slice: { enumerable: true } -}); - -Object.defineProperty(Blob.prototype, Symbol.toStringTag, { - value: 'Blob', - writable: false, - enumerable: false, - configurable: true -}); diff --git a/src/body.js b/src/body.js index a9d2e79..9d19c89 100644 --- a/src/body.js +++ b/src/body.js @@ -1,23 +1,18 @@ /** - * body.js + * Body.js * * Body interface provides common methods for Request and Response */ -import Stream from 'stream'; +import Stream, {PassThrough} from 'stream'; -import Blob, { BUFFER } from './blob.js'; -import FetchError from './fetch-error.js'; - -let convert; -try { convert = require('encoding').convert; } catch(e) {} +import Blob from 'fetch-blob'; +import FetchError from './errors/fetch-error'; +import {isBlob, isURLSearchParams, isArrayBuffer, isAbortError} from './utils/is'; const INTERNALS = Symbol('Body internals'); -// fix an issue where "PassThrough" isn't a named export for node <10 -const PassThrough = Stream.PassThrough; - /** * Body mixin * @@ -31,29 +26,30 @@ export default function Body(body, { size = 0, timeout = 0 } = {}) { - if (body == null) { - // body is undefined or null + if (body === null) { + // Body is undefined or null body = null; } else if (isURLSearchParams(body)) { - // body is a URLSearchParams + // Body is a URLSearchParams body = Buffer.from(body.toString()); } else if (isBlob(body)) { - // body is blob + // Body is blob } else if (Buffer.isBuffer(body)) { - // body is Buffer - } else if (Object.prototype.toString.call(body) === '[object ArrayBuffer]') { - // body is ArrayBuffer + // Body is Buffer + } else if (isArrayBuffer(body)) { + // Body is ArrayBuffer body = Buffer.from(body); } else if (ArrayBuffer.isView(body)) { - // body is ArrayBufferView + // Body is ArrayBufferView body = Buffer.from(body.buffer, body.byteOffset, body.byteLength); } else if (body instanceof Stream) { - // body is stream + // Body is stream } else { - // none of the above + // None of the above // coerce to string then buffer body = Buffer.from(String(body)); } + this[INTERNALS] = { body, disturbed: false, @@ -64,9 +60,9 @@ export default function Body(body, { if (body instanceof Stream) { body.on('error', err => { - const error = err.name === 'AbortError' - ? err - : new FetchError(`Invalid response body while trying to fetch ${this.url}: ${err.message}`, 'system', err); + const error = isAbortError(err) ? + err : + new FetchError(`Invalid response body while trying to fetch ${this.url}: ${err.message}`, 'system', err); this[INTERNALS].error = error; }); } @@ -87,7 +83,7 @@ Body.prototype = { * @return Promise */ arrayBuffer() { - return consumeBody.call(this).then(buf => buf.buffer.slice(buf.byteOffset, buf.byteOffset + buf.byteLength)); + return consumeBody.call(this).then(({buffer, byteOffset, byteLength}) => buffer.slice(byteOffset, byteOffset + byteLength)); }, /** @@ -96,16 +92,11 @@ Body.prototype = { * @return Promise */ blob() { - let ct = this.headers && this.headers.get('content-type') || ''; - return consumeBody.call(this).then(buf => Object.assign( - // Prevent copying - new Blob([], { - type: ct.toLowerCase() - }), - { - [BUFFER]: buf - } - )); + const ct = this.headers && this.headers.get('content-type') || this[INTERNALS].body && this[INTERNALS].body.type || ''; + return consumeBody.call(this).then(buf => new Blob([], { + type: ct.toLowerCase(), + buffer: buf + })); }, /** @@ -114,13 +105,7 @@ Body.prototype = { * @return Promise */ json() { - return consumeBody.call(this).then((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')); - } - }) + return consumeBody.call(this).then(buffer => JSON.parse(buffer.toString())); }, /** @@ -139,33 +124,23 @@ Body.prototype = { */ 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() { - return consumeBody.call(this).then(buffer => convertBody(buffer, this.headers)); } }; // In browsers, all properties are enumerable. Object.defineProperties(Body.prototype, { - body: { enumerable: true }, - bodyUsed: { enumerable: true }, - arrayBuffer: { enumerable: true }, - blob: { enumerable: true }, - json: { enumerable: true }, - text: { enumerable: true } + body: {enumerable: true}, + bodyUsed: {enumerable: true}, + arrayBuffer: {enumerable: true}, + blob: {enumerable: true}, + json: {enumerable: true}, + text: {enumerable: true} }); -Body.mixIn = function (proto) { +Body.mixIn = proto => { for (const name of Object.getOwnPropertyNames(Body.prototype)) { // istanbul ignore else: future proof - if (!(name in proto)) { + if (!Object.prototype.hasOwnProperty.call(proto, name)) { const desc = Object.getOwnPropertyDescriptor(Body.prototype, name); Object.defineProperty(proto, name, desc); } @@ -190,19 +165,19 @@ function consumeBody() { return Body.Promise.reject(this[INTERNALS].error); } - let body = this.body; + let {body} = this; - // body is null + // Body is null if (body === null) { return Body.Promise.resolve(Buffer.alloc(0)); } - // body is blob + // Body is blob if (isBlob(body)) { body = body.stream(); } - // body is buffer + // Body is buffer if (Buffer.isBuffer(body)) { return Body.Promise.resolve(body); } @@ -212,16 +187,16 @@ function consumeBody() { return Body.Promise.resolve(Buffer.alloc(0)); } - // body is stream + // Body is stream // get ready to actually consume the body - let accum = []; + const accum = []; let accumBytes = 0; let abort = false; return new Body.Promise((resolve, reject) => { let resTimeout; - // allow timeout on slow response body + // Allow timeout on slow response body if (this.timeout) { resTimeout = setTimeout(() => { abort = true; @@ -229,14 +204,14 @@ function consumeBody() { }, this.timeout); } - // handle stream errors + // Handle stream errors body.on('error', err => { - if (err.name === 'AbortError') { - // if the request was aborted, reject with this Error + if (isAbortError(err)) { + // If the request was aborted, reject with this Error abort = true; reject(err); } else { - // other errors, such as incorrect content-encoding + // Other errors, such as incorrect content-encoding reject(new FetchError(`Invalid response body while trying to fetch ${this.url}: ${err.message}`, 'system', err)); } }); @@ -265,148 +240,40 @@ function consumeBody() { try { resolve(Buffer.concat(accum, accumBytes)); - } catch (err) { - // handle streams that have accumulated too much data (issue #414) - reject(new FetchError(`Could not create Buffer from response body for ${this.url}: ${err.message}`, 'system', err)); + } catch (error) { + // Handle streams that have accumulated too much data (issue #414) + reject(new FetchError(`Could not create Buffer from response body for ${this.url}: ${error.message}`, 'system', error)); } }); }); } -/** - * 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 = /> + // 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)); + + pairs.push([...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 + // Record for (const key of Object.keys(init)) { const value = init[key]; this.append(key, value); @@ -117,7 +122,12 @@ export default class Headers { return null; } - return this[MAP][key].join(', '); + let value = this[MAP][key].join(', '); + if (name.toLowerCase() === 'content-encoding') { + value = value.toLowerCase(); + } + + return value; } /** @@ -199,7 +209,7 @@ export default class Headers { if (key !== undefined) { delete this[MAP][key]; } - }; + } /** * Return raw headers (non-spec api) @@ -249,15 +259,15 @@ Object.defineProperty(Headers.prototype, Symbol.toStringTag, { }); Object.defineProperties(Headers.prototype, { - get: { enumerable: true }, - forEach: { enumerable: true }, - set: { enumerable: true }, - append: { enumerable: true }, - has: { enumerable: true }, - delete: { enumerable: true }, - keys: { enumerable: true }, - values: { enumerable: true }, - entries: { enumerable: true } + get: {enumerable: true}, + forEach: {enumerable: true}, + set: {enumerable: true}, + append: {enumerable: true}, + has: {enumerable: true}, + delete: {enumerable: true}, + keys: {enumerable: true}, + values: {enumerable: true}, + entries: {enumerable: true} }); function getHeaders(headers, kind = 'key+value') { @@ -265,9 +275,9 @@ function getHeaders(headers, kind = 'key+value') { return keys.map( kind === 'key' ? k => k.toLowerCase() : - kind === 'value' ? + (kind === 'value' ? k => headers[MAP][k].join(', ') : - k => [k.toLowerCase(), headers[MAP][k].join(', ')] + k => [k.toLowerCase(), headers[MAP][k].join(', ')]) ); } @@ -297,8 +307,8 @@ const HeadersIteratorPrototype = Object.setPrototypeOf({ index } = this[INTERNAL]; const values = getHeaders(target, kind); - const len = values.length; - if (index >= len) { + const length_ = values.length; + if (index >= length_) { return { value: undefined, done: true @@ -330,16 +340,16 @@ Object.defineProperty(HeadersIteratorPrototype, Symbol.toStringTag, { * @return Object */ export function exportNodeCompatibleHeaders(headers) { - const obj = Object.assign({ __proto__: null }, headers[MAP]); + const object = {__proto__: null, ...headers[MAP]}; - // http.request() only supports string as Host header. This hack makes + // Http.request() only supports string as Host header. This hack makes // specifying custom Host header possible. const hostHeaderKey = find(headers[MAP], 'Host'); if (hostHeaderKey !== undefined) { - obj[hostHeaderKey] = obj[hostHeaderKey][0]; + object[hostHeaderKey] = object[hostHeaderKey][0]; } - return obj; + return object; } /** @@ -349,26 +359,29 @@ export function exportNodeCompatibleHeaders(headers) { * @param Object obj Object of headers * @return Headers */ -export function createHeadersLenient(obj) { +export function createHeadersLenient(object) { const headers = new Headers(); - for (const name of Object.keys(obj)) { + for (const name of Object.keys(object)) { if (invalidTokenRegex.test(name)) { continue; } - if (Array.isArray(obj[name])) { - for (const val of obj[name]) { - if (invalidHeaderCharRegex.test(val)) { + + if (Array.isArray(object[name])) { + for (const value of object[name]) { + if (invalidHeaderCharRegex.test(value)) { continue; } + if (headers[MAP][name] === undefined) { - headers[MAP][name] = [val]; + headers[MAP][name] = [value]; } else { - headers[MAP][name].push(val); + headers[MAP][name].push(value); } } - } else if (!invalidHeaderCharRegex.test(obj[name])) { - headers[MAP][name] = [obj[name]]; + } else if (!invalidHeaderCharRegex.test(object[name])) { + headers[MAP][name] = [object[name]]; } } + return headers; } diff --git a/src/index.js b/src/index.js index 8bf9248..6ba12d7 100644 --- a/src/index.js +++ b/src/index.js @@ -1,28 +1,23 @@ - /** - * index.js + * Index.js * * a request API compatible with window.fetch * * All spec algorithm step numbers are based on https://fetch.spec.whatwg.org/commit-snapshots/ae716822cb3a61843226cd090eefc6589446c1d2/. */ -import Url from 'url'; import http from 'http'; import https from 'https'; import zlib from 'zlib'; -import Stream from 'stream'; +import Stream, {PassThrough, pipeline as pump} from 'stream'; +import dataURIToBuffer from 'data-uri-to-buffer'; -import Body, { writeToStream, getTotalBytes } from './body'; +import Body, {writeToStream, getTotalBytes} from './body'; import Response from './response'; -import Headers, { createHeadersLenient } from './headers'; -import Request, { getNodeRequestOptions } from './request'; -import FetchError from './fetch-error'; -import AbortError from './abort-error'; - -// fix an issue where "PassThrough", "resolve" aren't a named export for node <10 -const PassThrough = Stream.PassThrough; -const resolve_url = Url.resolve; +import Headers, {createHeadersLenient} from './headers'; +import Request, {getNodeRequestOptions} from './request'; +import FetchError from './errors/fetch-error'; +import AbortError from './errors/abort-error'; /** * Fetch function @@ -31,45 +26,53 @@ const resolve_url = Url.resolve; * @param Object opts Fetch options * @return Promise */ -export default function fetch(url, opts) { - - // allow custom promise +export default function fetch(url, options_) { + // Allow custom promise if (!fetch.Promise) { throw new Error('native promise missing, set fetch.Promise to your favorite alternative'); } - if (/^data:/.test(url)) { - const request = new Request(url, opts); - try { - const data = Buffer.from(url.split(',')[1], 'base64') - const res = new Response(data.body, { headers: { 'Content-Type': data.mimeType || url.match(/^data:(.+);base64,.*$/)[1] } }); - return fetch.Promise.resolve(res); - } catch (err) { - return fetch.Promise.reject(new FetchError(`[${request.method}] ${request.url} invalid URL, ${err.message}`, 'system', err)); - } + // Regex for data uri + const dataUriRegex = /^\s*data:([a-z]+\/[a-z]+(;[a-z-]+=[a-z-]+)?)?(;base64)?,[\w!$&',()*+;=\-.~:@/?%\s]*\s*$/i; + + // If valid data uri + if (dataUriRegex.test(url)) { + const data = dataURIToBuffer(url); + const res = new Response(data, {headers: {'Content-Type': data.type}}); + return fetch.Promise.resolve(res); + } + + // If invalid data uri + if (url.toString().startsWith('data:')) { + const request = new Request(url, options_); + return fetch.Promise.reject(new FetchError(`[${request.method}] ${request.url} invalid URL`, 'system')); } Body.Promise = fetch.Promise; - // wrap http.request into fetch + // Wrap http.request into fetch return new fetch.Promise((resolve, reject) => { - // build request object - const request = new Request(url, opts); + // Build request object + const request = new Request(url, options_); const options = getNodeRequestOptions(request); const send = (options.protocol === 'https:' ? https : http).request; - const { signal } = request; + const {signal} = request; let response = null; - const abort = () => { - let error = new AbortError('The user aborted a request.'); + const abort = () => { + const error = new AbortError('The operation was aborted.'); reject(error); if (request.body && request.body instanceof Stream.Readable) { request.body.destroy(error); } - if (!response || !response.body) return; + + if (!response || !response.body) { + return; + } + response.body.emit('error', error); - } + }; if (signal && signal.aborted) { abort(); @@ -79,39 +82,35 @@ export default function fetch(url, opts) { const abortAndFinalize = () => { abort(); finalize(); - } + }; - // send request - const req = send(options); - let reqTimeout; + // Send request + const request_ = send(options); if (signal) { signal.addEventListener('abort', abortAndFinalize); } function finalize() { - req.abort(); - if (signal) signal.removeEventListener('abort', abortAndFinalize); - clearTimeout(reqTimeout); + request_.abort(); + if (signal) { + signal.removeEventListener('abort', abortAndFinalize); + } } if (request.timeout) { - req.once('socket', socket => { - reqTimeout = setTimeout(() => { - reject(new FetchError(`network timeout at: ${request.url}`, 'request-timeout')); - finalize(); - }, request.timeout); + request_.setTimeout(request.timeout, () => { + finalize(); + reject(new FetchError(`network timeout at: ${request.url}`, 'request-timeout')); }); } - req.on('error', err => { + request_.on('error', err => { reject(new FetchError(`request to ${request.url} failed, reason: ${err.message}`, 'system', err)); finalize(); }); - req.on('response', res => { - clearTimeout(reqTimeout); - + request_.on('response', res => { const headers = createHeadersLenient(res.headers); // HTTP fetch step 5 @@ -120,7 +119,7 @@ export default function fetch(url, opts) { const location = headers.get('Location'); // HTTP fetch step 5.3 - const locationURL = location === null ? null : resolve_url(request.url, location); + const locationURL = location === null ? null : new URL(location, request.url); // HTTP fetch step 5.5 switch (request.redirect) { @@ -129,18 +128,19 @@ export default function fetch(url, opts) { finalize(); return; case 'manual': - // node-fetch-specific step: make manual redirect a bit easier to use by setting the Location header value to the resolved URL. + // Node-fetch-specific step: make manual redirect a bit easier to use by setting the Location header value to the resolved URL. if (locationURL !== null) { - // handle corrupted header + // Handle corrupted header try { headers.set('Location', locationURL); - } catch (err) { + } catch (error) { // istanbul ignore next: nodejs server prevent invalid response headers, we can't test this through normal request - reject(err); + reject(error); } } + break; - case 'follow': + case 'follow': { // HTTP-redirect fetch step 2 if (locationURL === null) { break; @@ -155,7 +155,7 @@ export default function fetch(url, opts) { // HTTP-redirect fetch step 6 (counter increment) // Create a new Request object. - const requestOpts = { + const requestOptions = { headers: new Headers(request.headers), follow: request.follow, counter: request.counter + 1, @@ -176,32 +176,42 @@ export default function fetch(url, opts) { // HTTP-redirect fetch step 11 if (res.statusCode === 303 || ((res.statusCode === 301 || res.statusCode === 302) && request.method === 'POST')) { - requestOpts.method = 'GET'; - requestOpts.body = undefined; - requestOpts.headers.delete('content-length'); + requestOptions.method = 'GET'; + requestOptions.body = undefined; + requestOptions.headers.delete('content-length'); } // HTTP-redirect fetch step 15 - resolve(fetch(new Request(locationURL, requestOpts))); + resolve(fetch(new Request(locationURL, requestOptions))); finalize(); return; + } + + default: + // Do nothing } } - // prepare response + // Prepare response res.once('end', () => { - if (signal) signal.removeEventListener('abort', abortAndFinalize); + if (signal) { + signal.removeEventListener('abort', abortAndFinalize); + } }); - let body = res.pipe(new PassThrough()); - const response_options = { + let body = pump(res, new PassThrough(), error => { + reject(error); + }); + + const responseOptions = { url: request.url, status: res.statusCode, statusText: res.statusMessage, - headers: headers, + headers, size: request.size, timeout: request.timeout, - counter: request.counter + counter: request.counter, + highWaterMark: request.highWaterMark }; // HTTP-network fetch step 12.1.1.3 @@ -216,7 +226,7 @@ export default function fetch(url, opts) { // 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) { - response = new Response(body, response_options); + response = new Response(body, responseOptions); resolve(response); return; } @@ -231,49 +241,59 @@ export default function fetch(url, opts) { finishFlush: zlib.Z_SYNC_FLUSH }; - // for gzip - if (codings == 'gzip' || codings == 'x-gzip') { - body = body.pipe(zlib.createGunzip(zlibOptions)); - response = new Response(body, response_options); + // For gzip + if (codings === 'gzip' || codings === 'x-gzip') { + body = pump(body, zlib.createGunzip(zlibOptions), error => { + reject(error); + }); + response = new Response(body, responseOptions); resolve(response); return; } - // for deflate - if (codings == 'deflate' || codings == 'x-deflate') { - // handle the infamous raw deflate response from old servers + // 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()); + const raw = pump(res, new PassThrough(), error => { + reject(error); + }); raw.once('data', chunk => { - // see http://stackoverflow.com/questions/37519828 + // See http://stackoverflow.com/questions/37519828 if ((chunk[0] & 0x0F) === 0x08) { - body = body.pipe(zlib.createInflate()); + body = pump(body, zlib.createInflate(), error => { + reject(error); + }); } else { - body = body.pipe(zlib.createInflateRaw()); + body = pump(body, zlib.createInflateRaw(), error => { + reject(error); + }); } - response = new Response(body, response_options); + + response = new Response(body, responseOptions); resolve(response); }); return; } - // for br - if (codings == 'br' && typeof zlib.createBrotliDecompress === 'function') { - body = body.pipe(zlib.createBrotliDecompress()); - response = new Response(body, response_options); + // For br + if (codings === 'br' && typeof zlib.createBrotliDecompress === 'function') { + body = pump(body, zlib.createBrotliDecompress(), error => { + reject(error); + }); + response = new Response(body, responseOptions); resolve(response); return; } - // otherwise, use response as-is - response = new Response(body, response_options); + // Otherwise, use response as-is + response = new Response(body, responseOptions); resolve(response); }); - writeToStream(req, request); + writeToStream(request_, request); }); - -}; +} /** * Redirect code matching @@ -281,9 +301,9 @@ export default function fetch(url, opts) { * @param Number code Status code * @return Boolean */ -fetch.isRedirect = code => code === 301 || code === 302 || code === 303 || code === 307 || code === 308; +fetch.isRedirect = code => [301, 302, 303, 307, 308].includes(code); -// expose Promise +// Expose Promise fetch.Promise = global.Promise; export { Headers, diff --git a/src/request.js b/src/request.js index 45a7eb7..f62190f 100644 --- a/src/request.js +++ b/src/request.js @@ -1,45 +1,53 @@ /** - * request.js + * Request.js * * Request class contains server only options * * All spec algorithm step numbers are based on https://fetch.spec.whatwg.org/commit-snapshots/ae716822cb3a61843226cd090eefc6589446c1d2/. */ -import Url from 'url'; +import {format as formatUrl} from 'url'; import Stream from 'stream'; -import Headers, { exportNodeCompatibleHeaders } from './headers.js'; -import Body, { clone, extractContentType, getTotalBytes } from './body'; +import Headers, {exportNodeCompatibleHeaders} from './headers'; +import Body, {clone, extractContentType, getTotalBytes} from './body'; +import {isAbortSignal} from './utils/is'; const INTERNALS = Symbol('Request internals'); -// fix an issue where "format", "parse" aren't a named export for node <10 -const parse_url = Url.parse; -const format_url = Url.format; - const streamDestructionSupported = 'destroy' in Stream.Readable.prototype; /** - * Check if a value is an instance of Request. + * Check if `obj` is an instance of Request. * - * @param Mixed input - * @return Boolean + * @param {*} obj + * @return {boolean} */ -function isRequest(input) { +function isRequest(object) { return ( - typeof input === 'object' && - typeof input[INTERNALS] === 'object' + typeof object === 'object' && + typeof object[INTERNALS] === 'object' ); } -function isAbortSignal(signal) { - const proto = ( - signal - && typeof signal === 'object' - && Object.getPrototypeOf(signal) - ); - return !!(proto && proto.constructor.name === 'AbortSignal'); +/** + * Wrapper around `new URL` to handle relative URLs (https://github.com/nodejs/node/issues/12682) + * + * @param {string} urlStr + * @return {void} + */ +function parseURL(urlString) { + /* + Check whether the URL is absolute or not + + Scheme: https://tools.ietf.org/html/rfc3986#section-3.1 + Absolute URL: https://tools.ietf.org/html/rfc3986#section-4.3 + */ + if (/^[a-zA-Z][a-zA-Z\d+\-.]*:/.exec(urlString)) { + return new URL(urlString); + } + + throw new TypeError('Only absolute URLs are supported'); } /** @@ -53,35 +61,38 @@ export default class Request { constructor(input, init = {}) { let parsedURL; - // normalize input + // Normalize input and force URL to be encoded as UTF-8 (https://github.com/bitinn/node-fetch/issues/245) if (!isRequest(input)) { if (input && input.href) { - // in order to support Node.js' Url objects; though WHATWG's URL objects + // 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); + parsedURL = parseURL(input.href); } else { - // coerce input to a string before attempting to parse - parsedURL = parse_url(`${input}`); + // Coerce input to a string before attempting to parse + parsedURL = parseURL(`${input}`); } + input = {}; } else { - parsedURL = parse_url(input.url); + parsedURL = parseURL(input.url); } let method = init.method || input.method || 'GET'; method = method.toUpperCase(); + // eslint-disable-next-line no-eq-null, eqeqeq if ((init.body != null || isRequest(input) && input.body !== null) && (method === 'GET' || method === 'HEAD')) { throw new TypeError('Request with GET/HEAD method cannot have body'); } - let inputBody = init.body != null ? + // eslint-disable-next-line no-eq-null, eqeqeq + const inputBody = init.body != null ? init.body : - isRequest(input) && input.body !== null ? + (isRequest(input) && input.body !== null ? clone(input) : - null; + null); Body.call(this, inputBody, { timeout: init.timeout || input.timeout || 0, @@ -90,19 +101,21 @@ export default class Request { const headers = new Headers(init.headers || input.headers || {}); - if (inputBody != null && !headers.has('Content-Type')) { + if (inputBody !== null && !headers.has('Content-Type')) { const contentType = extractContentType(inputBody); if (contentType) { headers.append('Content-Type', contentType); } } - let signal = isRequest(input) - ? input.signal - : null; - if ('signal' in init) signal = init.signal + let signal = isRequest(input) ? + input.signal : + null; + if ('signal' in init) { + signal = init.signal; + } - if (signal != null && !isAbortSignal(signal)) { + if (signal !== null && !isAbortSignal(signal)) { throw new TypeError('Expected signal to be an instanceof AbortSignal'); } @@ -111,18 +124,19 @@ export default class Request { redirect: init.redirect || input.redirect || 'follow', headers, parsedURL, - signal, + signal }; - // node-fetch-only options + // Node-fetch-only options this.follow = init.follow !== undefined ? - init.follow : input.follow !== undefined ? - input.follow : 20; + init.follow : (input.follow !== undefined ? + input.follow : 20); this.compress = init.compress !== undefined ? - init.compress : input.compress !== undefined ? - input.compress : true; + init.compress : (input.compress !== undefined ? + input.compress : true); this.counter = init.counter || input.counter || 0; this.agent = init.agent || input.agent; + this.highWaterMark = init.highWaterMark || input.highWaterMark; } get method() { @@ -130,7 +144,7 @@ export default class Request { } get url() { - return format_url(this[INTERNALS].parsedURL); + return formatUrl(this[INTERNALS].parsedURL); } get headers() { @@ -165,12 +179,12 @@ Object.defineProperty(Request.prototype, Symbol.toStringTag, { }); Object.defineProperties(Request.prototype, { - method: { enumerable: true }, - url: { enumerable: true }, - headers: { enumerable: true }, - redirect: { enumerable: true }, - clone: { enumerable: true }, - signal: { enumerable: true }, + method: {enumerable: true}, + url: {enumerable: true}, + headers: {enumerable: true}, + redirect: {enumerable: true}, + clone: {enumerable: true}, + signal: {enumerable: true} }); /** @@ -180,10 +194,10 @@ Object.defineProperties(Request.prototype, { * @return Object The options object to be passed to http.request */ export function getNodeRequestOptions(request) { - const parsedURL = request[INTERNALS].parsedURL; + const {parsedURL} = request[INTERNALS]; const headers = new Headers(request[INTERNALS].headers); - // fetch step 1.3 + // Fetch step 1.3 if (!headers.has('Accept')) { headers.set('Accept', '*/*'); } @@ -198,24 +212,26 @@ export function getNodeRequestOptions(request) { } if ( - request.signal - && request.body instanceof Stream.Readable - && !streamDestructionSupported + request.signal && + request.body instanceof Stream.Readable && + !streamDestructionSupported ) { - throw new Error('Cancellation of streamed requests with AbortSignal is not supported in node < 8'); + throw new Error('Cancellation of streamed requests with AbortSignal is not supported'); } // HTTP-network-or-cache fetch steps 2.4-2.7 let contentLengthValue = null; - if (request.body == null && /^(POST|PUT)$/i.test(request.method)) { + if (request.body === null && /^(post|put)$/i.test(request.method)) { contentLengthValue = '0'; } - if (request.body != null) { + + if (request.body !== null) { const totalBytes = getTotalBytes(request); if (typeof totalBytes === 'number') { contentLengthValue = String(totalBytes); } } + if (contentLengthValue) { headers.set('Content-Length', contentLengthValue); } @@ -230,7 +246,7 @@ export function getNodeRequestOptions(request) { headers.set('Accept-Encoding', 'gzip,deflate'); } - let agent = request.agent; + let {agent} = request; if (typeof agent === 'function') { agent = agent(parsedURL); } @@ -242,9 +258,21 @@ export function getNodeRequestOptions(request) { // HTTP-network fetch step 4.2 // chunked encoding is handled by Node.js - return Object.assign({}, parsedURL, { + // manually spread the URL object instead of spread syntax + const requestOptions = { + path: parsedURL.pathname, + pathname: parsedURL.pathname, + hostname: parsedURL.hostname, + protocol: parsedURL.protocol, + port: parsedURL.port, + hash: parsedURL.hash, + search: parsedURL.search, + query: parsedURL.query, + href: parsedURL.href, method: request.method, headers: exportNodeCompatibleHeaders(headers), agent - }); + }; + + return requestOptions; } diff --git a/src/response.js b/src/response.js index e4801bb..a7ec567 100644 --- a/src/response.js +++ b/src/response.js @@ -1,20 +1,14 @@ - /** - * response.js + * Response.js * * Response class provides content decoding */ -import http from 'http'; - -import Headers from './headers.js'; -import Body, { clone, extractContentType } from './body'; +import Headers from './headers'; +import Body, {clone, extractContentType} from './body'; const INTERNALS = Symbol('Response internals'); -// fix an issue where "STATUS_CODES" aren't a named export for node <10 -const STATUS_CODES = http.STATUS_CODES; - /** * Response class * @@ -23,13 +17,13 @@ const STATUS_CODES = http.STATUS_CODES; * @return Void */ export default class Response { - constructor(body = null, opts = {}) { - Body.call(this, body, opts); + constructor(body = null, options = {}) { + Body.call(this, body, options); - const status = opts.status || 200; - const headers = new Headers(opts.headers) + const status = options.status || 200; + const headers = new Headers(options.headers); - if (body != null && !headers.has('Content-Type')) { + if (body !== null && !headers.has('Content-Type')) { const contentType = extractContentType(body); if (contentType) { headers.append('Content-Type', contentType); @@ -37,11 +31,12 @@ export default class Response { } this[INTERNALS] = { - url: opts.url, + url: options.url, status, - statusText: opts.statusText || STATUS_CODES[status], + statusText: options.statusText || '', headers, - counter: opts.counter + counter: options.counter, + highWaterMark: options.highWaterMark }; } @@ -72,19 +67,43 @@ export default class Response { return this[INTERNALS].headers; } + get highWaterMark() { + return this[INTERNALS].highWaterMark; + } + /** * Clone this response * * @return Response */ clone() { - return new Response(clone(this), { + return new Response(clone(this, this.highWaterMark), { url: this.url, status: this.status, statusText: this.statusText, headers: this.headers, ok: this.ok, - redirected: this.redirected + redirected: this.redirected, + size: this.size, + timeout: this.timeout + }); + } + + /** + * @param {string} url The URL that the new response is to originate from. + * @param {number} status An optional status code for the response (e.g., 302.) + * @returns {Response} A Response object. + */ + static redirect(url, status = 302) { + if (![301, 302, 303, 307, 308].includes(status)) { + throw new RangeError('Failed to execute "redirect" on "response": Invalid status code'); + } + + return new Response(null, { + headers: { + location: new URL(url).toString() + }, + status }); } } @@ -92,13 +111,13 @@ export default class Response { Body.mixIn(Response.prototype); Object.defineProperties(Response.prototype, { - url: { enumerable: true }, - status: { enumerable: true }, - ok: { enumerable: true }, - redirected: { enumerable: true }, - statusText: { enumerable: true }, - headers: { enumerable: true }, - clone: { enumerable: true } + url: {enumerable: true}, + status: {enumerable: true}, + ok: {enumerable: true}, + redirected: {enumerable: true}, + statusText: {enumerable: true}, + headers: {enumerable: true}, + clone: {enumerable: true} }); Object.defineProperty(Response.prototype, Symbol.toStringTag, { diff --git a/src/utils/is.js b/src/utils/is.js new file mode 100644 index 0000000..6059167 --- /dev/null +++ b/src/utils/is.js @@ -0,0 +1,78 @@ +/** + * Is.js + * + * Object type checks. + */ + +const NAME = Symbol.toStringTag; + +/** + * Check if `obj` is a URLSearchParams object + * ref: https://github.com/node-fetch/node-fetch/issues/296#issuecomment-307598143 + * + * @param {*} obj + * @return {boolean} + */ +export function isURLSearchParams(object) { + return ( + typeof object === 'object' && + typeof object.append === 'function' && + typeof object.delete === 'function' && + typeof object.get === 'function' && + typeof object.getAll === 'function' && + typeof object.has === 'function' && + typeof object.set === 'function' && + typeof object.sort === 'function' && + object[NAME] === 'URLSearchParams' + ); +} + +/** + * Check if `obj` is a W3C `Blob` object (which `File` inherits from) + * + * @param {*} obj + * @return {boolean} + */ +export function isBlob(object) { + return ( + typeof object === 'object' && + typeof object.arrayBuffer === 'function' && + typeof object.type === 'string' && + typeof object.stream === 'function' && + typeof object.constructor === 'function' && + /^(Blob|File)$/.test(object[NAME]) + ); +} + +/** + * Check if `obj` is an instance of AbortSignal. + * + * @param {*} obj + * @return {boolean} + */ +export function isAbortSignal(object) { + return ( + typeof object === 'object' && + object[NAME] === 'AbortSignal' + ); +} + +/** + * Check if `obj` is an instance of ArrayBuffer. + * + * @param {*} obj + * @return {boolean} + */ +export function isArrayBuffer(object) { + return object[NAME] === 'ArrayBuffer'; +} + +/** + * Check if `obj` is an instance of AbortError. + * + * @param {*} obj + * @return {boolean} + */ +export function isAbortError(object) { + return object[NAME] === 'AbortError'; +} diff --git a/test/external-encoding.js b/test/external-encoding.js new file mode 100644 index 0000000..b7a3137 --- /dev/null +++ b/test/external-encoding.js @@ -0,0 +1,34 @@ +import fetch from '../src'; +import chai from 'chai'; + +const {expect} = chai; + +describe('external encoding', () => { + describe('data uri', () => { + it('should accept data uri', () => { + return fetch('data:image/gif;base64,R0lGODlhAQABAIAAAAUEBAAAACwAAAAAAQABAAACAkQBADs=').then(r => { + expect(r.status).to.equal(200); + expect(r.headers.get('Content-Type')).to.equal('image/gif'); + + return r.buffer().then(b => { + expect(b).to.be.an.instanceOf(Buffer); + }); + }); + }); + + it('should accept data uri of plain text', () => { + return fetch('data:,Hello%20World!').then(r => { + expect(r.status).to.equal(200); + expect(r.headers.get('Content-Type')).to.equal('text/plain'); + return r.text().then(t => expect(t).to.equal('Hello World!')); + }); + }); + + it('should reject invalid data uri', () => { + return fetch('data:@@@@').catch(error => { + expect(error).to.exist; + expect(error.message).to.include('invalid URL'); + }); + }); + }); +}); diff --git a/test/headers.js b/test/headers.js new file mode 100644 index 0000000..90c40ef --- /dev/null +++ b/test/headers.js @@ -0,0 +1,232 @@ +import {Headers} from '../src'; +import chai from 'chai'; + +const {expect} = chai; + +describe('Headers', () => { + it('should have attributes conforming to Web IDL', () => { + const headers = new Headers(); + expect(Object.getOwnPropertyNames(headers)).to.be.empty; + const enumerableProperties = []; + + for (const property in headers) { + enumerableProperties.push(property); + } + + for (const toCheck of [ + 'append', + 'delete', + 'entries', + 'forEach', + 'get', + 'has', + 'keys', + 'set', + 'values' + ]) { + expect(enumerableProperties).to.contain(toCheck); + } + }); + + it('should allow iterating through all headers with forEach', () => { + const headers = new Headers([ + ['b', '2'], + ['c', '4'], + ['b', '3'], + ['a', '1'] + ]); + expect(headers).to.have.property('forEach'); + + const result = []; + headers.forEach((value, key) => { + result.push([key, value]); + }); + + expect(result).to.deep.equal([ + ['a', '1'], + ['b', '2, 3'], + ['c', '4'] + ]); + }); + + it('should allow iterating through all headers with for-of loop', () => { + const headers = new Headers([ + ['b', '2'], + ['c', '4'], + ['a', '1'] + ]); + headers.append('b', '3'); + expect(headers).to.be.iterable; + + const result = []; + for (const pair of headers) { + result.push(pair); + } + + expect(result).to.deep.equal([ + ['a', '1'], + ['b', '2, 3'], + ['c', '4'] + ]); + }); + + it('should allow iterating through all headers with entries()', () => { + const headers = new Headers([ + ['b', '2'], + ['c', '4'], + ['a', '1'] + ]); + headers.append('b', '3'); + + expect(headers.entries()).to.be.iterable + .and.to.deep.iterate.over([ + ['a', '1'], + ['b', '2, 3'], + ['c', '4'] + ]); + }); + + it('should allow iterating through all headers with keys()', () => { + const headers = new Headers([ + ['b', '2'], + ['c', '4'], + ['a', '1'] + ]); + headers.append('b', '3'); + + expect(headers.keys()).to.be.iterable + .and.to.iterate.over(['a', 'b', 'c']); + }); + + it('should allow iterating through all headers with values()', () => { + const headers = new Headers([ + ['b', '2'], + ['c', '4'], + ['a', '1'] + ]); + headers.append('b', '3'); + + expect(headers.values()).to.be.iterable + .and.to.iterate.over(['1', '2, 3', '4']); + }); + + it('should reject illegal header', () => { + const headers = new Headers(); + expect(() => new Headers({'He y': 'ok'})).to.throw(TypeError); + expect(() => new Headers({'Hé-y': 'ok'})).to.throw(TypeError); + expect(() => new Headers({'He-y': 'ăk'})).to.throw(TypeError); + expect(() => headers.append('Hé-y', 'ok')).to.throw(TypeError); + expect(() => headers.delete('Hé-y')).to.throw(TypeError); + expect(() => headers.get('Hé-y')).to.throw(TypeError); + expect(() => headers.has('Hé-y')).to.throw(TypeError); + expect(() => headers.set('Hé-y', 'ok')).to.throw(TypeError); + // Should reject empty header + expect(() => headers.append('', 'ok')).to.throw(TypeError); + }); + + it('should ignore unsupported attributes while reading headers', () => { + const FakeHeader = function () { }; + // Prototypes are currently ignored + // This might change in the future: #181 + FakeHeader.prototype.z = 'fake'; + + const res = new FakeHeader(); + res.a = 'string'; + res.b = ['1', '2']; + res.c = ''; + res.d = []; + res.e = 1; + res.f = [1, 2]; + res.g = {a: 1}; + res.h = undefined; + res.i = null; + res.j = NaN; + res.k = true; + res.l = false; + res.m = Buffer.from('test'); + + const h1 = new Headers(res); + h1.set('n', [1, 2]); + h1.append('n', ['3', 4]); + + const h1Raw = h1.raw(); + + expect(h1Raw.a).to.include('string'); + expect(h1Raw.b).to.include('1,2'); + expect(h1Raw.c).to.include(''); + expect(h1Raw.d).to.include(''); + expect(h1Raw.e).to.include('1'); + expect(h1Raw.f).to.include('1,2'); + expect(h1Raw.g).to.include('[object Object]'); + expect(h1Raw.h).to.include('undefined'); + expect(h1Raw.i).to.include('null'); + expect(h1Raw.j).to.include('NaN'); + expect(h1Raw.k).to.include('true'); + expect(h1Raw.l).to.include('false'); + expect(h1Raw.m).to.include('test'); + expect(h1Raw.n).to.include('1,2'); + expect(h1Raw.n).to.include('3,4'); + + expect(h1Raw.z).to.be.undefined; + }); + + it('should wrap headers', () => { + const h1 = new Headers({ + a: '1' + }); + const h1Raw = h1.raw(); + + const h2 = new Headers(h1); + h2.set('b', '1'); + const h2Raw = h2.raw(); + + const h3 = new Headers(h2); + h3.append('a', '2'); + const h3Raw = h3.raw(); + + expect(h1Raw.a).to.include('1'); + expect(h1Raw.a).to.not.include('2'); + + expect(h2Raw.a).to.include('1'); + expect(h2Raw.a).to.not.include('2'); + expect(h2Raw.b).to.include('1'); + + expect(h3Raw.a).to.include('1'); + expect(h3Raw.a).to.include('2'); + expect(h3Raw.b).to.include('1'); + }); + + it('should accept headers as an iterable of tuples', () => { + let headers; + + headers = new Headers([ + ['a', '1'], + ['b', '2'], + ['a', '3'] + ]); + expect(headers.get('a')).to.equal('1, 3'); + expect(headers.get('b')).to.equal('2'); + + headers = new Headers([ + new Set(['a', '1']), + ['b', '2'], + new Map([['a', null], ['3', null]]).keys() + ]); + expect(headers.get('a')).to.equal('1, 3'); + expect(headers.get('b')).to.equal('2'); + + headers = new Headers(new Map([ + ['a', '1'], + ['b', '2'] + ])); + expect(headers.get('a')).to.equal('1'); + expect(headers.get('b')).to.equal('2'); + }); + + it('should throw a TypeError if non-tuple exists in a headers initializer', () => { + expect(() => new Headers([['b', '2', 'huh?']])).to.throw(TypeError); + expect(() => new Headers(['b2'])).to.throw(TypeError); + expect(() => new Headers('b2')).to.throw(TypeError); + expect(() => new Headers({[Symbol.iterator]: 42})).to.throw(TypeError); + }); +}); diff --git a/test/test.js b/test/main.js similarity index 51% rename from test/test.js rename to test/main.js index c5d61c7..4f6134b 100644 --- a/test/test.js +++ b/test/main.js @@ -1,5 +1,13 @@ - -// test tools +// Test tools +import zlib from 'zlib'; +import crypto from 'crypto'; +import {spawn} from 'child_process'; +import * as http from 'http'; +import * as fs from 'fs'; +import * as path from 'path'; +import * as stream from 'stream'; +import {lookup} from 'dns'; +import vm from 'vm'; import chai from 'chai'; import chaiPromised from 'chai-as-promised'; import chaiIterator from 'chai-iterator'; @@ -8,55 +16,36 @@ import then from 'promise'; import resumer from 'resumer'; import FormData from 'form-data'; import stringToArrayBuffer from 'string-to-arraybuffer'; -import URLSearchParams_Polyfill from '@ungap/url-search-params'; -import { URL } from 'whatwg-url'; -import { AbortController } from 'abortcontroller-polyfill/dist/abortcontroller'; + +import {AbortController} from 'abortcontroller-polyfill/dist/abortcontroller'; import AbortController2 from 'abort-controller'; -const { spawn } = require('child_process'); -const http = require('http'); -const fs = require('fs'); -const path = require('path'); -const stream = require('stream'); -const { parse: parseURL, URLSearchParams } = require('url'); -const { lookup } = require('dns'); -const vm = require('vm'); - -const { - ArrayBuffer: VMArrayBuffer, - Uint8Array: VMUint8Array -} = vm.runInNewContext('this'); - -let convert; -try { convert = require('encoding').convert; } catch(e) { } - -chai.use(chaiPromised); -chai.use(chaiIterator); -chai.use(chaiString); -const expect = chai.expect; - -import TestServer from './server'; - -// test subjects +// Test subjects +import Blob from 'fetch-blob'; import fetch, { FetchError, Headers, Request, Response -} from '../src/'; -import FetchErrorOrig from '../src/fetch-error.js'; -import HeadersOrig, { createHeadersLenient } from '../src/headers.js'; -import RequestOrig from '../src/request.js'; -import ResponseOrig from '../src/response.js'; -import Body, { getTotalBytes, extractContentType } from '../src/body.js'; -import Blob from '../src/blob.js'; -import zlib from "zlib"; +} from '../src'; +import FetchErrorOrig from '../src/errors/fetch-error'; +import HeadersOrig, {createHeadersLenient} from '../src/headers'; +import RequestOrig from '../src/request'; +import ResponseOrig from '../src/response'; +import Body, {getTotalBytes, extractContentType} from '../src/body'; +import TestServer from './utils/server'; -const supportToString = ({ - [Symbol.toStringTag]: 'z' -}).toString() === '[object z]'; +const { + Uint8Array: VMUint8Array +} = vm.runInNewContext('this'); -const supportStreamDestroy = 'destroy' in stream.Readable.prototype; +import chaiTimeout from './utils/chai-timeout'; + +chai.use(chaiPromised); +chai.use(chaiIterator); +chai.use(chaiString); +chai.use(chaiTimeout); +const {expect} = chai; const local = new TestServer(); const base = `http://${local.hostname}:${local.port}/`; @@ -69,15 +58,29 @@ after(done => { local.stop(done); }); +const itIf = value => value ? it : it.skip; + +function streamToPromise(stream, dataHandler) { + return new Promise((resolve, reject) => { + stream.on('data', (...args) => { + Promise.resolve() + .then(() => dataHandler(...args)) + .catch(reject); + }); + stream.on('end', resolve); + stream.on('error', reject); + }); +} + describe('node-fetch', () => { - it('should return a promise', function() { + it('should return a promise', () => { const url = `${base}hello`; const p = fetch(url); expect(p).to.be.an.instanceof(fetch.Promise); expect(p).to.have.property('then'); }); - it('should allow custom promise', function() { + it('should allow custom promise', () => { const url = `${base}hello`; const old = fetch.Promise; fetch.Promise = then; @@ -86,52 +89,69 @@ describe('node-fetch', () => { fetch.Promise = old; }); - it('should throw error when no promise implementation are found', function() { + it('should throw error when no promise implementation are found', () => { const url = `${base}hello`; const old = fetch.Promise; fetch.Promise = undefined; expect(() => { - fetch(url) + fetch(url); }).to.throw(Error); fetch.Promise = old; }); - it('should expose Headers, Response and Request constructors', function() { + it('should expose Headers, Response and Request constructors', () => { expect(FetchError).to.equal(FetchErrorOrig); expect(Headers).to.equal(HeadersOrig); expect(Response).to.equal(ResponseOrig); expect(Request).to.equal(RequestOrig); }); - (supportToString ? it : it.skip)('should support proper toString output for Headers, Response and Request objects', function() { + it('should support proper toString output for Headers, Response and Request objects', () => { expect(new Headers().toString()).to.equal('[object Headers]'); expect(new Response().toString()).to.equal('[object Response]'); expect(new Request(base).toString()).to.equal('[object Request]'); }); - it('should reject with error if url is protocol relative', function() { + it('should reject with error if url is protocol relative', () => { const url = '//example.com/'; return expect(fetch(url)).to.eventually.be.rejectedWith(TypeError, 'Only absolute URLs are supported'); }); - it('should reject with error if url is relative path', function() { + it('should reject with error if url is relative path', () => { const url = '/some/path'; return expect(fetch(url)).to.eventually.be.rejectedWith(TypeError, 'Only absolute URLs are supported'); }); - it('should reject with error if protocol is unsupported', function() { + it('should reject with error if protocol is unsupported', () => { const url = 'ftp://example.com/'; return expect(fetch(url)).to.eventually.be.rejectedWith(TypeError, 'Only HTTP(S) protocols are supported'); }); - it('should reject with error on network failure', function() { + itIf(process.platform !== 'win32')('should reject with error on network failure', () => { const url = 'http://localhost:50000/'; return expect(fetch(url)).to.eventually.be.rejected .and.be.an.instanceOf(FetchError) - .and.include({ type: 'system', code: 'ECONNREFUSED', errno: 'ECONNREFUSED' }); + .and.include({type: 'system', code: 'ECONNREFUSED', errno: 'ECONNREFUSED'}); }); - it('should resolve into response', function() { + it('error should contain system error if one occurred', () => { + const err = new FetchError('a message', 'system', new Error('an error')); + return expect(err).to.have.property('erroredSysCall'); + }); + + it('error should not contain system error if none occurred', () => { + const err = new FetchError('a message', 'a type'); + return expect(err).to.not.have.property('erroredSysCall'); + }); + + itIf(process.platform !== 'win32')('system error is extracted from failed requests', () => { + const url = 'http://localhost:50000/'; + return expect(fetch(url)).to.eventually.be.rejected + .and.be.an.instanceOf(FetchError) + .and.have.property('erroredSysCall'); + }); + + it('should resolve into response', () => { const url = `${base}hello`; return fetch(url).then(res => { expect(res).to.be.an.instanceof(Response); @@ -146,7 +166,27 @@ describe('node-fetch', () => { }); }); - it('should accept plain text response', function() { + it('Response.redirect should resolve into response', () => { + const res = Response.redirect('http://localhost'); + expect(res).to.be.an.instanceof(Response); + expect(res.headers).to.be.an.instanceof(Headers); + expect(res.headers.get('location')).to.equal('http://localhost/'); + expect(res.status).to.equal(302); + }); + + it('Response.redirect /w invalid url should fail', () => { + expect(() => { + Response.redirect('localhost'); + }).to.throw(); + }); + + it('Response.redirect /w invalid status should fail', () => { + expect(() => { + Response.redirect('http://localhost', 200); + }).to.throw(); + }); + + it('should accept plain text response', () => { const url = `${base}plain`; return fetch(url).then(res => { expect(res.headers.get('content-type')).to.equal('text/plain'); @@ -158,7 +198,7 @@ describe('node-fetch', () => { }); }); - it('should accept html response (like plain text)', function() { + it('should accept html response (like plain text)', () => { const url = `${base}html`; return fetch(url).then(res => { expect(res.headers.get('content-type')).to.equal('text/html'); @@ -170,71 +210,71 @@ describe('node-fetch', () => { }); }); - it('should accept json response', function() { + it('should accept json response', () => { const url = `${base}json`; return fetch(url).then(res => { expect(res.headers.get('content-type')).to.equal('application/json'); return res.json().then(result => { expect(res.bodyUsed).to.be.true; expect(result).to.be.an('object'); - expect(result).to.deep.equal({ name: 'value' }); + expect(result).to.deep.equal({name: 'value'}); }); }); }); - it('should send request with custom headers', function() { + it('should send request with custom headers', () => { const url = `${base}inspect`; - const opts = { - headers: { 'x-custom-header': 'abc' } + const options = { + headers: {'x-custom-header': 'abc'} }; - return fetch(url, opts).then(res => { + return fetch(url, options).then(res => { return res.json(); }).then(res => { expect(res.headers['x-custom-header']).to.equal('abc'); }); }); - it('should accept headers instance', function() { + it('should accept headers instance', () => { const url = `${base}inspect`; - const opts = { - headers: new Headers({ 'x-custom-header': 'abc' }) + const options = { + headers: new Headers({'x-custom-header': 'abc'}) }; - return fetch(url, opts).then(res => { + return fetch(url, options).then(res => { return res.json(); }).then(res => { expect(res.headers['x-custom-header']).to.equal('abc'); }); }); - it('should accept custom host header', function() { + it('should accept custom host header', () => { const url = `${base}inspect`; - const opts = { + const options = { headers: { host: 'example.com' } }; - return fetch(url, opts).then(res => { + return fetch(url, options).then(res => { return res.json(); }).then(res => { - expect(res.headers['host']).to.equal('example.com'); + expect(res.headers.host).to.equal('example.com'); }); }); - it('should accept custom HoSt header', function() { + it('should accept custom HoSt header', () => { const url = `${base}inspect`; - const opts = { + const options = { headers: { HoSt: 'example.com' } }; - return fetch(url, opts).then(res => { + return fetch(url, options).then(res => { return res.json(); }).then(res => { - expect(res.headers['host']).to.equal('example.com'); + expect(res.headers.host).to.equal('example.com'); }); }); - it('should follow redirect code 301', function() { + it('should follow redirect code 301', () => { const url = `${base}redirect/301`; return fetch(url).then(res => { expect(res.url).to.equal(`${base}inspect`); @@ -243,7 +283,7 @@ describe('node-fetch', () => { }); }); - it('should follow redirect code 302', function() { + it('should follow redirect code 302', () => { const url = `${base}redirect/302`; return fetch(url).then(res => { expect(res.url).to.equal(`${base}inspect`); @@ -251,7 +291,7 @@ describe('node-fetch', () => { }); }); - it('should follow redirect code 303', function() { + it('should follow redirect code 303', () => { const url = `${base}redirect/303`; return fetch(url).then(res => { expect(res.url).to.equal(`${base}inspect`); @@ -259,7 +299,7 @@ describe('node-fetch', () => { }); }); - it('should follow redirect code 307', function() { + it('should follow redirect code 307', () => { const url = `${base}redirect/307`; return fetch(url).then(res => { expect(res.url).to.equal(`${base}inspect`); @@ -267,7 +307,7 @@ describe('node-fetch', () => { }); }); - it('should follow redirect code 308', function() { + it('should follow redirect code 308', () => { const url = `${base}redirect/308`; return fetch(url).then(res => { expect(res.url).to.equal(`${base}inspect`); @@ -275,7 +315,7 @@ describe('node-fetch', () => { }); }); - it('should follow redirect chain', function() { + it('should follow redirect chain', () => { const url = `${base}redirect/chain`; return fetch(url).then(res => { expect(res.url).to.equal(`${base}inspect`); @@ -283,13 +323,13 @@ describe('node-fetch', () => { }); }); - it('should follow POST request redirect code 301 with GET', function() { + it('should follow POST request redirect code 301 with GET', () => { const url = `${base}redirect/301`; - const opts = { + const options = { method: 'POST', body: 'a=1' }; - return fetch(url, opts).then(res => { + return fetch(url, options).then(res => { expect(res.url).to.equal(`${base}inspect`); expect(res.status).to.equal(200); return res.json().then(result => { @@ -299,13 +339,13 @@ describe('node-fetch', () => { }); }); - it('should follow PATCH request redirect code 301 with PATCH', function() { + it('should follow PATCH request redirect code 301 with PATCH', () => { const url = `${base}redirect/301`; - const opts = { + const options = { method: 'PATCH', body: 'a=1' }; - return fetch(url, opts).then(res => { + return fetch(url, options).then(res => { expect(res.url).to.equal(`${base}inspect`); expect(res.status).to.equal(200); return res.json().then(res => { @@ -315,13 +355,13 @@ describe('node-fetch', () => { }); }); - it('should follow POST request redirect code 302 with GET', function() { + it('should follow POST request redirect code 302 with GET', () => { const url = `${base}redirect/302`; - const opts = { + const options = { method: 'POST', body: 'a=1' }; - return fetch(url, opts).then(res => { + return fetch(url, options).then(res => { expect(res.url).to.equal(`${base}inspect`); expect(res.status).to.equal(200); return res.json().then(result => { @@ -331,13 +371,13 @@ describe('node-fetch', () => { }); }); - it('should follow PATCH request redirect code 302 with PATCH', function() { + it('should follow PATCH request redirect code 302 with PATCH', () => { const url = `${base}redirect/302`; - const opts = { + const options = { method: 'PATCH', body: 'a=1' }; - return fetch(url, opts).then(res => { + return fetch(url, options).then(res => { expect(res.url).to.equal(`${base}inspect`); expect(res.status).to.equal(200); return res.json().then(res => { @@ -347,13 +387,13 @@ describe('node-fetch', () => { }); }); - it('should follow redirect code 303 with GET', function() { + it('should follow redirect code 303 with GET', () => { const url = `${base}redirect/303`; - const opts = { + const options = { method: 'PUT', body: 'a=1' }; - return fetch(url, opts).then(res => { + return fetch(url, options).then(res => { expect(res.url).to.equal(`${base}inspect`); expect(res.status).to.equal(200); return res.json().then(result => { @@ -363,13 +403,13 @@ describe('node-fetch', () => { }); }); - it('should follow PATCH request redirect code 307 with PATCH', function() { + it('should follow PATCH request redirect code 307 with PATCH', () => { const url = `${base}redirect/307`; - const opts = { + const options = { method: 'PATCH', body: 'a=1' }; - return fetch(url, opts).then(res => { + return fetch(url, options).then(res => { expect(res.url).to.equal(`${base}inspect`); expect(res.status).to.equal(200); return res.json().then(result => { @@ -379,88 +419,88 @@ describe('node-fetch', () => { }); }); - it('should not follow non-GET redirect if body is a readable stream', function() { + it('should not follow non-GET redirect if body is a readable stream', () => { const url = `${base}redirect/307`; - const opts = { + const options = { method: 'PATCH', body: resumer().queue('a=1').end() }; - return expect(fetch(url, opts)).to.eventually.be.rejected + return expect(fetch(url, options)).to.eventually.be.rejected .and.be.an.instanceOf(FetchError) .and.have.property('type', 'unsupported-redirect'); }); - it('should obey maximum redirect, reject case', function() { + it('should obey maximum redirect, reject case', () => { const url = `${base}redirect/chain`; - const opts = { + const options = { follow: 1 - } - return expect(fetch(url, opts)).to.eventually.be.rejected + }; + return expect(fetch(url, options)).to.eventually.be.rejected .and.be.an.instanceOf(FetchError) .and.have.property('type', 'max-redirect'); }); - it('should obey redirect chain, resolve case', function() { + it('should obey redirect chain, resolve case', () => { const url = `${base}redirect/chain`; - const opts = { + const options = { follow: 2 - } - return fetch(url, opts).then(res => { + }; + return fetch(url, options).then(res => { expect(res.url).to.equal(`${base}inspect`); expect(res.status).to.equal(200); }); }); - it('should allow not following redirect', function() { + it('should allow not following redirect', () => { const url = `${base}redirect/301`; - const opts = { + const options = { follow: 0 - } - return expect(fetch(url, opts)).to.eventually.be.rejected + }; + return expect(fetch(url, options)).to.eventually.be.rejected .and.be.an.instanceOf(FetchError) .and.have.property('type', 'max-redirect'); }); - it('should support redirect mode, manual flag', function() { + it('should support redirect mode, manual flag', () => { const url = `${base}redirect/301`; - const opts = { + const options = { redirect: 'manual' }; - return fetch(url, opts).then(res => { + return fetch(url, options).then(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() { + it('should support redirect mode, error flag', () => { const url = `${base}redirect/301`; - const opts = { + const options = { redirect: 'error' }; - return expect(fetch(url, opts)).to.eventually.be.rejected + return expect(fetch(url, options)).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() { + it('should support redirect mode, manual flag when there is no redirect', () => { const url = `${base}hello`; - const opts = { + const options = { redirect: 'manual' }; - return fetch(url, opts).then(res => { + return fetch(url, options).then(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() { + it('should follow redirect code 301 and keep existing headers', () => { const url = `${base}redirect/301`; - const opts = { - headers: new Headers({ 'x-custom-header': 'abc' }) + const options = { + headers: new Headers({'x-custom-header': 'abc'}) }; - return fetch(url, opts).then(res => { + return fetch(url, options).then(res => { expect(res.url).to.equal(`${base}inspect`); return res.json(); }).then(res => { @@ -468,7 +508,7 @@ describe('node-fetch', () => { }); }); - it('should treat broken redirect as ordinary response (follow)', function() { + it('should treat broken redirect as ordinary response (follow)', () => { const url = `${base}redirect/no-location`; return fetch(url).then(res => { expect(res.url).to.equal(url); @@ -477,37 +517,37 @@ describe('node-fetch', () => { }); }); - it('should treat broken redirect as ordinary response (manual)', function() { + it('should treat broken redirect as ordinary response (manual)', () => { const url = `${base}redirect/no-location`; - const opts = { + const options = { redirect: 'manual' }; - return fetch(url, opts).then(res => { + return fetch(url, options).then(res => { expect(res.url).to.equal(url); expect(res.status).to.equal(301); expect(res.headers.get('location')).to.be.null; }); }); - it('should set redirected property on response when redirect', function() { + it('should set redirected property on response when redirect', () => { const url = `${base}redirect/301`; return fetch(url).then(res => { expect(res.redirected).to.be.true; }); }); - it('should not set redirected property on response without redirect', function() { - const url = `${base}hello`; + it('should not set redirected property on response without redirect', () => { + const url = `${base}hello`; return fetch(url).then(res => { expect(res.redirected).to.be.false; }); }); - it('should ignore invalid headers', function() { - var headers = { + it('should ignore invalid headers', () => { + let headers = { 'Invalid-Header ': 'abc\r\n', - 'Invalid-Header-Value': '\x07k\r\n', - 'Set-Cookie': ['\x07k\r\n', '\x07kk\r\n'] + 'Invalid-Header-Value': '\u0007k\r\n', + 'Set-Cookie': ['\u0007k\r\n', '\u0007kk\r\n'] }; headers = createHeadersLenient(headers); expect(headers).to.not.have.property('Invalid-Header '); @@ -515,7 +555,7 @@ describe('node-fetch', () => { expect(headers).to.not.have.property('Set-Cookie'); }); - it('should handle client-error response', function() { + it('should handle client-error response', () => { const url = `${base}error/400`; return fetch(url).then(res => { expect(res.headers.get('content-type')).to.equal('text/plain'); @@ -530,7 +570,7 @@ describe('node-fetch', () => { }); }); - it('should handle server-error response', function() { + it('should handle server-error response', () => { const url = `${base}error/500`; return fetch(url).then(res => { expect(res.headers.get('content-type')).to.equal('text/plain'); @@ -545,31 +585,29 @@ describe('node-fetch', () => { }); }); - it('should handle network-error response', function() { + it('should handle network-error response', () => { const 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() { + it('should handle DNS-error response', () => { const 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() { + it('should reject invalid json response', () => { const url = `${base}error/json`; return fetch(url).then(res => { expect(res.headers.get('content-type')).to.equal('application/json'); - return expect(res.json()).to.eventually.be.rejected - .and.be.an.instanceOf(FetchError) - .and.include({ type: 'invalid-json' }); + return expect(res.json()).to.eventually.be.rejectedWith(Error); }); }); - it('should handle no content response', function() { + it('should handle no content response', () => { const url = `${base}no-content`; return fetch(url).then(res => { expect(res.status).to.equal(204); @@ -582,19 +620,17 @@ describe('node-fetch', () => { }); }); - it('should reject when trying to parse no content response as json', function() { + it('should reject when trying to parse no content response as json', () => { const url = `${base}no-content`; return fetch(url).then(res => { expect(res.status).to.equal(204); expect(res.statusText).to.equal('No Content'); expect(res.ok).to.be.true; - return expect(res.json()).to.eventually.be.rejected - .and.be.an.instanceOf(FetchError) - .and.include({ type: 'invalid-json' }); + return expect(res.json()).to.eventually.be.rejectedWith(Error); }); }); - it('should handle no content response with gzip encoding', function() { + it('should handle no content response with gzip encoding', () => { const url = `${base}no-content/gzip`; return fetch(url).then(res => { expect(res.status).to.equal(204); @@ -608,7 +644,7 @@ describe('node-fetch', () => { }); }); - it('should handle not modified response', function() { + it('should handle not modified response', () => { const url = `${base}not-modified`; return fetch(url).then(res => { expect(res.status).to.equal(304); @@ -621,7 +657,7 @@ describe('node-fetch', () => { }); }); - it('should handle not modified response with gzip encoding', function() { + it('should handle not modified response with gzip encoding', () => { const url = `${base}not-modified/gzip`; return fetch(url).then(res => { expect(res.status).to.equal(304); @@ -635,7 +671,7 @@ describe('node-fetch', () => { }); }); - it('should decompress gzip response', function() { + it('should decompress gzip response', () => { const url = `${base}gzip`; return fetch(url).then(res => { expect(res.headers.get('content-type')).to.equal('text/plain'); @@ -646,7 +682,7 @@ describe('node-fetch', () => { }); }); - it('should decompress slightly invalid gzip response', function() { + it('should decompress slightly invalid gzip response', () => { const url = `${base}gzip-truncated`; return fetch(url).then(res => { expect(res.headers.get('content-type')).to.equal('text/plain'); @@ -657,7 +693,18 @@ describe('node-fetch', () => { }); }); - it('should decompress deflate response', function() { + it('should make capitalised Content-Encoding lowercase', () => { + const url = `${base}gzip-capital`; + return fetch(url).then(res => { + expect(res.headers.get('content-encoding')).to.equal('gzip'); + return res.text().then(result => { + expect(result).to.be.a('string'); + expect(result).to.equal('hello world'); + }); + }); + }); + + it('should decompress deflate response', () => { const url = `${base}deflate`; return fetch(url).then(res => { expect(res.headers.get('content-type')).to.equal('text/plain'); @@ -668,7 +715,7 @@ describe('node-fetch', () => { }); }); - it('should decompress deflate raw response from old apache server', function() { + it('should decompress deflate raw response from old apache server', () => { const url = `${base}deflate-raw`; return fetch(url).then(res => { expect(res.headers.get('content-type')).to.equal('text/plain'); @@ -679,8 +726,11 @@ describe('node-fetch', () => { }); }); - it('should decompress brotli response', function() { - if(typeof zlib.createBrotliDecompress !== 'function') this.skip(); + it('should decompress brotli response', function () { + if (typeof zlib.createBrotliDecompress !== 'function') { + this.skip(); + } + const url = `${base}brotli`; return fetch(url).then(res => { expect(res.headers.get('content-type')).to.equal('text/plain'); @@ -691,8 +741,11 @@ describe('node-fetch', () => { }); }); - it('should handle no content response with brotli encoding', function() { - if(typeof zlib.createBrotliDecompress !== 'function') this.skip(); + it('should handle no content response with brotli encoding', function () { + if (typeof zlib.createBrotliDecompress !== 'function') { + this.skip(); + } + const url = `${base}no-content/brotli`; return fetch(url).then(res => { expect(res.status).to.equal(204); @@ -706,7 +759,7 @@ describe('node-fetch', () => { }); }); - it('should skip decompression if unsupported', function() { + it('should skip decompression if unsupported', () => { const url = `${base}sdch`; return fetch(url).then(res => { expect(res.headers.get('content-type')).to.equal('text/plain'); @@ -717,7 +770,7 @@ describe('node-fetch', () => { }); }); - it('should reject if response compression is invalid', function() { + it('should reject if response compression is invalid', () => { const url = `${base}invalid-content-encoding`; return fetch(url).then(res => { expect(res.headers.get('content-type')).to.equal('text/plain'); @@ -727,13 +780,13 @@ describe('node-fetch', () => { }); }); - it('should handle errors on the body stream even if it is not used', function(done) { + it('should handle errors on the body stream even if it is not used', done => { const url = `${base}invalid-content-encoding`; fetch(url) .then(res => { expect(res.status).to.equal(200); }) - .catch(() => {}) + .catch(() => { }) .then(() => { // Wait a few ms to see if a uncaught error occurs setTimeout(() => { @@ -742,12 +795,11 @@ describe('node-fetch', () => { }); }); - it('should collect handled errors on the body stream to reject if the body is used later', function() { - + it('should collect handled errors on the body stream to reject if the body is used later', () => { function delay(value) { - return new Promise((resolve) => { + return new Promise(resolve => { setTimeout(() => { - resolve(value) + resolve(value); }, 20); }); } @@ -761,12 +813,12 @@ describe('node-fetch', () => { }); }); - it('should allow disabling auto decompression', function() { + it('should allow disabling auto decompression', () => { const url = `${base}gzip`; - const opts = { + const options = { compress: false }; - return fetch(url, opts).then(res => { + return fetch(url, options).then(res => { expect(res.headers.get('content-type')).to.equal('text/plain'); return res.text().then(result => { expect(result).to.be.a('string'); @@ -775,35 +827,35 @@ describe('node-fetch', () => { }); }); - it('should not overwrite existing accept-encoding header when auto decompression is true', function() { + it('should not overwrite existing accept-encoding header when auto decompression is true', () => { const url = `${base}inspect`; - const opts = { + const options = { compress: true, headers: { 'Accept-Encoding': 'gzip' } }; - return fetch(url, opts).then(res => res.json()).then(res => { + return fetch(url, options).then(res => res.json()).then(res => { expect(res.headers['accept-encoding']).to.equal('gzip'); }); }); - it('should allow custom timeout', function() { + it('should allow custom timeout', () => { const url = `${base}timeout`; - const opts = { + const options = { timeout: 20 }; - return expect(fetch(url, opts)).to.eventually.be.rejected + return expect(fetch(url, options)).to.eventually.be.rejected .and.be.an.instanceOf(FetchError) .and.have.property('type', 'request-timeout'); }); - it('should allow custom timeout on response body', function() { + it('should allow custom timeout on response body', () => { const url = `${base}slow`; - const opts = { + const options = { timeout: 20 }; - return fetch(url, opts).then(res => { + return fetch(url, options).then(res => { expect(res.ok).to.be.true; return expect(res.text()).to.eventually.be.rejected .and.be.an.instanceOf(FetchError) @@ -811,19 +863,19 @@ describe('node-fetch', () => { }); }); - it('should allow custom timeout on redirected requests', function() { + it('should allow custom timeout on redirected requests', () => { const url = `${base}redirect/slow-chain`; - const opts = { + const options = { timeout: 20 }; - return expect(fetch(url, opts)).to.eventually.be.rejected + return expect(fetch(url, options)).to.eventually.be.rejected .and.be.an.instanceOf(FetchError) .and.have.property('type', 'request-timeout'); }); it('should clear internal timeout on fetch response', function (done) { this.timeout(2000); - spawn('node', ['-e', `require('./')('${base}hello', { timeout: 10000 })`]) + spawn('node', ['-e', `require(’./’)(’${base}hello’, { timeout: 10000 })`]) .on('exit', () => { done(); }); @@ -831,7 +883,7 @@ describe('node-fetch', () => { it('should clear internal timeout on fetch redirect', function (done) { this.timeout(2000); - spawn('node', ['-e', `require('./')('${base}redirect/301', { timeout: 10000 })`]) + spawn('node', ['-e', `require(’./’)(’${base}redirect/301’, { timeout: 10000 })`]) .on('exit', () => { done(); }); @@ -839,7 +891,7 @@ describe('node-fetch', () => { it('should clear internal timeout on fetch error', function (done) { this.timeout(2000); - spawn('node', ['-e', `require('./')('${base}error/reset', { timeout: 10000 })`]) + spawn('node', ['-e', `require(’./’)(’${base}error/reset’, { timeout: 10000 })`]) .on('exit', () => { done(); }); @@ -851,8 +903,8 @@ describe('node-fetch', () => { const controller2 = new AbortController2(); const fetches = [ - fetch(`${base}timeout`, { signal: controller.signal }), - fetch(`${base}timeout`, { signal: controller2.signal }), + fetch(`${base}timeout`, {signal: controller.signal}), + fetch(`${base}timeout`, {signal: controller2.signal}), fetch( `${base}timeout`, { @@ -860,7 +912,7 @@ describe('node-fetch', () => { signal: controller.signal, headers: { 'Content-Type': 'application/json', - body: JSON.stringify({ hello: 'world' }) + body: JSON.stringify({hello: 'world'}) } } ) @@ -875,50 +927,50 @@ describe('node-fetch', () => { .and.be.an.instanceOf(Error) .and.include({ type: 'aborted', - name: 'AbortError', + name: 'AbortError' }) )); }); - it('should reject immediately if signal has already been aborted', function () { + it('should reject immediately if signal has already been aborted', () => { const url = `${base}timeout`; const controller = new AbortController(); - const opts = { + const options = { signal: controller.signal }; controller.abort(); - const fetched = fetch(url, opts); + const fetched = fetch(url, options); return expect(fetched).to.eventually.be.rejected .and.be.an.instanceOf(Error) .and.include({ type: 'aborted', - name: 'AbortError', + name: 'AbortError' }); }); - it('should clear internal timeout when request is cancelled with an AbortSignal', function(done) { + it('should clear internal timeout when request is cancelled with an AbortSignal', function (done) { this.timeout(2000); const script = ` - var AbortController = require('abortcontroller-polyfill/dist/cjs-ponyfill').AbortController; + var AbortController = require(’abortcontroller-polyfill/dist/cjs-ponyfill’).AbortController; var controller = new AbortController(); - require('./')( - '${base}timeout', + require(’./’)( + ’${base}timeout’, { signal: controller.signal, timeout: 10000 } ); setTimeout(function () { controller.abort(); }, 20); - ` + `; spawn('node', ['-e', script]) .on('exit', () => { done(); }); }); - it('should remove internal AbortSignal event listener after request is aborted', function () { + it('should remove internal AbortSignal event listener after request is aborted', () => { const controller = new AbortController(); - const { signal } = controller; + const {signal} = controller; const promise = fetch( `${base}timeout`, - { signal } + {signal} ); const result = expect(promise).to.eventually.be.rejected .and.be.an.instanceof(Error) @@ -930,7 +982,7 @@ describe('node-fetch', () => { return result; }); - it('should allow redirects to be aborted', function() { + it('should allow redirects to be aborted', () => { const abortController = new AbortController(); const request = new Request(`${base}redirect/slow`, { signal: abortController.signal @@ -943,7 +995,7 @@ describe('node-fetch', () => { .and.have.property('name', 'AbortError'); }); - it('should allow redirected response body to be aborted', function() { + it('should allow redirected response body to be aborted', () => { const abortController = new AbortController(); const request = new Request(`${base}redirect/slow-stream`, { signal: abortController.signal @@ -960,17 +1012,17 @@ describe('node-fetch', () => { it('should remove internal AbortSignal event listener after request and response complete without aborting', () => { const controller = new AbortController(); - const { signal } = controller; - const fetchHtml = fetch(`${base}html`, { signal }) + const {signal} = controller; + const fetchHtml = fetch(`${base}html`, {signal}) .then(res => res.text()); - const fetchResponseError = fetch(`${base}error/reset`, { signal }); - const fetchRedirect = fetch(`${base}redirect/301`, { signal }).then(res => res.json()); + const fetchResponseError = fetch(`${base}error/reset`, {signal}); + const fetchRedirect = fetch(`${base}redirect/301`, {signal}).then(res => res.json()); return Promise.all([ expect(fetchHtml).to.eventually.be.fulfilled.and.equal(''), expect(fetchResponseError).to.be.eventually.rejected, - expect(fetchRedirect).to.eventually.be.fulfilled, + expect(fetchRedirect).to.eventually.be.fulfilled ]).then(() => { - expect(signal.listeners.abort.length).to.equal(0) + expect(signal.listeners.abort.length).to.equal(0); }); }); @@ -978,10 +1030,10 @@ describe('node-fetch', () => { const controller = new AbortController(); return expect(fetch( `${base}slow`, - { signal: controller.signal } + {signal: controller.signal} )) .to.eventually.be.fulfilled - .then((res) => { + .then(res => { const promise = res.text(); controller.abort(); return expect(promise) @@ -995,10 +1047,10 @@ describe('node-fetch', () => { const controller = new AbortController(); return expect(fetch( `${base}slow`, - { signal: controller.signal } + {signal: controller.signal} )) .to.eventually.be.fulfilled - .then((res) => { + .then(res => { controller.abort(); return expect(res.text()) .to.eventually.be.rejected @@ -1007,15 +1059,15 @@ describe('node-fetch', () => { }); }); - it('should emit error event to response body with an AbortError when aborted before underlying stream is closed', (done) => { + it('should emit error event to response body with an AbortError when aborted before underlying stream is closed', done => { const controller = new AbortController(); expect(fetch( `${base}slow`, - { signal: controller.signal } + {signal: controller.signal} )) .to.eventually.be.fulfilled - .then((res) => { - res.body.on('error', (err) => { + .then(res => { + res.body.once('error', err => { expect(err) .to.be.an.instanceof(Error) .and.have.property('name', 'AbortError'); @@ -1025,23 +1077,23 @@ describe('node-fetch', () => { }); }); - (supportStreamDestroy ? it : it.skip)('should cancel request body of type Stream with AbortError when aborted', () => { + it('should cancel request body of type Stream with AbortError when aborted', () => { const controller = new AbortController(); - const body = new stream.Readable({ objectMode: true }); - body._read = () => {}; + const body = new stream.Readable({objectMode: true}); + body._read = () => { }; const promise = fetch( `${base}slow`, - { signal: controller.signal, body, method: 'POST' } + {signal: controller.signal, body, method: 'POST'} ); const result = Promise.all([ new Promise((resolve, reject) => { - body.on('error', (error) => { + body.on('error', error => { try { - expect(error).to.be.an.instanceof(Error).and.have.property('name', 'AbortError') + expect(error).to.be.an.instanceof(Error).and.have.property('name', 'AbortError'); resolve(); - } catch (err) { - reject(err); + } catch (error_) { + reject(error_); } }); }), @@ -1055,81 +1107,67 @@ describe('node-fetch', () => { return result; }); - (supportStreamDestroy ? it.skip : it)('should immediately reject when attempting to cancel streamed Requests in node < 8', () => { - const controller = new AbortController(); - const body = new stream.Readable({ objectMode: true }); - body._read = () => {}; - const promise = fetch( - `${base}slow`, - { signal: controller.signal, body, method: 'POST' } - ); - - return expect(promise).to.eventually.be.rejected - .and.be.an.instanceof(Error) - .and.have.property('message').includes('not supported'); - }); - it('should throw a TypeError if a signal is not of type AbortSignal', () => { return Promise.all([ - expect(fetch(`${base}inspect`, { signal: {} })) + expect(fetch(`${base}inspect`, {signal: {}})) .to.be.eventually.rejected .and.be.an.instanceof(TypeError) .and.have.property('message').includes('AbortSignal'), - expect(fetch(`${base}inspect`, { signal: '' })) + expect(fetch(`${base}inspect`, {signal: ''})) .to.be.eventually.rejected .and.be.an.instanceof(TypeError) .and.have.property('message').includes('AbortSignal'), - expect(fetch(`${base}inspect`, { signal: Object.create(null) })) + expect(fetch(`${base}inspect`, {signal: Object.create(null)})) .to.be.eventually.rejected .and.be.an.instanceof(TypeError) - .and.have.property('message').includes('AbortSignal'), + .and.have.property('message').includes('AbortSignal') ]); }); - it('should set default User-Agent', function () { + it('should set default User-Agent', () => { const url = `${base}inspect`; return fetch(url).then(res => res.json()).then(res => { - expect(res.headers['user-agent']).to.startWith('node-fetch/'); + expect(res.headers['user-agent']).to.startWith('node-fetch'); }); }); - it('should allow setting User-Agent', function () { + it('should allow setting User-Agent', () => { const url = `${base}inspect`; - const opts = { + const options = { headers: { 'user-agent': 'faked' } }; - return fetch(url, opts).then(res => res.json()).then(res => { + return fetch(url, options).then(res => res.json()).then(res => { expect(res.headers['user-agent']).to.equal('faked'); }); }); - it('should set default Accept header', function () { + it('should set default Accept header', () => { const url = `${base}inspect`; fetch(url).then(res => res.json()).then(res => { expect(res.headers.accept).to.equal('*/*'); }); }); - it('should allow setting Accept header', function () { + it('should allow setting Accept header', () => { const url = `${base}inspect`; - const opts = { + const options = { headers: { - 'accept': 'application/json' + accept: 'application/json' } }; - return fetch(url, opts).then(res => res.json()).then(res => { + return fetch(url, options).then(res => res.json()).then(res => { expect(res.headers.accept).to.equal('application/json'); }); }); - it('should allow POST request', function() { + it('should allow POST request', () => { const url = `${base}inspect`; - const opts = { + const options = { method: 'POST' }; - return fetch(url, opts).then(res => { + return fetch(url, options).then(res => { return res.json(); }).then(res => { expect(res.method).to.equal('POST'); @@ -1139,13 +1177,13 @@ describe('node-fetch', () => { }); }); - it('should allow POST request with string body', function() { + it('should allow POST request with string body', () => { const url = `${base}inspect`; - const opts = { + const options = { method: 'POST', body: 'a=1' }; - return fetch(url, opts).then(res => { + return fetch(url, options).then(res => { return res.json(); }).then(res => { expect(res.method).to.equal('POST'); @@ -1156,13 +1194,13 @@ describe('node-fetch', () => { }); }); - it('should allow POST request with buffer body', function() { + it('should allow POST request with buffer body', () => { const url = `${base}inspect`; - const opts = { + const options = { method: 'POST', body: Buffer.from('a=1', 'utf-8') }; - return fetch(url, opts).then(res => { + return fetch(url, options).then(res => { return res.json(); }).then(res => { expect(res.method).to.equal('POST'); @@ -1173,13 +1211,13 @@ describe('node-fetch', () => { }); }); - it('should allow POST request with ArrayBuffer body', function() { + it('should allow POST request with ArrayBuffer body', () => { const url = `${base}inspect`; - const opts = { + const options = { method: 'POST', body: stringToArrayBuffer('Hello, world!\n') }; - return fetch(url, opts).then(res => res.json()).then(res => { + return fetch(url, options).then(res => res.json()).then(res => { expect(res.method).to.equal('POST'); expect(res.body).to.equal('Hello, world!\n'); expect(res.headers['transfer-encoding']).to.be.undefined; @@ -1188,19 +1226,13 @@ describe('node-fetch', () => { }); }); - it('should allow POST request with ArrayBuffer body from a VM context', function() { - // TODO: Node.js v4 doesn't support ArrayBuffer from other contexts, so we skip this test, drop this check once Node.js v4 support is not needed - try { - Buffer.from(new VMArrayBuffer()); - } catch (err) { - this.skip(); - } + it('should allow POST request with ArrayBuffer body from a VM context', () => { const url = `${base}inspect`; - const opts = { + const options = { method: 'POST', body: new VMUint8Array(Buffer.from('Hello, world!\n')).buffer }; - return fetch(url, opts).then(res => res.json()).then(res => { + return fetch(url, options).then(res => res.json()).then(res => { expect(res.method).to.equal('POST'); expect(res.body).to.equal('Hello, world!\n'); expect(res.headers['transfer-encoding']).to.be.undefined; @@ -1209,13 +1241,13 @@ describe('node-fetch', () => { }); }); - it('should allow POST request with ArrayBufferView (Uint8Array) body', function() { + it('should allow POST request with ArrayBufferView (Uint8Array) body', () => { const url = `${base}inspect`; - const opts = { + const options = { method: 'POST', body: new Uint8Array(stringToArrayBuffer('Hello, world!\n')) }; - return fetch(url, opts).then(res => res.json()).then(res => { + return fetch(url, options).then(res => res.json()).then(res => { expect(res.method).to.equal('POST'); expect(res.body).to.equal('Hello, world!\n'); expect(res.headers['transfer-encoding']).to.be.undefined; @@ -1224,13 +1256,13 @@ describe('node-fetch', () => { }); }); - it('should allow POST request with ArrayBufferView (DataView) body', function() { + it('should allow POST request with ArrayBufferView (DataView) body', () => { const url = `${base}inspect`; - const opts = { + const options = { method: 'POST', body: new DataView(stringToArrayBuffer('Hello, world!\n')) }; - return fetch(url, opts).then(res => res.json()).then(res => { + return fetch(url, options).then(res => res.json()).then(res => { expect(res.method).to.equal('POST'); expect(res.body).to.equal('Hello, world!\n'); expect(res.headers['transfer-encoding']).to.be.undefined; @@ -1239,19 +1271,13 @@ describe('node-fetch', () => { }); }); - it('should allow POST request with ArrayBufferView (Uint8Array) body from a VM context', function() { - // TODO: Node.js v4 doesn't support ArrayBufferView from other contexts, so we skip this test, drop this check once Node.js v4 support is not needed - try { - Buffer.from(new VMArrayBuffer()); - } catch (err) { - this.skip(); - } + it('should allow POST request with ArrayBufferView (Uint8Array) body from a VM context', () => { const url = `${base}inspect`; - const opts = { + const options = { method: 'POST', body: new VMUint8Array(Buffer.from('Hello, world!\n')) }; - return fetch(url, opts).then(res => res.json()).then(res => { + return fetch(url, options).then(res => res.json()).then(res => { expect(res.method).to.equal('POST'); expect(res.body).to.equal('Hello, world!\n'); expect(res.headers['transfer-encoding']).to.be.undefined; @@ -1260,14 +1286,13 @@ describe('node-fetch', () => { }); }); - // TODO: Node.js v4 doesn't support necessary Buffer API, so we skip this test, drop this check once Node.js v4 support is not needed - (Buffer.from.length === 3 ? it : it.skip)('should allow POST request with ArrayBufferView (Uint8Array, offset, length) body', function() { + it('should allow POST request with ArrayBufferView (Uint8Array, offset, length) body', () => { const url = `${base}inspect`; - const opts = { + const options = { method: 'POST', body: new Uint8Array(stringToArrayBuffer('Hello, world!\n'), 7, 6) }; - return fetch(url, opts).then(res => res.json()).then(res => { + return fetch(url, options).then(res => res.json()).then(res => { expect(res.method).to.equal('POST'); expect(res.body).to.equal('world!'); expect(res.headers['transfer-encoding']).to.be.undefined; @@ -1276,13 +1301,13 @@ describe('node-fetch', () => { }); }); - it('should allow POST request with blob body without type', function() { + it('should allow POST request with blob body without type', () => { const url = `${base}inspect`; - const opts = { + const options = { method: 'POST', body: new Blob(['a=1']) }; - return fetch(url, opts).then(res => { + return fetch(url, options).then(res => { return res.json(); }).then(res => { expect(res.method).to.equal('POST'); @@ -1293,15 +1318,15 @@ describe('node-fetch', () => { }); }); - it('should allow POST request with blob body with type', function() { + it('should allow POST request with blob body with type', () => { const url = `${base}inspect`; - const opts = { + const options = { method: 'POST', body: new Blob(['a=1'], { type: 'text/plain;charset=UTF-8' }) }; - return fetch(url, opts).then(res => { + return fetch(url, options).then(res => { return res.json(); }).then(res => { expect(res.method).to.equal('POST'); @@ -1312,16 +1337,16 @@ describe('node-fetch', () => { }); }); - it('should allow POST request with readable stream as body', function() { + it('should allow POST request with readable stream as body', () => { let body = resumer().queue('a=1').end(); body = body.pipe(new stream.PassThrough()); const url = `${base}inspect`; - const opts = { + const options = { method: 'POST', body }; - return fetch(url, opts).then(res => { + return fetch(url, options).then(res => { return res.json(); }).then(res => { expect(res.method).to.equal('POST'); @@ -1332,16 +1357,16 @@ describe('node-fetch', () => { }); }); - it('should allow POST request with form-data as body', function() { + it('should allow POST request with form-data as body', () => { const form = new FormData(); - form.append('a','1'); + form.append('a', '1'); const url = `${base}multipart`; - const opts = { + const options = { method: 'POST', body: form }; - return fetch(url, opts).then(res => { + return fetch(url, options).then(res => { return res.json(); }).then(res => { expect(res.method).to.equal('POST'); @@ -1351,17 +1376,17 @@ describe('node-fetch', () => { }); }); - it('should allow POST request with form-data using stream as body', function() { + itIf(process.platform !== 'win32')('should allow POST request with form-data using stream as body', () => { const form = new FormData(); - form.append('my_field', fs.createReadStream(path.join(__dirname, 'dummy.txt'))); + form.append('my_field', fs.createReadStream(path.join(__dirname, './utils/dummy.txt'))); const url = `${base}multipart`; - const opts = { + const options = { method: 'POST', body: form }; - return fetch(url, opts).then(res => { + return fetch(url, options).then(res => { return res.json(); }).then(res => { expect(res.method).to.equal('POST'); @@ -1371,20 +1396,20 @@ describe('node-fetch', () => { }); }); - it('should allow POST request with form-data as body and custom headers', function() { + it('should allow POST request with form-data as body and custom headers', () => { const form = new FormData(); - form.append('a','1'); + form.append('a', '1'); const headers = form.getHeaders(); - headers['b'] = '2'; + headers.b = '2'; const url = `${base}multipart`; - const opts = { + const options = { method: 'POST', body: form, headers }; - return fetch(url, opts).then(res => { + return fetch(url, options).then(res => { return res.json(); }).then(res => { expect(res.method).to.equal('POST'); @@ -1395,14 +1420,14 @@ describe('node-fetch', () => { }); }); - it('should allow POST request with object body', function() { + it('should allow POST request with object body', () => { const url = `${base}inspect`; - // note that fetch simply calls tostring on an object - const opts = { + // Note that fetch simply calls tostring on an object + const options = { method: 'POST', - body: { a: 1 } + body: {a: 1} }; - return fetch(url, opts).then(res => { + return fetch(url, options).then(res => { return res.json(); }).then(res => { expect(res.method).to.equal('POST'); @@ -1412,49 +1437,47 @@ describe('node-fetch', () => { }); }); - const itUSP = typeof URLSearchParams === 'function' ? it : it.skip; - - itUSP('constructing a Response with URLSearchParams as body should have a Content-Type', function() { - const params = new URLSearchParams(); - const res = new Response(params); + it('constructing a Response with URLSearchParams as body should have a Content-Type', () => { + const parameters = new URLSearchParams(); + const res = new Response(parameters); res.headers.get('Content-Type'); expect(res.headers.get('Content-Type')).to.equal('application/x-www-form-urlencoded;charset=UTF-8'); }); - itUSP('constructing a Request with URLSearchParams as body should have a Content-Type', function() { - const params = new URLSearchParams(); - const req = new Request(base, { method: 'POST', body: params }); - expect(req.headers.get('Content-Type')).to.equal('application/x-www-form-urlencoded;charset=UTF-8'); + it('constructing a Request with URLSearchParams as body should have a Content-Type', () => { + const parameters = new URLSearchParams(); + const request = new Request(base, {method: 'POST', body: parameters}); + expect(request.headers.get('Content-Type')).to.equal('application/x-www-form-urlencoded;charset=UTF-8'); }); - itUSP('Reading a body with URLSearchParams should echo back the result', function() { - const params = new URLSearchParams(); - params.append('a','1'); - return new Response(params).text().then(text => { + it('Reading a body with URLSearchParams should echo back the result', () => { + const parameters = new URLSearchParams(); + parameters.append('a', '1'); + return new Response(parameters).text().then(text => { expect(text).to.equal('a=1'); }); }); // Body should been cloned... - itUSP('constructing a Request/Response with URLSearchParams and mutating it should not affected body', function() { - const params = new URLSearchParams(); - const req = new Request(`${base}inspect`, { method: 'POST', body: params }) - params.append('a','1') - return req.text().then(text => { + it('constructing a Request/Response with URLSearchParams and mutating it should not affected body', () => { + const parameters = new URLSearchParams(); + const request = new Request(`${base}inspect`, {method: 'POST', body: parameters}); + parameters.append('a', '1'); + return request.text().then(text => { expect(text).to.equal(''); }); }); - itUSP('should allow POST request with URLSearchParams as body', function() { - const params = new URLSearchParams(); - params.append('a','1'); + it('should allow POST request with URLSearchParams as body', () => { + const parameters = new URLSearchParams(); + parameters.append('a', '1'); const url = `${base}inspect`; - const opts = { + const options = { method: 'POST', - body: params, + body: parameters }; - return fetch(url, opts).then(res => { + return fetch(url, options).then(res => { return res.json(); }).then(res => { expect(res.method).to.equal('POST'); @@ -1464,17 +1487,17 @@ describe('node-fetch', () => { }); }); - itUSP('should still recognize URLSearchParams when extended', function() { - class CustomSearchParams extends URLSearchParams {} - const params = new CustomSearchParams(); - params.append('a','1'); + it('should still recognize URLSearchParams when extended', () => { + class CustomSearchParameters extends URLSearchParams { } + const parameters = new CustomSearchParameters(); + parameters.append('a', '1'); const url = `${base}inspect`; - const opts = { + const options = { method: 'POST', - body: params, + body: parameters }; - return fetch(url, opts).then(res => { + return fetch(url, options).then(res => { return res.json(); }).then(res => { expect(res.method).to.equal('POST'); @@ -1484,19 +1507,19 @@ describe('node-fetch', () => { }); }); - /* for 100% code coverage, checks for duck-typing-only detection + /* For 100% code coverage, checks for duck-typing-only detection * where both constructor.name and brand tests fail */ - it('should still recognize URLSearchParams when extended from polyfill', function() { - class CustomPolyfilledSearchParams extends URLSearchParams_Polyfill {} - const params = new CustomPolyfilledSearchParams(); - params.append('a','1'); + it('should still recognize URLSearchParams when extended from polyfill', () => { + class CustomPolyfilledSearchParameters extends URLSearchParams { } + const parameters = new CustomPolyfilledSearchParameters(); + parameters.append('a', '1'); const url = `${base}inspect`; - const opts = { + const options = { method: 'POST', - body: params, + body: parameters }; - return fetch(url, opts).then(res => { + return fetch(url, options).then(res => { return res.json(); }).then(res => { expect(res.method).to.equal('POST'); @@ -1506,17 +1529,17 @@ describe('node-fetch', () => { }); }); - it('should overwrite Content-Length if possible', function() { + it('should overwrite Content-Length if possible', () => { const url = `${base}inspect`; - // note that fetch simply calls tostring on an object - const opts = { + // Note that fetch simply calls tostring on an object + const options = { method: 'POST', headers: { 'Content-Length': '1000' }, body: 'a=1' }; - return fetch(url, opts).then(res => { + return fetch(url, options).then(res => { return res.json(); }).then(res => { expect(res.method).to.equal('POST'); @@ -1527,13 +1550,13 @@ describe('node-fetch', () => { }); }); - it('should allow PUT request', function() { + it('should allow PUT request', () => { const url = `${base}inspect`; - const opts = { + const options = { method: 'PUT', body: 'a=1' }; - return fetch(url, opts).then(res => { + return fetch(url, options).then(res => { return res.json(); }).then(res => { expect(res.method).to.equal('PUT'); @@ -1541,25 +1564,25 @@ describe('node-fetch', () => { }); }); - it('should allow DELETE request', function() { + it('should allow DELETE request', () => { const url = `${base}inspect`; - const opts = { + const options = { method: 'DELETE' }; - return fetch(url, opts).then(res => { + return fetch(url, options).then(res => { return res.json(); }).then(res => { expect(res.method).to.equal('DELETE'); }); }); - it('should allow DELETE request with string body', function() { + it('should allow DELETE request with string body', () => { const url = `${base}inspect`; - const opts = { + const options = { method: 'DELETE', body: 'a=1' }; - return fetch(url, opts).then(res => { + return fetch(url, options).then(res => { return res.json(); }).then(res => { expect(res.method).to.equal('DELETE'); @@ -1569,13 +1592,13 @@ describe('node-fetch', () => { }); }); - it('should allow PATCH request', function() { + it('should allow PATCH request', () => { const url = `${base}inspect`; - const opts = { + const options = { method: 'PATCH', body: 'a=1' }; - return fetch(url, opts).then(res => { + return fetch(url, options).then(res => { return res.json(); }).then(res => { expect(res.method).to.equal('PATCH'); @@ -1583,12 +1606,12 @@ describe('node-fetch', () => { }); }); - it('should allow HEAD request', function() { + it('should allow HEAD request', () => { const url = `${base}hello`; - const opts = { + const options = { method: 'HEAD' }; - return fetch(url, opts).then(res => { + return fetch(url, options).then(res => { expect(res.status).to.equal(200); expect(res.statusText).to.equal('OK'); expect(res.headers.get('content-type')).to.equal('text/plain'); @@ -1599,12 +1622,12 @@ describe('node-fetch', () => { }); }); - it('should allow HEAD request with content-encoding header', function() { + it('should allow HEAD request with content-encoding header', () => { const url = `${base}error/404`; - const opts = { + const options = { method: 'HEAD' }; - return fetch(url, opts).then(res => { + return fetch(url, options).then(res => { expect(res.status).to.equal(404); expect(res.headers.get('content-encoding')).to.equal('gzip'); return res.text(); @@ -1613,12 +1636,12 @@ describe('node-fetch', () => { }); }); - it('should allow OPTIONS request', function() { + it('should allow OPTIONS request', () => { const url = `${base}options`; - const opts = { + const options = { method: 'OPTIONS' }; - return fetch(url, opts).then(res => { + return fetch(url, options).then(res => { expect(res.status).to.equal(200); expect(res.statusText).to.equal('OK'); expect(res.headers.get('allow')).to.equal('GET, HEAD, OPTIONS'); @@ -1626,23 +1649,23 @@ describe('node-fetch', () => { }); }); - it('should reject decoding body twice', function() { + it('should reject decoding body twice', () => { const url = `${base}plain`; return fetch(url).then(res => { expect(res.headers.get('content-type')).to.equal('text/plain'); - return res.text().then(result => { + return res.text().then(() => { expect(res.bodyUsed).to.be.true; return expect(res.text()).to.eventually.be.rejectedWith(Error); }); }); }); - it('should support maximum response size, multiple chunk', function() { + it('should support maximum response size, multiple chunk', () => { const url = `${base}size/chunk`; - const opts = { + const options = { size: 5 }; - return fetch(url, opts).then(res => { + return fetch(url, options).then(res => { expect(res.status).to.equal(200); expect(res.headers.get('content-type')).to.equal('text/plain'); return expect(res.text()).to.eventually.be.rejected @@ -1651,12 +1674,12 @@ describe('node-fetch', () => { }); }); - it('should support maximum response size, single chunk', function() { + it('should support maximum response size, single chunk', () => { const url = `${base}size/long`; - const opts = { + const options = { size: 5 }; - return fetch(url, opts).then(res => { + return fetch(url, options).then(res => { expect(res.status).to.equal(200); expect(res.headers.get('content-type')).to.equal('text/plain'); return expect(res.text()).to.eventually.be.rejected @@ -1665,7 +1688,7 @@ describe('node-fetch', () => { }); }); - it('should allow piping response body as stream', function() { + it('should allow piping response body as stream', () => { const url = `${base}hello`; return fetch(url).then(res => { expect(res.body).to.be.an.instanceof(stream.Transform); @@ -1673,12 +1696,13 @@ describe('node-fetch', () => { if (chunk === null) { return; } + expect(chunk.toString()).to.equal('world'); }); }); }); - it('should allow cloning a response, and use both as stream', function() { + it('should allow cloning a response, and use both as stream', () => { const url = `${base}hello`; return fetch(url).then(res => { const r1 = res.clone(); @@ -1688,6 +1712,7 @@ describe('node-fetch', () => { if (chunk === null) { return; } + expect(chunk.toString()).to.equal('world'); }; @@ -1698,7 +1723,7 @@ describe('node-fetch', () => { }); }); - it('should allow cloning a json response and log it as text response', function() { + it('should allow cloning a json response and log it as text response', () => { const url = `${base}json`; return fetch(url).then(res => { const r1 = res.clone(); @@ -1709,7 +1734,7 @@ describe('node-fetch', () => { }); }); - it('should allow cloning a json response, and then log it as text response', function() { + it('should allow cloning a json response, and then log it as text response', () => { const url = `${base}json`; return fetch(url).then(res => { const r1 = res.clone(); @@ -1722,7 +1747,7 @@ describe('node-fetch', () => { }); }); - it('should allow cloning a json response, first log as text response, then return json object', function() { + it('should allow cloning a json response, first log as text response, then return json object', () => { const url = `${base}json`; return fetch(url).then(res => { const r1 = res.clone(); @@ -1735,10 +1760,10 @@ describe('node-fetch', () => { }); }); - it('should not allow cloning a response after its been used', function() { + it('should not allow cloning a response after its been used', () => { const url = `${base}hello`; return fetch(url).then(res => - res.text().then(result => { + res.text().then(() => { expect(() => { res.clone(); }).to.throw(Error); @@ -1746,7 +1771,70 @@ describe('node-fetch', () => { ); }); - it('should allow get all responses of a header', function() { + it('should timeout on cloning response without consuming one of the streams when the second packet size is equal default highWaterMark', function () { + this.timeout(300); + const url = local.mockResponse(res => { + // Observed behavior of TCP packets splitting: + // - response body size <= 65438 → single packet sent + // - response body size > 65438 → multiple packets sent + // Max TCP packet size is 64kB (https://stackoverflow.com/a/2614188/5763764), + // but first packet probably transfers more than the response body. + const firstPacketMaxSize = 65438; + const secondPacketSize = 16 * 1024; // = defaultHighWaterMark + res.end(crypto.randomBytes(firstPacketMaxSize + secondPacketSize)); + }); + return expect( + fetch(url).then(res => res.clone().buffer()) + ).to.timeout; + }); + + it('should timeout on cloning response without consuming one of the streams when the second packet size is equal custom highWaterMark', function () { + this.timeout(300); + const url = local.mockResponse(res => { + const firstPacketMaxSize = 65438; + const secondPacketSize = 10; + res.end(crypto.randomBytes(firstPacketMaxSize + secondPacketSize)); + }); + return expect( + fetch(url, {highWaterMark: 10}).then(res => res.clone().buffer()) + ).to.timeout; + }); + + it('should not timeout on cloning response without consuming one of the streams when the second packet size is less than default highWaterMark', function () { + this.timeout(300); + const url = local.mockResponse(res => { + const firstPacketMaxSize = 65438; + const secondPacketSize = 16 * 1024; // = defaultHighWaterMark + res.end(crypto.randomBytes(firstPacketMaxSize + secondPacketSize - 1)); + }); + return expect( + fetch(url).then(res => res.clone().buffer()) + ).not.to.timeout; + }); + + it('should not timeout on cloning response without consuming one of the streams when the second packet size is less than custom highWaterMark', function () { + this.timeout(300); + const url = local.mockResponse(res => { + const firstPacketMaxSize = 65438; + const secondPacketSize = 10; + res.end(crypto.randomBytes(firstPacketMaxSize + secondPacketSize - 1)); + }); + return expect( + fetch(url, {highWaterMark: 10}).then(res => res.clone().buffer()) + ).not.to.timeout; + }); + + it('should not timeout on cloning response without consuming one of the streams when the response size is double the custom large highWaterMark - 1', function () { + this.timeout(300); + const url = local.mockResponse(res => { + res.end(crypto.randomBytes(2 * 512 * 1024 - 1)); + }); + return expect( + fetch(url, {highWaterMark: 512 * 1024}).then(res => res.clone().buffer()) + ).not.to.timeout; + }); + + it('should allow get all responses of a header', () => { const url = `${base}cookie`; return fetch(url).then(res => { const expected = 'a=1, b=1'; @@ -1755,7 +1843,7 @@ describe('node-fetch', () => { }); }); - it('should return all headers using raw()', function() { + it('should return all headers using raw()', () => { const url = `${base}cookie`; return fetch(url).then(res => { const expected = [ @@ -1767,7 +1855,7 @@ describe('node-fetch', () => { }); }); - it('should allow deleting header', function() { + it('should allow deleting header', () => { const url = `${base}cookie`; return fetch(url).then(res => { res.headers.delete('set-cookie'); @@ -1775,54 +1863,54 @@ describe('node-fetch', () => { }); }); - it('should send request with connection keep-alive if agent is provided', function() { + it('should send request with connection keep-alive if agent is provided', () => { const url = `${base}inspect`; - const opts = { + const options = { agent: new http.Agent({ keepAlive: true }) }; - return fetch(url, opts).then(res => { + return fetch(url, options).then(res => { return res.json(); }).then(res => { - expect(res.headers['connection']).to.equal('keep-alive'); + expect(res.headers.connection).to.equal('keep-alive'); }); }); - it('should support fetch with Request instance', function() { + it('should support fetch with Request instance', () => { const url = `${base}hello`; - const req = new Request(url); - return fetch(req).then(res => { + const request = new Request(url); + return fetch(request).then(res => { expect(res.url).to.equal(url); expect(res.ok).to.be.true; expect(res.status).to.equal(200); }); }); - it('should support fetch with Node.js URL object', function() { + it('should support fetch with Node.js URL object', () => { const url = `${base}hello`; - const urlObj = parseURL(url); - const req = new Request(urlObj); - return fetch(req).then(res => { + const urlObject = new URL(url); + const request = new Request(urlObject); + return fetch(request).then(res => { expect(res.url).to.equal(url); expect(res.ok).to.be.true; expect(res.status).to.equal(200); }); }); - it('should support fetch with WHATWG URL object', function() { + it('should support fetch with WHATWG URL object', () => { const url = `${base}hello`; - const urlObj = new URL(url); - const req = new Request(urlObj); - return fetch(req).then(res => { + const urlObject = new URL(url); + const request = new Request(urlObject); + return fetch(request).then(res => { expect(res.url).to.equal(url); expect(res.ok).to.be.true; expect(res.status).to.equal(200); }); }); - it('should support reading blob as text', function() { - return new Response(`hello`) + it('should support reading blob as text', () => { + return new Response('hello') .blob() .then(blob => blob.text()) .then(body => { @@ -1830,29 +1918,30 @@ describe('node-fetch', () => { }); }); - it('should support reading blob as arrayBuffer', function() { - return new Response(`hello`) + it('should support reading blob as arrayBuffer', () => { + return new Response('hello') .blob() .then(blob => blob.arrayBuffer()) .then(ab => { - const str = String.fromCharCode.apply(null, new Uint8Array(ab)); - expect(str).to.equal('hello'); + const string = String.fromCharCode.apply(null, new Uint8Array(ab)); + expect(string).to.equal('hello'); }); }); - it('should support reading blob as stream', function() { - return new Response(`hello`) + it('should support reading blob as stream', () => { + return new Response('hello') .blob() .then(blob => streamToPromise(blob.stream(), data => { - const str = data.toString(); - expect(str).to.equal('hello'); + const string = data.toString(); + expect(string).to.equal('hello'); })); }); - it('should support blob round-trip', function() { + it('should support blob round-trip', () => { const url = `${base}hello`; - let length, type; + let length; + let type; return fetch(url).then(res => res.blob()).then(blob => { const url = `${base}inspect`; @@ -1869,15 +1958,15 @@ describe('node-fetch', () => { }); }); - it('should support overwrite Request instance', function() { + it('should support overwrite Request instance', () => { const url = `${base}inspect`; - const req = new Request(url, { + const request = new Request(url, { method: 'POST', headers: { a: '1' } }); - return fetch(req, { + return fetch(request, { method: 'GET', headers: { a: '2' @@ -1890,7 +1979,7 @@ describe('node-fetch', () => { }); }); - it('should support arrayBuffer(), blob(), text(), json() and buffer() method in Body constructor', function() { + it('should support arrayBuffer(), blob(), text(), json() and buffer() method in Body constructor', () => { const body = new Body('a=1'); expect(body).to.have.property('arrayBuffer'); expect(body).to.have.property('blob'); @@ -1899,6 +1988,7 @@ describe('node-fetch', () => { expect(body).to.have.property('buffer'); }); + /* eslint-disable-next-line func-names */ it('should create custom FetchError', function funcName() { const systemError = new Error('system'); systemError.code = 'ESOMEERROR'; @@ -1911,30 +2001,35 @@ describe('node-fetch', () => { expect(err.type).to.equal('test-error'); expect(err.code).to.equal('ESOMEERROR'); expect(err.errno).to.equal('ESOMEERROR'); - // reading the stack is quite slow (~30-50ms) + // Reading the stack is quite slow (~30-50ms) expect(err.stack).to.include('funcName').and.to.startWith(`${err.name}: ${err.message}`); }); - it('should support https request', function() { + it('should support https request', function () { this.timeout(5000); const url = 'https://github.com/'; - const opts = { + const options = { method: 'HEAD' }; - return fetch(url, opts).then(res => { + return fetch(url, options).then(res => { expect(res.status).to.equal(200); expect(res.ok).to.be.true; }); }); - // issue #414 - it('should reject if attempt to accumulate body stream throws', function () { + // Issue #414 + it('should reject if attempt to accumulate body stream throws', () => { let body = resumer().queue('a=1').end(); body = body.pipe(new stream.PassThrough()); const res = new Response(body); const bufferConcat = Buffer.concat; - const restoreBufferConcat = () => Buffer.concat = bufferConcat; - Buffer.concat = () => { throw new Error('embedded error'); }; + const restoreBufferConcat = () => { + Buffer.concat = bufferConcat; + }; + + Buffer.concat = () => { + throw new Error('embedded error'); + }; const textPromise = res.text(); // Ensure that `Buffer.concat` is always restored: @@ -1942,41 +2037,43 @@ describe('node-fetch', () => { return expect(textPromise).to.eventually.be.rejected .and.be.an.instanceOf(FetchError) - .and.include({ type: 'system' }) + .and.include({type: 'system'}) .and.have.property('message').that.includes('Could not create Buffer') .and.that.includes('embedded error'); }); - it("supports supplying a lookup function to the agent", function() { + it('supports supplying a lookup function to the agent', () => { const url = `${base}redirect/301`; let called = 0; function lookupSpy(hostname, options, callback) { called++; return lookup(hostname, options, callback); } - const agent = http.Agent({ lookup: lookupSpy }); - return fetch(url, { agent }).then(() => { + + const agent = http.Agent({lookup: lookupSpy}); + return fetch(url, {agent}).then(() => { expect(called).to.equal(2); }); }); - it("supports supplying a famliy option to the agent", function() { + it('supports supplying a famliy option to the agent', () => { const url = `${base}redirect/301`; const families = []; const family = Symbol('family'); function lookupSpy(hostname, options, callback) { - families.push(options.family) + families.push(options.family); return lookup(hostname, {}, callback); } - const agent = http.Agent({ lookup: lookupSpy, family }); - return fetch(url, { agent }).then(() => { + + const agent = http.Agent({lookup: lookupSpy, family}); + return fetch(url, {agent}).then(() => { expect(families).to.have.length(2); expect(families[0]).to.equal(family); expect(families[1]).to.equal(family); }); }); - it('should allow a function supplying the agent', function() { + it('should allow a function supplying the agent', () => { const url = `${base}inspect`; const agent = new http.Agent({ @@ -1986,21 +2083,21 @@ describe('node-fetch', () => { let parsedURL; return fetch(url, { - agent: function(_parsedURL) { + agent(_parsedURL) { parsedURL = _parsedURL; return agent; } }).then(res => { return res.json(); }).then(res => { - // the agent provider should have been called + // The agent provider should have been called expect(parsedURL.protocol).to.equal('http:'); - // the agent we returned should have been used - expect(res.headers['connection']).to.equal('keep-alive'); + // The agent we returned should have been used + expect(res.headers.connection).to.equal('keep-alive'); }); }); - it('should calculate content length and extract content type for each body type', function () { + it('should calculate content length and extract content type for each body type', () => { const url = `${base}hello`; const bodyContent = 'a=1'; @@ -2012,14 +2109,14 @@ describe('node-fetch', () => { size: 1024 }); - let blobBody = new Blob([bodyContent], { type: 'text/plain' }); + const blobBody = new Blob([bodyContent], {type: 'text/plain'}); const blobRequest = new Request(url, { method: 'POST', body: blobBody, size: 1024 }); - let formBody = new FormData(); + const formBody = new FormData(); formBody.append('a', '1'); const formRequest = new Request(url, { method: 'POST', @@ -2027,7 +2124,7 @@ describe('node-fetch', () => { size: 1024 }); - let bufferBody = Buffer.from(bodyContent); + const bufferBody = Buffer.from(bodyContent); const bufferRequest = new Request(url, { method: 'POST', body: bufferBody, @@ -2060,800 +2157,22 @@ describe('node-fetch', () => { expect(extractContentType(bodyContent)).to.equal('text/plain;charset=UTF-8'); expect(extractContentType(null)).to.be.null; }); -}); -describe('Headers', function () { - it('should have attributes conforming to Web IDL', function () { - const headers = new Headers(); - expect(Object.getOwnPropertyNames(headers)).to.be.empty; - const enumerableProperties = []; - for (const property in headers) { - enumerableProperties.push(property); - } - for (const toCheck of [ - 'append', 'delete', 'entries', 'forEach', 'get', 'has', 'keys', 'set', - 'values' - ]) { - expect(enumerableProperties).to.contain(toCheck); - } + it('should encode URLs as UTF-8', () => { + const url = `${base}möbius`; + + fetch(url).then(res => expect(res.url).to.equal(`${base}m%C3%B6bius`)); }); - it('should allow iterating through all headers with forEach', function() { - const headers = new Headers([ - ['b', '2'], - ['c', '4'], - ['b', '3'], - ['a', '1'] - ]); - expect(headers).to.have.property('forEach'); - - const result = []; - headers.forEach((val, key) => { - result.push([key, val]); - }); - - expect(result).to.deep.equal([ - ["a", "1"], - ["b", "2, 3"], - ["c", "4"] - ]); - }); - - it('should allow iterating through all headers with for-of loop', function() { - const headers = new Headers([ - ['b', '2'], - ['c', '4'], - ['a', '1'] - ]); - headers.append('b', '3'); - expect(headers).to.be.iterable; - - const result = []; - for (let pair of headers) { - result.push(pair); - } - expect(result).to.deep.equal([ - ['a', '1'], - ['b', '2, 3'], - ['c', '4'] - ]); - }); - - it('should allow iterating through all headers with entries()', function() { - const headers = new Headers([ - ['b', '2'], - ['c', '4'], - ['a', '1'] - ]); - headers.append('b', '3'); - - expect(headers.entries()).to.be.iterable - .and.to.deep.iterate.over([ - ['a', '1'], - ['b', '2, 3'], - ['c', '4'] - ]); - }); - - it('should allow iterating through all headers with keys()', function() { - const headers = new Headers([ - ['b', '2'], - ['c', '4'], - ['a', '1'] - ]); - headers.append('b', '3'); - - expect(headers.keys()).to.be.iterable - .and.to.iterate.over(['a', 'b', 'c']); - }); - - it('should allow iterating through all headers with values()', function() { - const headers = new Headers([ - ['b', '2'], - ['c', '4'], - ['a', '1'] - ]); - headers.append('b', '3'); - - expect(headers.values()).to.be.iterable - .and.to.iterate.over(['1', '2, 3', '4']); - }); - - it('should reject illegal header', function() { - const headers = new Headers(); - expect(() => new Headers({ 'He y': 'ok' })).to.throw(TypeError); - expect(() => new Headers({ 'Hé-y': 'ok' })).to.throw(TypeError); - expect(() => new Headers({ 'He-y': 'ăk' })).to.throw(TypeError); - expect(() => headers.append('Hé-y', 'ok')) .to.throw(TypeError); - expect(() => headers.delete('Hé-y')) .to.throw(TypeError); - expect(() => headers.get('Hé-y')) .to.throw(TypeError); - expect(() => headers.has('Hé-y')) .to.throw(TypeError); - expect(() => headers.set('Hé-y', 'ok')) .to.throw(TypeError); - // should reject empty header - expect(() => headers.append('', 'ok')) .to.throw(TypeError); - - // 'o k' is valid value but invalid name - new Headers({ 'He-y': 'o k' }); - }); - - it('should ignore unsupported attributes while reading headers', function() { - const FakeHeader = function () {}; - // prototypes are currently ignored - // This might change in the future: #181 - FakeHeader.prototype.z = 'fake'; - - const res = new FakeHeader; - res.a = 'string'; - res.b = ['1','2']; - res.c = ''; - res.d = []; - res.e = 1; - res.f = [1, 2]; - res.g = { a:1 }; - res.h = undefined; - res.i = null; - res.j = NaN; - res.k = true; - res.l = false; - res.m = Buffer.from('test'); - - const h1 = new Headers(res); - h1.set('n', [1, 2]); - h1.append('n', ['3', 4]) - - const h1Raw = h1.raw(); - - expect(h1Raw['a']).to.include('string'); - expect(h1Raw['b']).to.include('1,2'); - expect(h1Raw['c']).to.include(''); - expect(h1Raw['d']).to.include(''); - expect(h1Raw['e']).to.include('1'); - expect(h1Raw['f']).to.include('1,2'); - expect(h1Raw['g']).to.include('[object Object]'); - expect(h1Raw['h']).to.include('undefined'); - expect(h1Raw['i']).to.include('null'); - expect(h1Raw['j']).to.include('NaN'); - expect(h1Raw['k']).to.include('true'); - expect(h1Raw['l']).to.include('false'); - expect(h1Raw['m']).to.include('test'); - expect(h1Raw['n']).to.include('1,2'); - expect(h1Raw['n']).to.include('3,4'); - - expect(h1Raw['z']).to.be.undefined; - }); - - it('should wrap headers', function() { - const h1 = new Headers({ - a: '1' - }); - const h1Raw = h1.raw(); - - const h2 = new Headers(h1); - h2.set('b', '1'); - const h2Raw = h2.raw(); - - const h3 = new Headers(h2); - h3.append('a', '2'); - const h3Raw = h3.raw(); - - expect(h1Raw['a']).to.include('1'); - expect(h1Raw['a']).to.not.include('2'); - - expect(h2Raw['a']).to.include('1'); - expect(h2Raw['a']).to.not.include('2'); - expect(h2Raw['b']).to.include('1'); - - expect(h3Raw['a']).to.include('1'); - expect(h3Raw['a']).to.include('2'); - expect(h3Raw['b']).to.include('1'); - }); - - it('should accept headers as an iterable of tuples', function() { - let headers; - - headers = new Headers([ - ['a', '1'], - ['b', '2'], - ['a', '3'] - ]); - expect(headers.get('a')).to.equal('1, 3'); - expect(headers.get('b')).to.equal('2'); - - headers = new Headers([ - new Set(['a', '1']), - ['b', '2'], - new Map([['a', null], ['3', null]]).keys() - ]); - expect(headers.get('a')).to.equal('1, 3'); - expect(headers.get('b')).to.equal('2'); - - headers = new Headers(new Map([ - ['a', '1'], - ['b', '2'] - ])); - expect(headers.get('a')).to.equal('1'); - expect(headers.get('b')).to.equal('2'); - }); - - it('should throw a TypeError if non-tuple exists in a headers initializer', function() { - expect(() => new Headers([ ['b', '2', 'huh?'] ])).to.throw(TypeError); - expect(() => new Headers([ 'b2' ])).to.throw(TypeError); - expect(() => new Headers('b2')).to.throw(TypeError); - expect(() => new Headers({ [Symbol.iterator]: 42 })).to.throw(TypeError); - }); -}); - -describe('Response', function () { - it('should have attributes conforming to Web IDL', function () { - const res = new Response(); - const enumerableProperties = []; - for (const property in res) { - enumerableProperties.push(property); - } - for (const toCheck of [ - 'body', 'bodyUsed', 'arrayBuffer', 'blob', 'json', 'text', - 'url', 'status', 'ok', 'redirected', 'statusText', 'headers', 'clone' - ]) { - expect(enumerableProperties).to.contain(toCheck); - } - for (const toCheck of [ - 'body', 'bodyUsed', 'url', 'status', 'ok', 'redirected', 'statusText', - 'headers' - ]) { - expect(() => { - res[toCheck] = 'abc'; - }).to.throw(); - } - }); - - it('should support empty options', function() { - let body = resumer().queue('a=1').end(); - body = body.pipe(new stream.PassThrough()); - const res = new Response(body); - return res.text().then(result => { - expect(result).to.equal('a=1'); - }); - }); - - it('should support parsing headers', function() { - const res = new Response(null, { - headers: { - a: '1' - } - }); - expect(res.headers.get('a')).to.equal('1'); - }); - - it('should support text() method', function() { - const res = new Response('a=1'); - return res.text().then(result => { - expect(result).to.equal('a=1'); - }); - }); - - it('should support json() method', function() { - const res = new Response('{"a":1}'); - return res.json().then(result => { - expect(result.a).to.equal(1); - }); - }); - - it('should support buffer() method', function() { - const res = new Response('a=1'); - return res.buffer().then(result => { - expect(result.toString()).to.equal('a=1'); - }); - }); - - it('should support blob() method', function() { - const res = new Response('a=1', { - method: 'POST', - headers: { - 'Content-Type': 'text/plain' - } - }); - return res.blob().then(function(result) { - expect(result).to.be.an.instanceOf(Blob); - expect(result.size).to.equal(3); - expect(result.type).to.equal('text/plain'); - }); - }); - - it('should support clone() method', function() { - let body = resumer().queue('a=1').end(); - body = body.pipe(new stream.PassThrough()); - const res = new Response(body, { - headers: { - a: '1' - }, - url: base, - status: 346, - statusText: 'production' - }); - const 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(result => { - expect(result).to.equal('a=1'); - }); - }); - - it('should support stream as body', function() { - let body = resumer().queue('a=1').end(); - body = body.pipe(new stream.PassThrough()); - const res = new Response(body); - return res.text().then(result => { - expect(result).to.equal('a=1'); - }); - }); - - it('should support string as body', function() { - const res = new Response('a=1'); - return res.text().then(result => { - expect(result).to.equal('a=1'); - }); - }); - - it('should support buffer as body', function() { - const res = new Response(Buffer.from('a=1')); - return res.text().then(result => { - expect(result).to.equal('a=1'); - }); - }); - - it('should support ArrayBuffer as body', function() { - const res = new Response(stringToArrayBuffer('a=1')); - return res.text().then(result => { - expect(result).to.equal('a=1'); - }); - }); - - it('should support blob as body', function() { - const res = new Response(new Blob(['a=1'])); - return res.text().then(result => { - expect(result).to.equal('a=1'); - }); - }); - - it('should support Uint8Array as body', function() { - const res = new Response(new Uint8Array(stringToArrayBuffer('a=1'))); - return res.text().then(result => { - expect(result).to.equal('a=1'); - }); - }); - - it('should support DataView as body', function() { - const res = new Response(new DataView(stringToArrayBuffer('a=1'))); - return res.text().then(result => { - expect(result).to.equal('a=1'); - }); - }); - - it('should default to null as body', function() { - const res = new Response(); - expect(res.body).to.equal(null); - - return res.text().then(result => expect(result).to.equal('')); - }); - - it('should default to 200 as status code', function() { - const res = new Response(null); - expect(res.status).to.equal(200); - }); - - it('should default to empty string as url', function() { - const res = new Response(); - expect(res.url).to.equal(''); - }); -}); - -describe('Request', function () { - it('should have attributes conforming to Web IDL', function () { - const req = new Request('https://github.com/'); - const enumerableProperties = []; - for (const property in req) { - enumerableProperties.push(property); - } - for (const toCheck of [ - 'body', 'bodyUsed', 'arrayBuffer', 'blob', 'json', 'text', - 'method', 'url', 'headers', 'redirect', 'clone', 'signal', - ]) { - expect(enumerableProperties).to.contain(toCheck); - } - for (const toCheck of [ - 'body', 'bodyUsed', 'method', 'url', 'headers', 'redirect', 'signal', - ]) { - expect(() => { - req[toCheck] = 'abc'; - }).to.throw(); - } - }); - - it('should support wrapping Request instance', function() { - const url = `${base}hello`; - - const form = new FormData(); - form.append('a', '1'); - const { signal } = new AbortController(); - - const r1 = new Request(url, { - method: 'POST', - follow: 1, - body: form, - signal, - }); - const r2 = new Request(r1, { - follow: 2 - }); - - expect(r2.url).to.equal(url); - expect(r2.method).to.equal('POST'); - expect(r2.signal).to.equal(signal); - // 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 override signal on derived Request instances', function() { - const parentAbortController = new AbortController(); - const derivedAbortController = new AbortController(); - const parentRequest = new Request(`test`, { - signal: parentAbortController.signal - }); - const derivedRequest = new Request(parentRequest, { - signal: derivedAbortController.signal - }); - expect(parentRequest.signal).to.equal(parentAbortController.signal); - expect(derivedRequest.signal).to.equal(derivedAbortController.signal); - }); - - it('should allow removing signal on derived Request instances', function() { - const parentAbortController = new AbortController(); - const parentRequest = new Request(`test`, { - signal: parentAbortController.signal - }); - const derivedRequest = new Request(parentRequest, { - signal: null - }); - expect(parentRequest.signal).to.equal(parentAbortController.signal); - expect(derivedRequest.signal).to.equal(null); - }); - - it('should throw error with GET/HEAD requests with body', function() { - expect(() => new Request('.', { body: '' })) - .to.throw(TypeError); - expect(() => new Request('.', { body: 'a' })) - .to.throw(TypeError); - expect(() => new Request('.', { body: '', method: 'HEAD' })) - .to.throw(TypeError); - expect(() => new Request('.', { body: 'a', method: 'HEAD' })) - .to.throw(TypeError); - expect(() => new Request('.', { body: 'a', method: 'get' })) - .to.throw(TypeError); - expect(() => new Request('.', { body: 'a', method: 'head' })) - .to.throw(TypeError); - }); - - it('should default to null as body', function() { - const req = new Request('.'); - expect(req.body).to.equal(null); - return req.text().then(result => expect(result).to.equal('')); - }); - - it('should support parsing headers', function() { - const url = base; - const req = new Request(url, { - headers: { - a: '1' - } - }); - expect(req.url).to.equal(url); - expect(req.headers.get('a')).to.equal('1'); - }); - - it('should support arrayBuffer() method', function() { - const url = base; - var req = new Request(url, { - method: 'POST', - body: 'a=1' - }); - expect(req.url).to.equal(url); - return req.arrayBuffer().then(function(result) { - expect(result).to.be.an.instanceOf(ArrayBuffer); - const str = String.fromCharCode.apply(null, new Uint8Array(result)); - expect(str).to.equal('a=1'); - }); - }); - - it('should support text() method', function() { - const url = base; - const req = new Request(url, { - method: 'POST', - body: 'a=1' - }); - expect(req.url).to.equal(url); - return req.text().then(result => { - expect(result).to.equal('a=1'); - }); - }); - - it('should support json() method', function() { - const url = base; - const req = new Request(url, { - method: 'POST', - body: '{"a":1}' - }); - expect(req.url).to.equal(url); - return req.json().then(result => { - expect(result.a).to.equal(1); - }); - }); - - it('should support buffer() method', function() { - const url = base; - const req = new Request(url, { - method: 'POST', - body: 'a=1' - }); - expect(req.url).to.equal(url); - return req.buffer().then(result => { - expect(result.toString()).to.equal('a=1'); - }); - }); - - it('should support blob() method', function() { - const url = base; - var req = new Request(url, { - method: 'POST', - body: Buffer.from('a=1') - }); - expect(req.url).to.equal(url); - return req.blob().then(function(result) { - expect(result).to.be.an.instanceOf(Blob); - expect(result.size).to.equal(3); - expect(result.type).to.equal(''); - }); - }); - - it('should support arbitrary url', function() { - const url = 'anything'; - const req = new Request(url); - expect(req.url).to.equal('anything'); - }); - - it('should support clone() method', function() { - const url = base; - let body = resumer().queue('a=1').end(); - body = body.pipe(new stream.PassThrough()); - const agent = new http.Agent(); - const { signal } = new AbortController(); - const req = new Request(url, { - body, - method: 'POST', - redirect: 'manual', - headers: { - b: '2' - }, - follow: 3, - compress: false, - agent, - signal, - }); - const 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); - expect(cl.signal).to.equal(signal); - // clone body shouldn't be the same body - expect(cl.body).to.not.equal(body); - return Promise.all([cl.text(), req.text()]).then(results => { - expect(results[0]).to.equal('a=1'); - expect(results[1]).to.equal('a=1'); - }); - }); - - it('should support ArrayBuffer as body', function() { - const req = new Request('', { - method: 'POST', - body: stringToArrayBuffer('a=1') - }); - return req.text().then(result => { - expect(result).to.equal('a=1'); - }); - }); - - it('should support Uint8Array as body', function() { - const req = new Request('', { - method: 'POST', - body: new Uint8Array(stringToArrayBuffer('a=1')) - }); - return req.text().then(result => { - expect(result).to.equal('a=1'); - }); - }); - - it('should support DataView as body', function() { - const req = new Request('', { - method: 'POST', - body: new DataView(stringToArrayBuffer('a=1')) - }); - return req.text().then(result => { - expect(result).to.equal('a=1'); - }); - }); -}); - -function streamToPromise(stream, dataHandler) { - return new Promise((resolve, reject) => { - stream.on('data', (...args) => { - Promise.resolve() - .then(() => dataHandler(...args)) - .catch(reject); - }); - stream.on('end', resolve); - stream.on('error', reject); - }); -} - -describe('external encoding', () => { - const hasEncoding = typeof convert === 'function'; - - describe('with optional `encoding`', function() { - before(function() { - if(!hasEncoding) this.skip(); - }); - - it('should only use UTF-8 decoding with text()', function() { - const url = `${base}encoding/euc-jp`; - return fetch(url).then(res => { - expect(res.status).to.equal(200); - return res.text().then(result => { - expect(result).to.equal('\ufffd\ufffd\ufffd\u0738\ufffd'); - }); - }); - }); - - it('should support encoding decode, xml dtd detect', function() { - const url = `${base}encoding/euc-jp`; - return fetch(url).then(res => { - expect(res.status).to.equal(200); - return res.textConverted().then(result => { - expect(result).to.equal('日本語'); - }); - }); - }); - - it('should support encoding decode, content-type detect', function() { - const url = `${base}encoding/shift-jis`; - return fetch(url).then(res => { - expect(res.status).to.equal(200); - return res.textConverted().then(result => { - expect(result).to.equal('
日本語
'); - }); - }); - }); - - it('should support encoding decode, html5 detect', function() { - const url = `${base}encoding/gbk`; - return fetch(url).then(res => { - expect(res.status).to.equal(200); - return res.textConverted().then(result => { - expect(result).to.equal('
中文
'); - }); - }); - }); - - it('should support encoding decode, html4 detect', function() { - const url = `${base}encoding/gb2312`; - return fetch(url).then(res => { - expect(res.status).to.equal(200); - return res.textConverted().then(result => { - expect(result).to.equal('
中文
'); - }); - }); - }); - - it('should support encoding decode, html4 detect reverse http-equiv', function() { - const url = `${base}encoding/gb2312-reverse`; - return fetch(url).then(res => { - expect(res.status).to.equal(200); - return res.textConverted().then(result => { - expect(result).to.equal('
中文
'); - }); - }); - }); - - it('should default to utf8 encoding', function() { - const url = `${base}encoding/utf8`; - return fetch(url).then(res => { - expect(res.status).to.equal(200); - expect(res.headers.get('content-type')).to.be.null; - return res.textConverted().then(result => { - expect(result).to.equal('中文'); - }); - }); - }); - - it('should support uncommon content-type order, charset in front', function() { - const url = `${base}encoding/order1`; - return fetch(url).then(res => { - expect(res.status).to.equal(200); - return res.textConverted().then(result => { - expect(result).to.equal('中文'); - }); - }); - }); - - it('should support uncommon content-type order, end with qs', function() { - const url = `${base}encoding/order2`; - return fetch(url).then(res => { - expect(res.status).to.equal(200); - return res.textConverted().then(result => { - expect(result).to.equal('中文'); - }); - }); - }); - - it('should support chunked encoding, html4 detect', function() { - const url = `${base}encoding/chunked`; - return fetch(url).then(res => { - expect(res.status).to.equal(200); - const padding = 'a'.repeat(10); - return res.textConverted().then(result => { - expect(result).to.equal(`${padding}
日本語
`); - }); - }); - }); - - it('should only do encoding detection up to 1024 bytes', function() { - const url = `${base}encoding/invalid`; - return fetch(url).then(res => { - expect(res.status).to.equal(200); - const padding = 'a'.repeat(1200); - return res.textConverted().then(result => { - expect(result).to.not.equal(`${padding}中文`); - }); - }); - }); - }); - - describe('without optional `encoding`', function() { - before(function() { - if (hasEncoding) this.skip() - }); - - it('should throw a FetchError if res.textConverted() is called without `encoding` in require cache', () => { - const url = `${base}hello`; - return fetch(url).then((res) => { - return expect(res.textConverted()).to.eventually.be.rejected - .and.have.property('message').which.includes('encoding') - }); - }); - }); - - describe('data uri', function() { + describe('data uri', () => { const dataUrl = 'data:image/gif;base64,R0lGODlhAQABAIAAAAUEBAAAACwAAAAAAQABAAACAkQBADs='; const invalidDataUrl = 'data:@@@@'; - it('should accept data uri', function() { + it('should accept data uri', () => { return fetch(dataUrl).then(r => { - console.assert(r.status == 200); - console.assert(r.headers.get('Content-Type') == 'image/gif'); + console.assert(r.status === 200); + console.assert(r.headers.get('Content-Type') === 'image/gif'); return r.buffer().then(b => { console.assert(b instanceof Buffer); @@ -2861,11 +2180,10 @@ describe('external encoding', () => { }); }); - it('should reject invalid data uri', function() { - return fetch(invalidDataUrl) - .catch(e => { - console.assert(e); - console.assert(e.message.includes('invalid URL')); + it('should reject invalid data uri', () => { + return fetch(invalidDataUrl).catch(error => { + console.assert(error); + console.assert(error.message.includes('invalid URL')); }); }); }); diff --git a/test/request.js b/test/request.js new file mode 100644 index 0000000..c83461b --- /dev/null +++ b/test/request.js @@ -0,0 +1,266 @@ +import * as stream from 'stream'; +import * as http from 'http'; +import {Request} from '../src'; +import TestServer from './utils/server'; +import {AbortController} from 'abortcontroller-polyfill/dist/abortcontroller'; +import chai from 'chai'; +import FormData from 'form-data'; +import Blob from 'fetch-blob'; +import resumer from 'resumer'; +import stringToArrayBuffer from 'string-to-arraybuffer'; + +const {expect} = chai; + +const local = new TestServer(); +const base = `http://${local.hostname}:${local.port}/`; + +describe('Request', () => { + it('should have attributes conforming to Web IDL', () => { + const request = new Request('https://github.com/'); + const enumerableProperties = []; + for (const property in request) { + enumerableProperties.push(property); + } + + for (const toCheck of [ + 'body', + 'bodyUsed', + 'arrayBuffer', + 'blob', + 'json', + 'text', + 'method', + 'url', + 'headers', + 'redirect', + 'clone', + 'signal' + ]) { + expect(enumerableProperties).to.contain(toCheck); + } + + for (const toCheck of [ + 'body', 'bodyUsed', 'method', 'url', 'headers', 'redirect', 'signal' + ]) { + expect(() => { + request[toCheck] = 'abc'; + }).to.throw(); + } + }); + + it('should support wrapping Request instance', () => { + const url = `${base}hello`; + + const form = new FormData(); + form.append('a', '1'); + const {signal} = new AbortController(); + + const r1 = new Request(url, { + method: 'POST', + follow: 1, + body: form, + signal + }); + const r2 = new Request(r1, { + follow: 2 + }); + + expect(r2.url).to.equal(url); + expect(r2.method).to.equal('POST'); + expect(r2.signal).to.equal(signal); + // 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 override signal on derived Request instances', () => { + const parentAbortController = new AbortController(); + const derivedAbortController = new AbortController(); + const parentRequest = new Request(`${base}hello`, { + signal: parentAbortController.signal + }); + const derivedRequest = new Request(parentRequest, { + signal: derivedAbortController.signal + }); + expect(parentRequest.signal).to.equal(parentAbortController.signal); + expect(derivedRequest.signal).to.equal(derivedAbortController.signal); + }); + + it('should allow removing signal on derived Request instances', () => { + const parentAbortController = new AbortController(); + const parentRequest = new Request(`${base}hello`, { + signal: parentAbortController.signal + }); + const derivedRequest = new Request(parentRequest, { + signal: null + }); + expect(parentRequest.signal).to.equal(parentAbortController.signal); + expect(derivedRequest.signal).to.equal(null); + }); + + it('should throw error with GET/HEAD requests with body', () => { + expect(() => new Request('.', {body: ''})) + .to.throw(TypeError); + expect(() => new Request('.', {body: 'a'})) + .to.throw(TypeError); + expect(() => new Request('.', {body: '', method: 'HEAD'})) + .to.throw(TypeError); + expect(() => new Request('.', {body: 'a', method: 'HEAD'})) + .to.throw(TypeError); + expect(() => new Request('.', {body: 'a', method: 'get'})) + .to.throw(TypeError); + expect(() => new Request('.', {body: 'a', method: 'head'})) + .to.throw(TypeError); + }); + + it('should default to null as body', () => { + const request = new Request(base); + expect(request.body).to.equal(null); + return request.text().then(result => expect(result).to.equal('')); + }); + + it('should support parsing headers', () => { + const url = base; + const request = new Request(url, { + headers: { + a: '1' + } + }); + expect(request.url).to.equal(url); + expect(request.headers.get('a')).to.equal('1'); + }); + + it('should support arrayBuffer() method', () => { + const url = base; + const request = new Request(url, { + method: 'POST', + body: 'a=1' + }); + expect(request.url).to.equal(url); + return request.arrayBuffer().then(result => { + expect(result).to.be.an.instanceOf(ArrayBuffer); + const string = String.fromCharCode.apply(null, new Uint8Array(result)); + expect(string).to.equal('a=1'); + }); + }); + + it('should support text() method', () => { + const url = base; + const request = new Request(url, { + method: 'POST', + body: 'a=1' + }); + expect(request.url).to.equal(url); + return request.text().then(result => { + expect(result).to.equal('a=1'); + }); + }); + + it('should support json() method', () => { + const url = base; + const request = new Request(url, { + method: 'POST', + body: '{"a":1}' + }); + expect(request.url).to.equal(url); + return request.json().then(result => { + expect(result.a).to.equal(1); + }); + }); + + it('should support buffer() method', () => { + const url = base; + const request = new Request(url, { + method: 'POST', + body: 'a=1' + }); + expect(request.url).to.equal(url); + return request.buffer().then(result => { + expect(result.toString()).to.equal('a=1'); + }); + }); + + it('should support blob() method', () => { + const url = base; + const request = new Request(url, { + method: 'POST', + body: Buffer.from('a=1') + }); + expect(request.url).to.equal(url); + return request.blob().then(result => { + expect(result).to.be.an.instanceOf(Blob); + expect(result.size).to.equal(3); + expect(result.type).to.equal(''); + }); + }); + + it('should support clone() method', () => { + const url = base; + let body = resumer().queue('a=1').end(); + body = body.pipe(new stream.PassThrough()); + const agent = new http.Agent(); + const {signal} = new AbortController(); + const request = new Request(url, { + body, + method: 'POST', + redirect: 'manual', + headers: { + b: '2' + }, + follow: 3, + compress: false, + agent, + signal + }); + const cl = request.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); + expect(cl.signal).to.equal(signal); + // Clone body shouldn't be the same body + expect(cl.body).to.not.equal(body); + return Promise.all([cl.text(), request.text()]).then(results => { + expect(results[0]).to.equal('a=1'); + expect(results[1]).to.equal('a=1'); + }); + }); + + it('should support ArrayBuffer as body', () => { + const request = new Request(base, { + method: 'POST', + body: stringToArrayBuffer('a=1') + }); + return request.text().then(result => { + expect(result).to.equal('a=1'); + }); + }); + + it('should support Uint8Array as body', () => { + const request = new Request(base, { + method: 'POST', + body: new Uint8Array(stringToArrayBuffer('a=1')) + }); + return request.text().then(result => { + expect(result).to.equal('a=1'); + }); + }); + + it('should support DataView as body', () => { + const request = new Request(base, { + method: 'POST', + body: new DataView(stringToArrayBuffer('a=1')) + }); + return request.text().then(result => { + expect(result).to.equal('a=1'); + }); + }); +}); diff --git a/test/response.js b/test/response.js new file mode 100644 index 0000000..35a8090 --- /dev/null +++ b/test/response.js @@ -0,0 +1,200 @@ +import * as stream from 'stream'; +import {Response} from '../src'; +import TestServer from './utils/server'; +import chai from 'chai'; +import resumer from 'resumer'; +import stringToArrayBuffer from 'string-to-arraybuffer'; +import Blob from 'fetch-blob'; + +const {expect} = chai; + +const local = new TestServer(); +const base = `http://${local.hostname}:${local.port}/`; + +describe('Response', () => { + it('should have attributes conforming to Web IDL', () => { + const res = new Response(); + const enumerableProperties = []; + for (const property in res) { + enumerableProperties.push(property); + } + + for (const toCheck of [ + 'body', + 'bodyUsed', + 'arrayBuffer', + 'blob', + 'json', + 'text', + 'url', + 'status', + 'ok', + 'redirected', + 'statusText', + 'headers', + 'clone' + ]) { + expect(enumerableProperties).to.contain(toCheck); + } + + for (const toCheck of [ + 'body', + 'bodyUsed', + 'url', + 'status', + 'ok', + 'redirected', + 'statusText', + 'headers' + ]) { + expect(() => { + res[toCheck] = 'abc'; + }).to.throw(); + } + }); + + it('should support empty options', () => { + let body = resumer().queue('a=1').end(); + body = body.pipe(new stream.PassThrough()); + const res = new Response(body); + return res.text().then(result => { + expect(result).to.equal('a=1'); + }); + }); + + it('should support parsing headers', () => { + const res = new Response(null, { + headers: { + a: '1' + } + }); + expect(res.headers.get('a')).to.equal('1'); + }); + + it('should support text() method', () => { + const res = new Response('a=1'); + return res.text().then(result => { + expect(result).to.equal('a=1'); + }); + }); + + it('should support json() method', () => { + const res = new Response('{"a":1}'); + return res.json().then(result => { + expect(result.a).to.equal(1); + }); + }); + + it('should support buffer() method', () => { + const res = new Response('a=1'); + return res.buffer().then(result => { + expect(result.toString()).to.equal('a=1'); + }); + }); + + it('should support blob() method', () => { + const res = new Response('a=1', { + method: 'POST', + headers: { + 'Content-Type': 'text/plain' + } + }); + return res.blob().then(result => { + expect(result).to.be.an.instanceOf(Blob); + expect(result.size).to.equal(3); + expect(result.type).to.equal('text/plain'); + }); + }); + + it('should support clone() method', () => { + let body = resumer().queue('a=1').end(); + body = body.pipe(new stream.PassThrough()); + const res = new Response(body, { + headers: { + a: '1' + }, + url: base, + status: 346, + statusText: 'production' + }); + const 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(result => { + expect(result).to.equal('a=1'); + }); + }); + + it('should support stream as body', () => { + let body = resumer().queue('a=1').end(); + body = body.pipe(new stream.PassThrough()); + const res = new Response(body); + return res.text().then(result => { + expect(result).to.equal('a=1'); + }); + }); + + it('should support string as body', () => { + const res = new Response('a=1'); + return res.text().then(result => { + expect(result).to.equal('a=1'); + }); + }); + + it('should support buffer as body', () => { + const res = new Response(Buffer.from('a=1')); + return res.text().then(result => { + expect(result).to.equal('a=1'); + }); + }); + + it('should support ArrayBuffer as body', () => { + const res = new Response(stringToArrayBuffer('a=1')); + return res.text().then(result => { + expect(result).to.equal('a=1'); + }); + }); + + it('should support blob as body', () => { + const res = new Response(new Blob(['a=1'])); + return res.text().then(result => { + expect(result).to.equal('a=1'); + }); + }); + + it('should support Uint8Array as body', () => { + const res = new Response(new Uint8Array(stringToArrayBuffer('a=1'))); + return res.text().then(result => { + expect(result).to.equal('a=1'); + }); + }); + + it('should support DataView as body', () => { + const res = new Response(new DataView(stringToArrayBuffer('a=1'))); + return res.text().then(result => { + expect(result).to.equal('a=1'); + }); + }); + + it('should default to null as body', () => { + const res = new Response(); + expect(res.body).to.equal(null); + + return res.text().then(result => expect(result).to.equal('')); + }); + + it('should default to 200 as status code', () => { + const res = new Response(null); + expect(res.status).to.equal(200); + }); + + it('should default to empty string as url', () => { + const res = new Response(); + expect(res.url).to.equal(''); + }); +}); diff --git a/test/utils/chai-timeout.js b/test/utils/chai-timeout.js new file mode 100644 index 0000000..6fed2cf --- /dev/null +++ b/test/utils/chai-timeout.js @@ -0,0 +1,18 @@ +export default ({Assertion}, utils) => { + utils.addProperty(Assertion.prototype, 'timeout', function () { + return new Promise(resolve => { + const timer = setTimeout(() => resolve(true), 150); + this._obj.then(() => { + clearTimeout(timer); + resolve(false); + }); + }).then(timeouted => { + this.assert( + timeouted, + 'expected promise to timeout but it was resolved', + 'expected promise not to timeout but it timed out' + ); + }); + }); +}; + diff --git a/test/dummy.txt b/test/utils/dummy.txt similarity index 100% rename from test/dummy.txt rename to test/utils/dummy.txt diff --git a/test/server.js b/test/utils/server.js similarity index 64% rename from test/server.js rename to test/utils/server.js index 06c715d..14f5af4 100644 --- a/test/server.js +++ b/test/utils/server.js @@ -1,24 +1,19 @@ import * as http from 'http'; -import { parse } from 'url'; import * as zlib from 'zlib'; -import * as stream from 'stream'; -import { multipart as Multipart } from 'parted'; - -let convert; -try { convert = require('encoding').convert; } catch(e) {} +import {multipart as Multipart} from 'parted'; export default class TestServer { constructor() { this.server = http.createServer(this.router); this.port = 30001; this.hostname = 'localhost'; - // node 8 default keepalive timeout is 5000ms + // 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) { + this.server.on('error', err => { console.log(err.stack); }); - this.server.on('connection', function(socket) { + this.server.on('connection', socket => { socket.setTimeout(1500); }); } @@ -31,8 +26,22 @@ export default class TestServer { this.server.close(cb); } - router(req, res) { - let p = parse(req.url).pathname; + mockResponse(responseHandler) { + this.server.nextResponseHandler = responseHandler; + return `http://${this.hostname}:${this.port}/mocked`; + } + + router(request, res) { + const p = request.url; + + if (p === '/mocked') { + if (this.nextResponseHandler) { + this.nextResponseHandler(res); + this.nextResponseHandler = undefined; + } else { + throw new Error('No mocked response. Use ’TestServer.mockResponse()’.'); + } + } if (p === '/hello') { res.statusCode = 200; @@ -70,7 +79,11 @@ export default class TestServer { res.statusCode = 200; res.setHeader('Content-Type', 'text/plain'); res.setHeader('Content-Encoding', 'gzip'); - zlib.gzip('hello world', function(err, buffer) { + zlib.gzip('hello world', (err, buffer) => { + if (err) { + throw err; + } + res.end(buffer); }); } @@ -79,9 +92,26 @@ export default class TestServer { res.statusCode = 200; res.setHeader('Content-Type', 'text/plain'); res.setHeader('Content-Encoding', 'gzip'); - zlib.gzip('hello world', function(err, buffer) { - // truncate the CRC checksum and size check at the end of the stream - res.end(buffer.slice(0, buffer.length - 8)); + zlib.gzip('hello world', (err, buffer) => { + if (err) { + throw err; + } + + // Truncate the CRC checksum and size check at the end of the stream + res.end(buffer.slice(0, -8)); + }); + } + + if (p === '/gzip-capital') { + res.statusCode = 200; + res.setHeader('Content-Type', 'text/plain'); + res.setHeader('Content-Encoding', 'GZip'); + zlib.gzip('hello world', (err, buffer) => { + if (err) { + throw err; + } + + res.end(buffer); }); } @@ -89,7 +119,11 @@ export default class TestServer { res.statusCode = 200; res.setHeader('Content-Type', 'text/plain'); res.setHeader('Content-Encoding', 'deflate'); - zlib.deflate('hello world', function(err, buffer) { + zlib.deflate('hello world', (err, buffer) => { + if (err) { + throw err; + } + res.end(buffer); }); } @@ -99,18 +133,25 @@ export default class TestServer { res.setHeader('Content-Type', 'text/plain'); if (typeof zlib.createBrotliDecompress === 'function') { res.setHeader('Content-Encoding', 'br'); - zlib.brotliCompress('hello world', function (err, buffer) { + zlib.brotliCompress('hello world', (err, buffer) => { + if (err) { + throw err; + } + 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) { + zlib.deflateRaw('hello world', (err, buffer) => { + if (err) { + throw err; + } + res.end(buffer); }); } @@ -130,7 +171,7 @@ export default class TestServer { } if (p === '/timeout') { - setTimeout(function() { + setTimeout(() => { res.statusCode = 200; res.setHeader('Content-Type', 'text/plain'); res.end('text'); @@ -141,7 +182,7 @@ export default class TestServer { res.statusCode = 200; res.setHeader('Content-Type', 'text/plain'); res.write('test'); - setTimeout(function() { + setTimeout(() => { res.end('test'); }, 1000); } @@ -155,10 +196,10 @@ export default class TestServer { if (p === '/size/chunk') { res.statusCode = 200; res.setHeader('Content-Type', 'text/plain'); - setTimeout(function() { + setTimeout(() => { res.write('test'); }, 10); - setTimeout(function() { + setTimeout(() => { res.end('test'); }, 20); } @@ -169,69 +210,6 @@ export default class TestServer { 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/gb2312-reverse') { - 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'); - res.write('a'.repeat(10)); - res.end(convert('
日本語
', 'Shift_JIS')); - } - - if (p === '/encoding/invalid') { - res.statusCode = 200; - res.setHeader('Content-Type', 'text/html'); - res.setHeader('Transfer-Encoding', 'chunked'); - res.write('a'.repeat(1200)); - res.end(convert('中文', 'gbk')); - } - if (p === '/redirect/301') { res.statusCode = 301; res.setHeader('Location', '/inspect'); @@ -276,7 +254,7 @@ export default class TestServer { if (p === '/redirect/slow') { res.statusCode = 301; res.setHeader('Location', '/redirect/301'); - setTimeout(function() { + setTimeout(() => { res.end(); }, 1000); } @@ -284,7 +262,7 @@ export default class TestServer { if (p === '/redirect/slow-chain') { res.statusCode = 301; res.setHeader('Location', '/redirect/slow'); - setTimeout(function() { + setTimeout(() => { res.end(); }, 10); } @@ -355,12 +333,14 @@ export default class TestServer { res.statusCode = 200; res.setHeader('Content-Type', 'application/json'); let body = ''; - req.on('data', function(c) { body += c }); - req.on('end', function() { + request.on('data', c => { + body += c; + }); + request.on('end', () => { res.end(JSON.stringify({ - method: req.method, - url: req.url, - headers: req.headers, + method: request.method, + url: request.url, + headers: request.headers, body })); }); @@ -369,26 +349,32 @@ export default class TestServer { if (p === '/multipart') { res.statusCode = 200; res.setHeader('Content-Type', 'application/json'); - const parser = new Multipart(req.headers['content-type']); + const parser = new Multipart(request.headers['content-type']); let body = ''; - parser.on('part', function(field, part) { + parser.on('part', (field, part) => { body += field + '=' + part; }); - parser.on('end', function() { + parser.on('end', () => { res.end(JSON.stringify({ - method: req.method, - url: req.url, - headers: req.headers, - body: body + method: request.method, + url: request.url, + headers: request.headers, + body })); }); - req.pipe(parser); + request.pipe(parser); + } + + if (p === '/m%C3%B6bius') { + res.statusCode = 200; + res.setHeader('Content-Type', 'text/plain'); + res.end('ok'); } } } if (require.main === module) { - const server = new TestServer; + const server = new TestServer(); server.start(() => { console.log(`Server started listening at port ${server.port}`); });