pax_global_header00006660000000000000000000000064141710025350014510gustar00rootroot0000000000000052 comment=1ef4b560a17e644a02a3bfdea7631ffeee578b35 node-fetch-2.6.7/000077500000000000000000000000001417100253500135405ustar00rootroot00000000000000node-fetch-2.6.7/.babelrc000066400000000000000000000022141417100253500151320ustar00rootroot00000000000000{ 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 } ] ] } } } node-fetch-2.6.7/.github/000077500000000000000000000000001417100253500151005ustar00rootroot00000000000000node-fetch-2.6.7/.github/FUNDING.yml000066400000000000000000000012711417100253500167160ustar00rootroot00000000000000# These are supported funding model platforms github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] patreon: # Replace with a single Patreon username open_collective: node-fetch # Replace with a single Open Collective username ko_fi: # Replace with a single Ko-fi username tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry liberapay: # Replace with a single Liberapay username issuehunt: # Replace with a single IssueHunt username otechie: # Replace with a single Otechie username custom: # Replace with a single custom sponsorship URL node-fetch-2.6.7/.gitignore000066400000000000000000000013461417100253500155340ustar00rootroot00000000000000# Logs logs *.log # Runtime data pids *.pid *.seed # Directory for instrumented libs generated by jscoverage/JSCover lib-cov # Coverage directory used by tools like nyc and istanbul .nyc_output coverage # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) .grunt # Compiled binary addons (http://nodejs.org/api/addons.html) build/Release # Dependency directory # Commenting this out is preferred by some people, see # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git- node_modules # Users Environment Variables .lock-wscript # OS files .DS_Store # Babel-compiled files lib # Ignore package manager lock files package-lock.json yarn.lock # Ignore IDE .idea node-fetch-2.6.7/.npmrc000066400000000000000000000000231417100253500146530ustar00rootroot00000000000000package-lock=false node-fetch-2.6.7/.nycrc000066400000000000000000000001311417100253500146520ustar00rootroot00000000000000{ "require": [ "babel-register" ], "sourceMap": false, "instrument": false } node-fetch-2.6.7/.travis.yml000066400000000000000000000005641417100253500156560ustar00rootroot00000000000000language: 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' script: - npm uninstall encoding - npm run coverage - npm install encoding - npm run coverage cache: directories: - node_modules node-fetch-2.6.7/CHANGELOG.md000066400000000000000000000244671417100253500153660ustar00rootroot00000000000000 Changelog ========= # 2.x release ## v2.6.5 - Fix: import `whatwg-url` in a way compatible with ESM ## v2.6.4 - Hotfix: fix v2.6.3 that did not sending query params ## v2.6.3 - Fix: properly encode url with unicode characters ## v2.6.2 - Fix: used full filename for main in package.json - Other: pinned codecov & teeny-request (had one breaking change with spread operators) ## v2.6.1 **This is an important security release. It is strongly recommended to update as soon as possible.** - Fix: honor the `size` option after following a redirect. ## v2.6.0 - Enhance: `options.agent`, it now accepts a function that returns custom http(s).Agent instance based on current URL, see readme for more information. - Fix: incorrect `Content-Length` was returned for stream body in 2.5.0 release; note that `node-fetch` doesn't calculate content length for stream body. - Fix: `Response.url` should return empty string instead of `null` by default. ## v2.5.0 - Enhance: `Response` object now includes `redirected` property. - Enhance: `fetch()` now accepts third-party `Blob` implementation as body. - Other: disable `package-lock.json` generation as we never commit them. - Other: dev dependency update. - Other: readme update. ## v2.4.1 - Fix: `Blob` import rule for node < 10, as `Readable` isn't a named export. ## v2.4.0 - Enhance: added `Brotli` compression support (using node's zlib). - Enhance: updated `Blob` implementation per spec. - Fix: set content type automatically for `URLSearchParams`. - Fix: `Headers` now reject empty header names. - Fix: test cases, as node 12+ no longer accepts invalid header response. ## v2.3.0 - Enhance: added `AbortSignal` support, with README example. - Enhance: handle invalid `Location` header during redirect by rejecting them explicitly with `FetchError`. - Fix: update `browser.js` to support react-native environment, where `self` isn't available globally. ## 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. - Other: Better README. ## v2.2.0 - Enhance: Support all `ArrayBuffer` view types - Enhance: Support Web Workers - Enhance: Support Node.js' `--experimental-modules` mode; deprecate `.es.js` file - Fix: Add `__esModule` property to the exports object - Other: Better example in README for writing response to a file - Other: More tests for Agent ## v2.1.2 - Fix: allow `Body` methods to work on `ArrayBuffer`-backed `Body` objects - Fix: reject promise returned by `Body` methods when the accumulated `Buffer` exceeds the maximum size - Fix: support custom `Host` headers with any casing - Fix: support importing `fetch()` from TypeScript in `browser.js` - Fix: handle the redirect response body properly ## v2.1.1 Fix packaging errors in v2.1.0. ## v2.1.0 - Enhance: allow using ArrayBuffer as the `body` of a `fetch()` or `Request` - Fix: store HTTP headers of a `Headers` object internally with the given case, for compatibility with older servers that incorrectly treated header names in a case-sensitive manner - Fix: silently ignore invalid HTTP headers - Fix: handle HTTP redirect responses without a `Location` header just like non-redirect responses - Fix: include bodies when following a redirection when appropriate ## 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. ### General changes - Major: Node.js 0.10.x and 0.12.x support is dropped - Major: `require('node-fetch/lib/response')` etc. is now unsupported; use `require('node-fetch').Response` or ES6 module imports - Enhance: start testing on Node.js v4.x, v6.x, v8.x LTS, as well as v9.x stable - Enhance: use Rollup to produce a distributed bundle (less memory overhead and faster startup) - Enhance: make `Object.prototype.toString()` on Headers, Requests, and Responses return correct class strings - Other: rewrite in ES2015 using Babel - Other: use Codecov for code coverage tracking - Other: update package.json script for npm 5 - Other: `encoding` module is now optional (alpha.7) - Other: expose browser.js through package.json, avoid bundling mishaps (alpha.9) - Other: allow TypeScript to `import` node-fetch by exposing default (alpha.9) ### HTTP requests - Major: overwrite user's `Content-Length` if we can be sure our information is correct (per spec) - Fix: errors in a response are caught before the body is accessed - Fix: support WHATWG URL objects, created by `whatwg-url` package or `require('url').URL` in Node.js 7+ ### 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: 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) - Enhance: add experimental `response.blob()` (also applies to Requests) - Enhance: `URLSearchParams` is now accepted as a body - Enhance: wrap `response.json()` json parsing error as `FetchError` - Fix: fix Request and Response with `null` body ### Headers class - Major: remove `headers.getAll()`; make `get()` return all headers delimited by commas (per spec) - Enhance: make Headers iterable - Enhance: make Headers constructor accept an array of tuples - Enhance: make sure header names and values are valid in HTTP - Fix: coerce Headers prototype function parameters to strings, where applicable ### Documentation - Enhance: more comprehensive API docs - Enhance: add a list of default headers in README # 1.x release ## 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. ## v1.6.3 - Enhance: error handling document to explain `FetchError` design - Fix: support `form-data` 2.x releases (requires `form-data` >= 2.1.0) ## v1.6.2 - Enhance: minor document update - Fix: response.json() returns empty object on 204 no-content response instead of throwing a syntax error ## v1.6.1 - Fix: if `res.body` is a non-stream non-formdata object, we will call `body.toString` and send it as a string - Fix: `counter` value is incorrectly set to `follow` value when wrapping Request instance - Fix: documentation update ## v1.6.0 - Enhance: added `res.buffer()` api for convenience, it returns body as a Node.js buffer - Enhance: better old server support by handling raw deflate response - Enhance: skip encoding detection for non-HTML/XML response - Enhance: minor document update - Fix: HEAD request doesn't need decompression, as body is empty - Fix: `req.body` now accepts a Node.js buffer ## v1.5.3 - Fix: handle 204 and 304 responses when body is empty but content-encoding is gzip/deflate - Fix: allow resolving response and cloned response in any order - Fix: avoid setting `content-length` when `form-data` body use streams - Fix: send DELETE request with content-length when body is present - Fix: allow any url when calling new Request, but still reject non-http(s) url in fetch ## v1.5.2 - Fix: allow node.js core to handle keep-alive connection pool when passing a custom agent ## v1.5.1 - Fix: redirect mode `manual` should work even when there is no redirection or broken redirection ## v1.5.0 - Enhance: rejected promise now use custom `Error` (thx to @pekeler) - Enhance: `FetchError` contains `err.type` and `err.code`, allows for better error handling (thx to @pekeler) - Enhance: basic support for redirect mode `manual` and `error`, allows for location header extraction (thx to @jimmywarting for the initial PR) ## v1.4.1 - Fix: wrapping Request instance with FormData body again should preserve the body as-is ## v1.4.0 - Enhance: Request and Response now have `clone` method (thx to @kirill-konshin for the initial PR) - Enhance: Request and Response now have proper string and buffer body support (thx to @kirill-konshin) - Enhance: Body constructor has been refactored out (thx to @kirill-konshin) - Enhance: Headers now has `forEach` method (thx to @tricoder42) - Enhance: back to 100% code coverage - Fix: better form-data support (thx to @item4) - Fix: better character encoding detection under chunked encoding (thx to @dsuket for the initial PR) ## v1.3.3 - Fix: make sure `Content-Length` header is set when body is string for POST/PUT/PATCH requests - Fix: handle body stream error, for cases such as incorrect `Content-Encoding` header - Fix: when following certain redirects, use `GET` on subsequent request per Fetch Spec - Fix: `Request` and `Response` constructors now parse headers input using `Headers` ## v1.3.2 - Enhance: allow auto detect of form-data input (no `FormData` spec on node.js, this is form-data specific feature) ## v1.3.1 - Enhance: allow custom host header to be set (server-side only feature, as it's a forbidden header on client-side) ## v1.3.0 - Enhance: now `fetch.Request` is exposed as well ## v1.2.1 - Enhance: `Headers` now normalized `Number` value to `String`, prevent common mistakes ## v1.2.0 - Enhance: now fetch.Headers and fetch.Response are exposed, making testing easier ## v1.1.2 - Fix: `Headers` should only support `String` and `Array` properties, and ignore others ## v1.1.1 - Enhance: now req.headers accept both plain object and `Headers` instance ## v1.1.0 - Enhance: timeout now also applies to response body (in case of slow response) - Fix: timeout is now cleared properly when fetch is done/has failed ## v1.0.6 - Fix: less greedy content-type charset matching ## v1.0.5 - Fix: when `follow = 0`, fetch should not follow redirect - Enhance: update tests for better coverage - Enhance: code formatting - Enhance: clean up doc ## v1.0.4 - Enhance: test iojs support - Enhance: timeout attached to socket event only fire once per redirect ## v1.0.3 - Fix: response size limit should reject large chunk - Enhance: added character encoding detection for xml, such as rss/atom feed (encoding in DTD) ## v1.0.2 - Fix: added res.ok per spec change ## v1.0.0 - Enhance: better test coverage and doc # 0.x release ## v0.1 - Major: initial public release node-fetch-2.6.7/ERROR-HANDLING.md000066400000000000000000000032531417100253500161400ustar00rootroot00000000000000 Error handling with node-fetch ============================== Because `window.fetch` isn't designed to be transparent about the cause of request errors, we have to come up with our own solutions. 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`. ```js fetch(url, { signal }).catch(err => { if (err.name === 'AbortError') { // 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 errors come with an `err.message` detailing the cause of errors. - All errors originating from `node-fetch` are marked with a custom `err.type`. - All errors originating from Node.js core are marked with `err.type = 'system'`, and in addition contain an `err.code` and an `err.errno` for error handling. These are aliases for error codes thrown by Node.js core. - [Programmer errors][joyent-guide] are either thrown as soon as possible, or rejected with default `Error` with `err.message` for ease of troubleshooting. List of error types: - Because we maintain 100% coverage, see [test.js](https://github.com/bitinn/node-fetch/blob/master/test/test.js) for a full list of custom `FetchError` types, as well as some of the common errors from Node.js [joyent-guide]: https://www.joyent.com/node-js/production/design/errors#operational-errors-vs-programmer-errors node-fetch-2.6.7/LICENSE.md000066400000000000000000000020671417100253500151510ustar00rootroot00000000000000The MIT License (MIT) Copyright (c) 2016 David Frank Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. node-fetch-2.6.7/LIMITS.md000066400000000000000000000031601417100253500150630ustar00rootroot00000000000000 Known differences ================= *As of 2.x release* - Topics such as Cross-Origin, Content Security Policy, Mixed Content, Service Workers are ignored, given our server-side context. - URL input must be an absolute URL, using either `http` or `https` as scheme. - On the upside, there are no forbidden headers. - `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 string, 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). - 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 node-fetch-2.6.7/README.md000066400000000000000000000465061417100253500150320ustar00rootroot00000000000000node-fetch ========== [![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] - [Motivation](#motivation) - [Features](#features) - [Difference from client-side fetch](#difference-from-client-side-fetch) - [Installation](#installation) - [Loading and configuring the module](#loading-and-configuring-the-module) - [Common Usage](#common-usage) - [Plain text or HTML](#plain-text-or-html) - [JSON](#json) - [Simple Post](#simple-post) - [Post with JSON](#post-with-json) - [Post with form parameters](#post-with-form-parameters) - [Handling exceptions](#handling-exceptions) - [Handling client and server errors](#handling-client-and-server-errors) - [Advanced Usage](#advanced-usage) - [Streams](#streams) - [Buffer](#buffer) - [Accessing Headers and other Meta data](#accessing-headers-and-other-meta-data) - [Extract Set-Cookie Header](#extract-set-cookie-header) - [Post data using a file stream](#post-data-using-a-file-stream) - [Post with form-data (detect multipart)](#post-with-form-data-detect-multipart) - [Request cancellation with AbortSignal](#request-cancellation-with-abortsignal) - [API](#api) - [fetch(url[, options])](#fetchurl-options) - [Options](#options) - [Class: Request](#class-request) - [Class: Response](#class-response) - [Class: Headers](#class-headers) - [Interface: Body](#interface-body) - [Class: FetchError](#class-fetcherror) - [License](#license) - [Acknowledgement](#acknowledgement) ## Motivation Instead of implementing `XMLHttpRequest` in Node.js to run browser-specific [Fetch polyfill](https://github.com/github/fetch), why not go from native `http` to `fetch` API directly? Hence, `node-fetch`, minimal code for a `window.fetch` compatible API on Node.js runtime. See Matt Andrews' [isomorphic-fetch](https://github.com/matthew-andrews/isomorphic-fetch) or Leonardo Quixada's [cross-fetch](https://github.com/lquixada/cross-fetch) for isomorphic usage (exports `node-fetch` for server-side, `whatwg-fetch` for client-side). ## Features - Stay consistent with `window.fetch` API. - Make conscious trade-off when following [WHATWG fetch spec][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. ## Difference from client-side fetch - See [Known Differences](LIMITS.md) for details. - If you happen to use a missing feature that `window.fetch` offers, feel free to open an issue. - Pull requests are welcomed too! ## Installation Current stable release (`2.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 const fetch = require('node-fetch'); ``` If you are using a Promise library other than native, set it through `fetch.Promise`: ```js const Bluebird = require('bluebird'); fetch.Promise = Bluebird; ``` ## 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. #### Plain text or HTML ```js fetch('https://github.com/') .then(res => res.text()) .then(body => console.log(body)); ``` #### JSON ```js fetch('https://api.github.com/users/github') .then(res => res.json()) .then(json => console.log(json)); ``` #### 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)); ``` #### Post with JSON ```js 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)); ``` #### 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. 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 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)); ``` #### Handling exceptions NOTE: 3xx-5xx responses are *NOT* exceptions and should be handled in `then()`; see the next section for more information. 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. ```js fetch('https://domain.invalid/') .catch(err => console.error(err)); ``` #### 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 function checkStatus(res) { 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...')) ``` ## Advanced Usage #### 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); }); ``` #### Buffer If you prefer to cache binary data in full, use buffer(). (NOTE: `buffer()` is a `node-fetch`-only API) ```js 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 => { /* ... */ }); ``` #### 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')); }); ``` #### 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']); }); ``` #### Post data using a file stream ```js const { createReadStream } = require('fs'); const stream = createReadStream('input.txt'); fetch('https://httpbin.org/post', { method: 'POST', body: stream }) .then(res => res.json()) .then(json => console.log(json)); ``` #### Post with form-data (detect multipart) ```js 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)); // 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() } fetch('https://httpbin.org/post', options) .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 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 controller = new AbortController(); 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); }); ``` See [test cases](https://github.com/bitinn/node-fetch/blob/master/test/test.js) for more examples. ## API ### fetch(url[, options]) - `url` A string representing the URL for fetching - `options` [Options](#fetch-options) for the HTTP(S) request - Returns: Promise<[Response](#class-response)> 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. ```js { // 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 // 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) } ``` ##### 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)` Note: when `body` is a `Stream`, `Content-Length` is not set automatically. ##### 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: - Support self-signed certificate - Use only IPv4 or IPv6 - Custom DNS Lookup See [`http.Agent`](https://nodejs.org/api/http.html#http_new_agent_options) for more information. 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 httpAgent = new http.Agent({ keepAlive: true }); const httpsAgent = new https.Agent({ keepAlive: true }); const options = { agent: function (_parsedURL) { if (_parsedURL.protocol == 'http:') { return httpAgent; } else { return httpsAgent; } } } ``` ### Class: Request An HTTP(S) request containing information about URL, method, headers, and the body. This class implements the [Body](#iface-body) interface. Due to the nature of Node.js, the following properties are not implemented at this moment: - `type` - `destination` - `referrer` - `referrerPolicy` - `mode` - `credentials` - `cache` - `integrity` - `keepalive` The following node-fetch extension properties are provided: - `follow` - `compress` - `counter` - `agent` See [options](#fetch-options) for exact meaning of these extensions. #### new Request(input[, options]) *(spec-compliant)* - `input` A string representing a URL, or another `Request` (which will be cloned) - `options` [Options][#fetch-options] for the HTTP(S) request Constructs a new `Request` object. The constructor is identical to that in the [browser](https://developer.mozilla.org/en-US/docs/Web/API/Request/Request). 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. The following properties are not implemented in node-fetch at this moment: - `Response.error()` - `Response.redirect()` - `type` - `trailer` #### new Response([body[, options]]) *(spec-compliant)* - `body` A `String` or [`Readable` stream][node-readable] - `options` A [`ResponseInit`][response-init] options dictionary Constructs a new `Response` object. The constructor is identical to that in the [browser](https://developer.mozilla.org/en-US/docs/Web/API/Response/Response). Because Node.js does not implement service workers (for which this class was designed), one rarely has to construct a `Response` directly. #### response.ok *(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)* 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)* - `init` Optional argument to pre-fill the `Headers` object Construct a new `Headers` object. `init` can be either `null`, a `Headers` object, an key-value map object or any iterable object. ```js // Example adapted from https://fetch.spec.whatwg.org/#example-headers-class const meta = { '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 headers = new Headers(meta); // You can in fact use any iterable objects, like a Map or even another Headers const meta = new Map(); meta.set('Content-Type', 'text/xml'); meta.set('Breaking-Bad', '<3'); const headers = new Headers(meta); const copyOfHeaders = new Headers(headers); ``` ### Interface: Body `Body` is an abstract interface with methods that are applicable to both `Request` and `Response` classes. The following methods are not yet implemented in node-fetch at this moment: - `formData()` #### body.body *(deviation from spec)* * 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)* * `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)* * Returns: Promise Consume the body and return a promise that will resolve to one of these formats. #### body.buffer() *(node-fetch extension)* * Returns: Promise<Buffer> 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)* An operational error in the fetching process. See [ERROR-HANDLING.md][] for more info. ### Class: AbortError *(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. ## 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). ## 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://flat.badgen.net/codecov/c/github/bitinn/node-fetch/master [codecov-url]: https://codecov.io/gh/bitinn/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 node-fetch-2.6.7/UPGRADE-GUIDE.md000066400000000000000000000105721417100253500160110ustar00rootroot00000000000000# Upgrade to node-fetch v2.x node-fetch v2.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 v1.x needs to be updated to work with node-fetch v2.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 v2.x, but rather that of the most important breaking changes. See our [changelog] for other comparatively minor modifications. ## `.text()` no longer tries to detect encoding In v1.x, `response.text()` attempts to guess the text encoding of the input material and decode it for the user. However, it runs counter to the Fetch Standard which demands `.text()` to always use UTF-8. In "response" to that, we have changed `.text()` to use UTF-8. A new function **`response.textConverted()`** is created that maintains the behavior of `.text()` in v1.x. ## Internal methods hidden In v1.x, the user can access internal methods such as `_clone()`, `_decode()`, and `_convert()` on the `response` object. While these methods should never have been used, node-fetch v2.x makes these functions completely inaccessible. If your app makes use of these functions, it may break when upgrading to v2.x. If you have a use case that requires these methods to be available, feel free to file an issue and we will be happy to help you solve the problem. ## Headers The main goal we have for the `Headers` class in v2.x is to make it completely spec-compliant. These changes are done in conjunction with GitHub's [`whatwg-fetch`][gh-fetch] polyfill, [Chrome][chrome-headers], and [Firefox][firefox-headers]. ```js ////////////////////////////////////////////////////////////////////////////// // `get()` now returns **all** headers, joined by a comma, instead of only the // first one. Its original behavior can be emulated using // `get().split(',')[0]`. const headers = new Headers({ 'Abc': 'string', 'Multi': [ 'header1', 'header2' ] }); // before after headers.get('Abc') => headers.get('Abc') => 'string' 'string' headers.get('Multi') => headers.get('Multi') => 'header1'; 'header1,header2'; headers.get('Multi').split(',')[0] => 'header1'; ////////////////////////////////////////////////////////////////////////////// // `getAll()` is removed. Its behavior in v1 can be emulated with // `get().split(',')`. const headers = new Headers({ 'Abc': 'string', 'Multi': [ 'header1', 'header2' ] }); // before after headers.getAll('Multi') => headers.getAll('Multi') => [ 'header1', 'header2' ]; throws ReferenceError headers.get('Multi').split(',') => [ 'header1', 'header2' ]; ////////////////////////////////////////////////////////////////////////////// // All method parameters are now stringified. const headers = new Headers(); headers.set('null-header', null); headers.set('undefined', undefined); // before after headers.get('null-header') headers.get('null-header') => null => 'null' headers.get(undefined) headers.get(undefined) => throws => 'undefined' ////////////////////////////////////////////////////////////////////////////// // Invalid HTTP header names and values are now rejected outright. const headers = new Headers(); headers.set('Héy', 'ok'); // now throws headers.get('Héy'); // now throws new Headers({ 'Héy': 'ok' }); // now throws ``` ## Node.js v0.x support dropped If you are still using Node.js v0.10 or v0.12, upgrade ASAP. Not only has it become too much work for us to maintain, Node.js has also dropped support for those release branches in 2016. Check out Node.js' official [LTS plan] for more information on Node.js' support lifetime. [whatwg-fetch]: https://fetch.spec.whatwg.org/ [LTS plan]: https://github.com/nodejs/LTS#lts-plan [gh-fetch]: https://github.com/github/fetch [chrome-headers]: https://crbug.com/645492 [firefox-headers]: https://bugzilla.mozilla.org/show_bug.cgi?id=1278275 [changelog]: CHANGELOG.md node-fetch-2.6.7/browser.js000066400000000000000000000013341417100253500155620ustar00rootroot00000000000000"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;node-fetch-2.6.7/build/000077500000000000000000000000001417100253500146375ustar00rootroot00000000000000node-fetch-2.6.7/build/babel-plugin.js000066400000000000000000000037641417100253500175500ustar00rootroot00000000000000// 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; } } } }); node-fetch-2.6.7/build/rollup-plugin.js000066400000000000000000000011341417100253500200050ustar00rootroot00000000000000export 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'); } }; } node-fetch-2.6.7/codecov.yml000066400000000000000000000000601417100253500157010ustar00rootroot00000000000000parsers: javascript: enable_partials: yes node-fetch-2.6.7/package.json000066400000000000000000000044511417100253500160320ustar00rootroot00000000000000{ "name": "node-fetch", "version": "2.6.7", "description": "A light-weight module that brings window.fetch to node.js", "main": "lib/index.js", "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", "dependencies": { "whatwg-url": "^5.0.0" }, "peerDependencies": { "encoding": "^0.1.0" }, "peerDependenciesMeta": { "encoding": { "optional": true } }, "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", "teeny-request": "3.7.0" } } node-fetch-2.6.7/rollup.config.js000066400000000000000000000014141417100253500166570ustar00rootroot00000000000000import isBuiltin from 'is-builtin-module'; import babel from 'rollup-plugin-babel'; import packageJson from './package.json'; import tweakDefault from './build/rollup-plugin'; process.env.BABEL_ENV = 'rollup'; const dependencies = Object.keys(packageJson.dependencies); 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) { return dependencies.includes(id) || isBuiltin(id); } }; node-fetch-2.6.7/src/000077500000000000000000000000001417100253500143275ustar00rootroot00000000000000node-fetch-2.6.7/src/abort-error.js000066400000000000000000000011101417100253500171140ustar00rootroot00000000000000/** * 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'; node-fetch-2.6.7/src/blob.js000066400000000000000000000055301417100253500156060ustar00rootroot00000000000000// 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 }); node-fetch-2.6.7/src/body.js000066400000000000000000000306361417100253500156320ustar00rootroot00000000000000 /** * body.js * * Body interface provides common methods for Request and Response */ import Stream from 'stream'; import Blob, { BUFFER } from './blob.js'; import FetchError from './fetch-error.js'; let convert; try { convert = require('encoding').convert; } catch(e) {} const INTERNALS = Symbol('Body internals'); // fix an issue where "PassThrough" isn't a named export for node <10 const PassThrough = Stream.PassThrough; /** * Body mixin * * Ref: https://fetch.spec.whatwg.org/#body * * @param Stream body Readable stream * @param Object opts Response options * @return Void */ export default function Body(body, { size = 0, timeout = 0 } = {}) { if (body == null) { // body is undefined or null body = null; } else if (isURLSearchParams(body)) { // body is a URLSearchParams body = Buffer.from(body.toString()); } else if (isBlob(body)) { // body is blob } else if (Buffer.isBuffer(body)) { // body is Buffer } else if (Object.prototype.toString.call(body) === '[object ArrayBuffer]') { // body is ArrayBuffer body = Buffer.from(body); } else if (ArrayBuffer.isView(body)) { // body is ArrayBufferView body = Buffer.from(body.buffer, body.byteOffset, body.byteLength); } else if (body instanceof Stream) { // body is stream } else { // none of the above // coerce to string then buffer body = Buffer.from(String(body)); } this[INTERNALS] = { body, disturbed: false, error: null }; this.size = size; this.timeout = timeout; 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); this[INTERNALS].error = error; }); } } Body.prototype = { get body() { return this[INTERNALS].body; }, get bodyUsed() { return this[INTERNALS].disturbed; }, /** * Decode response as ArrayBuffer * * @return Promise */ arrayBuffer() { return consumeBody.call(this).then(buf => buf.buffer.slice(buf.byteOffset, buf.byteOffset + buf.byteLength)); }, /** * Return raw response as Blob * * @return Promise */ blob() { let ct = this.headers && this.headers.get('content-type') || ''; return consumeBody.call(this).then(buf => Object.assign( // Prevent copying new Blob([], { type: ct.toLowerCase() }), { [BUFFER]: buf } )); }, /** * Decode response as json * * @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')); } }) }, /** * Decode response as text * * @return Promise */ text() { return consumeBody.call(this).then(buffer => buffer.toString()); }, /** * Decode response as buffer (non-spec api) * * @return Promise */ buffer() { return consumeBody.call(this); }, /** * Decode response as text, while automatically detecting the encoding and * trying to decode to UTF-8 (non-spec api) * * @return Promise */ textConverted() { 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.mixIn = function (proto) { for (const name of Object.getOwnPropertyNames(Body.prototype)) { // istanbul ignore else: future proof if (!(name in proto)) { const desc = Object.getOwnPropertyDescriptor(Body.prototype, name); Object.defineProperty(proto, name, desc); } } }; /** * Consume and convert an entire Body to a Buffer. * * Ref: https://fetch.spec.whatwg.org/#concept-body-consume-body * * @return Promise */ function consumeBody() { if (this[INTERNALS].disturbed) { return Body.Promise.reject(new TypeError(`body used already for: ${this.url}`)); } this[INTERNALS].disturbed = true; if (this[INTERNALS].error) { return Body.Promise.reject(this[INTERNALS].error); } let body = this.body; // body is null if (body === null) { return Body.Promise.resolve(Buffer.alloc(0)); } // body is blob if (isBlob(body)) { body = body.stream(); } // body is buffer if (Buffer.isBuffer(body)) { return Body.Promise.resolve(body); } // istanbul ignore if: should never happen if (!(body instanceof Stream)) { return Body.Promise.resolve(Buffer.alloc(0)); } // body is stream // get ready to actually consume the body let accum = []; let accumBytes = 0; let abort = false; return new Body.Promise((resolve, reject) => { let resTimeout; // allow timeout on slow response body if (this.timeout) { resTimeout = setTimeout(() => { abort = true; reject(new FetchError(`Response timeout while trying to fetch ${this.url} (over ${this.timeout}ms)`, 'body-timeout')); }, this.timeout); } // handle stream errors body.on('error', err => { if (err.name === 'AbortError') { // if the request was aborted, reject with this Error abort = true; reject(err); } else { // other errors, such as incorrect content-encoding reject(new FetchError(`Invalid response body while trying to fetch ${this.url}: ${err.message}`, 'system', err)); } }); body.on('data', chunk => { if (abort || chunk === null) { return; } if (this.size && accumBytes + chunk.length > this.size) { abort = true; reject(new FetchError(`content size at ${this.url} over limit: ${this.size}`, 'max-size')); return; } accumBytes += chunk.length; accum.push(chunk); }); body.on('end', () => { if (abort) { return; } clearTimeout(resTimeout); 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)); } }); }); } /** * 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 = /> // Note: per spec we have to first exhaust the lists then process them const pairs = []; for (const pair of init) { if (typeof pair !== 'object' || typeof pair[Symbol.iterator] !== 'function') { throw new TypeError('Each header pair must be iterable'); } pairs.push(Array.from(pair)); } for (const pair of pairs) { if (pair.length !== 2) { throw new TypeError('Each header pair must be a name/value tuple'); } this.append(pair[0], pair[1]); } } else { // record for (const key of Object.keys(init)) { const value = init[key]; this.append(key, value); } } } else { throw new TypeError('Provided initializer must be an object'); } } /** * Return combined header value given name * * @param String name Header name * @return Mixed */ get(name) { name = `${name}`; validateName(name); const key = find(this[MAP], name); if (key === undefined) { return null; } return this[MAP][key].join(', '); } /** * Iterate over all headers * * @param Function callback Executed for each item with parameters (value, name, thisArg) * @param Boolean thisArg `this` context for callback function * @return Void */ forEach(callback, thisArg = undefined) { let pairs = getHeaders(this); let i = 0; while (i < pairs.length) { const [name, value] = pairs[i]; callback.call(thisArg, value, name, this); pairs = getHeaders(this); i++; } } /** * Overwrite header values given name * * @param String name Header name * @param String value Header value * @return Void */ set(name, value) { name = `${name}`; value = `${value}`; validateName(name); validateValue(value); const key = find(this[MAP], name); this[MAP][key !== undefined ? key : name] = [value]; } /** * Append a value onto existing header * * @param String name Header name * @param String value Header value * @return Void */ append(name, value) { name = `${name}`; value = `${value}`; validateName(name); validateValue(value); const key = find(this[MAP], name); if (key !== undefined) { this[MAP][key].push(value); } else { this[MAP][name] = [value]; } } /** * Check for header name existence * * @param String name Header name * @return Boolean */ has(name) { name = `${name}`; validateName(name); return find(this[MAP], name) !== undefined; } /** * Delete all header values given name * * @param String name Header name * @return Void */ delete(name) { name = `${name}`; validateName(name); const key = find(this[MAP], name); if (key !== undefined) { delete this[MAP][key]; } }; /** * Return raw headers (non-spec api) * * @return Object */ raw() { return this[MAP]; } /** * Get an iterator on keys. * * @return Iterator */ keys() { return createHeadersIterator(this, 'key'); } /** * Get an iterator on values. * * @return Iterator */ values() { return createHeadersIterator(this, 'value'); } /** * Get an iterator on entries. * * This is the default iterator of the Headers object. * * @return Iterator */ [Symbol.iterator]() { return createHeadersIterator(this, 'key+value'); } } Headers.prototype.entries = Headers.prototype[Symbol.iterator]; Object.defineProperty(Headers.prototype, Symbol.toStringTag, { value: 'Headers', writable: false, enumerable: false, configurable: true }); 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 } }); function getHeaders(headers, kind = 'key+value') { const keys = Object.keys(headers[MAP]).sort(); return keys.map( kind === 'key' ? k => k.toLowerCase() : kind === 'value' ? k => headers[MAP][k].join(', ') : k => [k.toLowerCase(), headers[MAP][k].join(', ')] ); } const INTERNAL = Symbol('internal'); function createHeadersIterator(target, kind) { const iterator = Object.create(HeadersIteratorPrototype); iterator[INTERNAL] = { target, kind, index: 0 }; return iterator; } const HeadersIteratorPrototype = Object.setPrototypeOf({ next() { // istanbul ignore if if (!this || Object.getPrototypeOf(this) !== HeadersIteratorPrototype) { throw new TypeError('Value of `this` is not a HeadersIterator'); } const { target, kind, index } = this[INTERNAL]; const values = getHeaders(target, kind); const len = values.length; if (index >= len) { return { value: undefined, done: true }; } this[INTERNAL].index = index + 1; return { value: values[index], done: false }; } }, Object.getPrototypeOf( Object.getPrototypeOf([][Symbol.iterator]()) )); Object.defineProperty(HeadersIteratorPrototype, Symbol.toStringTag, { value: 'HeadersIterator', writable: false, enumerable: false, configurable: true }); /** * Export the Headers object in a form that Node.js can consume. * * @param Headers headers * @return Object */ export function exportNodeCompatibleHeaders(headers) { const obj = Object.assign({ __proto__: null }, headers[MAP]); // 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]; } return obj; } /** * Create a Headers object from an object of headers, ignoring those that do * not conform to HTTP grammar productions. * * @param Object obj Object of headers * @return Headers */ export function createHeadersLenient(obj) { const headers = new Headers(); for (const name of Object.keys(obj)) { if (invalidTokenRegex.test(name)) { continue; } if (Array.isArray(obj[name])) { for (const val of obj[name]) { if (invalidHeaderCharRegex.test(val)) { continue; } if (headers[MAP][name] === undefined) { headers[MAP][name] = [val]; } else { headers[MAP][name].push(val); } } } else if (!invalidHeaderCharRegex.test(obj[name])) { headers[MAP][name] = [obj[name]]; } } return headers; } node-fetch-2.6.7/src/index.js000066400000000000000000000214471417100253500160040ustar00rootroot00000000000000 /** * 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 Body, { writeToStream, getTotalBytes } from './body.js'; import Response from './response.js'; import Headers, { createHeadersLenient } from './headers.js'; import Request, { getNodeRequestOptions } from './request.js'; import FetchError from './fetch-error.js'; import AbortError from './abort-error.js'; import whatwgUrl from 'whatwg-url'; const URL = Url.URL || whatwgUrl.URL; // fix an issue where "PassThrough", "resolve" aren't a named export for node <10 const PassThrough = Stream.PassThrough; const isDomainOrSubdomain = (destination, original) => { const orig = new URL(original).hostname; const dest = new URL(destination).hostname; return orig === dest || ( orig[orig.length - dest.length - 1] === '.' && orig.endsWith(dest) ); }; /** * Fetch function * * @param Mixed url Absolute url or Request instance * @param Object opts Fetch options * @return Promise */ export default function fetch(url, opts) { // allow custom promise if (!fetch.Promise) { throw new Error('native promise missing, set fetch.Promise to your favorite alternative'); } Body.Promise = fetch.Promise; // wrap http.request into fetch return new fetch.Promise((resolve, reject) => { // build request object const request = new Request(url, opts); const options = getNodeRequestOptions(request); const send = (options.protocol === 'https:' ? https : http).request; const { signal } = request; let response = null; const abort = () => { let error = new AbortError('The user aborted a request.'); reject(error); if (request.body && request.body instanceof Stream.Readable) { request.body.destroy(error); } if (!response || !response.body) return; response.body.emit('error', error); } if (signal && signal.aborted) { abort(); return; } const abortAndFinalize = () => { abort(); finalize(); } // send request const req = send(options); let reqTimeout; if (signal) { signal.addEventListener('abort', abortAndFinalize); } function finalize() { req.abort(); if (signal) signal.removeEventListener('abort', abortAndFinalize); clearTimeout(reqTimeout); } if (request.timeout) { req.once('socket', socket => { reqTimeout = setTimeout(() => { reject(new FetchError(`network timeout at: ${request.url}`, 'request-timeout')); finalize(); }, request.timeout); }); } req.on('error', err => { reject(new FetchError(`request to ${request.url} failed, reason: ${err.message}`, 'system', err)); finalize(); }); req.on('response', res => { clearTimeout(reqTimeout); const headers = createHeadersLenient(res.headers); // HTTP fetch step 5 if (fetch.isRedirect(res.statusCode)) { // HTTP fetch step 5.2 const location = headers.get('Location'); // HTTP fetch step 5.3 let locationURL = null; try { locationURL = location === null ? null : new URL(location, request.url).toString(); } catch (err) { // error here can only be invalid URL in Location: header // do not throw when options.redirect == manual // let the user extract the errorneous redirect URL if (request.redirect !== 'manual') { reject(new FetchError(`uri requested responds with an invalid redirect URL: ${location}`, 'invalid-redirect')); finalize(); return; } } // HTTP fetch step 5.5 switch (request.redirect) { case 'error': reject(new FetchError(`uri requested responds with a redirect, redirect mode is set to error: ${request.url}`, 'no-redirect')); 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. if (locationURL !== null) { // handle corrupted header try { headers.set('Location', locationURL); } catch (err) { // istanbul ignore next: nodejs server prevent invalid response headers, we can't test this through normal request reject(err); } } break; case 'follow': // HTTP-redirect fetch step 2 if (locationURL === null) { break; } // HTTP-redirect fetch step 5 if (request.counter >= request.follow) { reject(new FetchError(`maximum redirect reached at: ${request.url}`, 'max-redirect')); finalize(); return; } // HTTP-redirect fetch step 6 (counter increment) // Create a new Request object. const requestOpts = { headers: new Headers(request.headers), follow: request.follow, counter: request.counter + 1, agent: request.agent, compress: request.compress, method: request.method, body: request.body, signal: request.signal, timeout: request.timeout, size: request.size }; if (!isDomainOrSubdomain(request.url, locationURL)) { for (const name of ['authorization', 'www-authenticate', 'cookie', 'cookie2']) { requestOpts.headers.delete(name); } } // HTTP-redirect fetch step 9 if (res.statusCode !== 303 && request.body && getTotalBytes(request) === null) { reject(new FetchError('Cannot follow redirect with body being a readable stream', 'unsupported-redirect')); finalize(); return; } // 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'); } // HTTP-redirect fetch step 15 resolve(fetch(new Request(locationURL, requestOpts))); finalize(); return; } } // prepare response res.once('end', () => { if (signal) signal.removeEventListener('abort', abortAndFinalize); }); let body = res.pipe(new PassThrough()); const response_options = { url: request.url, status: res.statusCode, statusText: res.statusMessage, headers: headers, size: request.size, timeout: request.timeout, counter: request.counter }; // HTTP-network fetch step 12.1.1.3 const codings = headers.get('Content-Encoding'); // HTTP-network fetch step 12.1.1.4: handle content codings // in following scenarios we ignore compression support // 1. compression support is disabled // 2. HEAD request // 3. no Content-Encoding header // 4. no content response (204) // 5. content not modified response (304) if (!request.compress || request.method === 'HEAD' || codings === null || res.statusCode === 204 || res.statusCode === 304) { response = new Response(body, response_options); resolve(response); return; } // For Node v6+ // Be less strict when decoding compressed responses, since sometimes // servers send slightly invalid responses that are still accepted // by common browsers. // Always using Z_SYNC_FLUSH is what cURL does. const zlibOptions = { flush: zlib.Z_SYNC_FLUSH, finishFlush: zlib.Z_SYNC_FLUSH }; // for gzip if (codings == 'gzip' || codings == 'x-gzip') { body = body.pipe(zlib.createGunzip(zlibOptions)); response = new Response(body, response_options); resolve(response); return; } // for deflate if (codings == 'deflate' || codings == 'x-deflate') { // handle the infamous raw deflate response from old servers // a hack for old IIS and Apache servers const raw = res.pipe(new PassThrough()); raw.once('data', chunk => { // see http://stackoverflow.com/questions/37519828 if ((chunk[0] & 0x0F) === 0x08) { body = body.pipe(zlib.createInflate()); } else { body = body.pipe(zlib.createInflateRaw()); } response = new Response(body, response_options); resolve(response); }); return; } // for br if (codings == 'br' && typeof zlib.createBrotliDecompress === 'function') { body = body.pipe(zlib.createBrotliDecompress()); response = new Response(body, response_options); resolve(response); return; } // otherwise, use response as-is response = new Response(body, response_options); resolve(response); }); writeToStream(req, request); }); }; /** * Redirect code matching * * @param Number code Status code * @return Boolean */ fetch.isRedirect = code => code === 301 || code === 302 || code === 303 || code === 307 || code === 308; // expose Promise fetch.Promise = global.Promise; export { Headers, Request, Response, FetchError }; node-fetch-2.6.7/src/request.js000066400000000000000000000150401417100253500163550ustar00rootroot00000000000000 /** * 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 Stream from 'stream'; import whatwgUrl from 'whatwg-url'; import Headers, { exportNodeCompatibleHeaders } from './headers.js'; import Body, { clone, extractContentType, getTotalBytes } from './body'; const INTERNALS = Symbol('Request internals'); const URL = Url.URL || whatwgUrl.URL; // fix an issue where "format", "parse" aren't a named export for node <10 const parse_url = Url.parse; const format_url = Url.format; /** * Wrapper around `new URL` to handle arbitrary URLs * * @param {string} urlStr * @return {void} */ function parseURL(urlStr) { /* 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(urlStr)) { urlStr = new URL(urlStr).toString() } // Fallback to old implementation for arbitrary URLs return parse_url(urlStr); } const streamDestructionSupported = 'destroy' in Stream.Readable.prototype; /** * Check if a value is an instance of Request. * * @param Mixed input * @return Boolean */ function isRequest(input) { return ( typeof input === 'object' && typeof input[INTERNALS] === 'object' ); } function isAbortSignal(signal) { const proto = ( signal && typeof signal === 'object' && Object.getPrototypeOf(signal) ); return !!(proto && proto.constructor.name === 'AbortSignal'); } /** * Request class * * @param Mixed input Url or Request instance * @param Object init Custom options * @return Void */ export default class Request { constructor(input, init = {}) { let parsedURL; // normalize input if (!isRequest(input)) { if (input && input.href) { // in order to support Node.js' Url objects; though WHATWG's URL objects // will fall into this branch also (since their `toString()` will return // `href` property anyway) parsedURL = parseURL(input.href); } else { // coerce input to a string before attempting to parse parsedURL = parseURL(`${input}`); } input = {}; } else { parsedURL = parseURL(input.url); } let method = init.method || input.method || 'GET'; method = method.toUpperCase(); 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 ? init.body : isRequest(input) && input.body !== null ? clone(input) : null; Body.call(this, inputBody, { timeout: init.timeout || input.timeout || 0, size: init.size || input.size || 0 }); const headers = new Headers(init.headers || input.headers || {}); 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 if (signal != null && !isAbortSignal(signal)) { throw new TypeError('Expected signal to be an instanceof AbortSignal'); } this[INTERNALS] = { method, redirect: init.redirect || input.redirect || 'follow', headers, parsedURL, signal, }; // node-fetch-only options this.follow = init.follow !== undefined ? init.follow : input.follow !== undefined ? input.follow : 20; this.compress = init.compress !== undefined ? init.compress : input.compress !== undefined ? input.compress : true; this.counter = init.counter || input.counter || 0; this.agent = init.agent || input.agent; } get method() { return this[INTERNALS].method; } get url() { return format_url(this[INTERNALS].parsedURL); } get headers() { return this[INTERNALS].headers; } get redirect() { return this[INTERNALS].redirect; } get signal() { return this[INTERNALS].signal; } /** * Clone this request * * @return Request */ clone() { return new Request(this); } } Body.mixIn(Request.prototype); Object.defineProperty(Request.prototype, Symbol.toStringTag, { value: 'Request', writable: false, enumerable: false, configurable: true }); Object.defineProperties(Request.prototype, { method: { enumerable: true }, url: { enumerable: true }, headers: { enumerable: true }, redirect: { enumerable: true }, clone: { enumerable: true }, signal: { enumerable: true }, }); /** * Convert a Request to Node.js http request options. * * @param Request A Request instance * @return Object The options object to be passed to http.request */ export function getNodeRequestOptions(request) { const parsedURL = request[INTERNALS].parsedURL; const headers = new Headers(request[INTERNALS].headers); // fetch step 1.3 if (!headers.has('Accept')) { headers.set('Accept', '*/*'); } // Basic fetch if (!parsedURL.protocol || !parsedURL.hostname) { throw new TypeError('Only absolute URLs are supported'); } if (!/^https?:$/.test(parsedURL.protocol)) { throw new TypeError('Only HTTP(S) protocols are supported'); } if ( request.signal && request.body instanceof Stream.Readable && !streamDestructionSupported ) { throw new Error('Cancellation of streamed requests with AbortSignal is not supported in node < 8'); } // HTTP-network-or-cache fetch steps 2.4-2.7 let contentLengthValue = null; if (request.body == null && /^(POST|PUT)$/i.test(request.method)) { contentLengthValue = '0'; } if (request.body != null) { const totalBytes = getTotalBytes(request); if (typeof totalBytes === 'number') { contentLengthValue = String(totalBytes); } } if (contentLengthValue) { headers.set('Content-Length', contentLengthValue); } // HTTP-network-or-cache fetch step 2.11 if (!headers.has('User-Agent')) { headers.set('User-Agent', 'node-fetch/1.0 (+https://github.com/bitinn/node-fetch)'); } // HTTP-network-or-cache fetch step 2.15 if (request.compress && !headers.has('Accept-Encoding')) { headers.set('Accept-Encoding', 'gzip,deflate'); } let agent = request.agent; if (typeof agent === 'function') { agent = agent(parsedURL); } if (!headers.has('Connection') && !agent) { headers.set('Connection', 'close'); } // HTTP-network fetch step 4.2 // chunked encoding is handled by Node.js return Object.assign({}, parsedURL, { method: request.method, headers: exportNodeCompatibleHeaders(headers), agent }); } node-fetch-2.6.7/src/response.js000066400000000000000000000041741417100253500165310ustar00rootroot00000000000000 /** * response.js * * Response class provides content decoding */ import http from 'http'; import Headers from './headers.js'; 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 * * @param Stream body Readable stream * @param Object opts Response options * @return Void */ export default class Response { constructor(body = null, opts = {}) { Body.call(this, body, opts); const status = opts.status || 200; const headers = new Headers(opts.headers) if (body != null && !headers.has('Content-Type')) { const contentType = extractContentType(body); if (contentType) { headers.append('Content-Type', contentType); } } this[INTERNALS] = { url: opts.url, status, statusText: opts.statusText || STATUS_CODES[status], headers, counter: opts.counter }; } get url() { return this[INTERNALS].url || ''; } get status() { return this[INTERNALS].status; } /** * Convenience property representing if the request ended normally */ get ok() { return this[INTERNALS].status >= 200 && this[INTERNALS].status < 300; } get redirected() { return this[INTERNALS].counter > 0; } get statusText() { return this[INTERNALS].statusText; } get headers() { return this[INTERNALS].headers; } /** * Clone this response * * @return Response */ clone() { return new Response(clone(this), { url: this.url, status: this.status, statusText: this.statusText, headers: this.headers, ok: this.ok, redirected: this.redirected }); } } 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 } }); Object.defineProperty(Response.prototype, Symbol.toStringTag, { value: 'Response', writable: false, enumerable: false, configurable: true }); node-fetch-2.6.7/test/000077500000000000000000000000001417100253500145175ustar00rootroot00000000000000node-fetch-2.6.7/test/dummy.txt000066400000000000000000000000141417100253500164060ustar00rootroot00000000000000i am a dummynode-fetch-2.6.7/test/server.js000066400000000000000000000227621417100253500163740ustar00rootroot00000000000000import * as http from 'http'; import { parse } from 'url'; import * as zlib from 'zlib'; import { multipart as Multipart } from 'parted'; let convert; try { convert = require('encoding').convert; } catch(e) {} export default class TestServer { constructor() { this.server = http.createServer(this.router); this.port = 30001; this.hostname = 'localhost'; // node 8 default keepalive timeout is 5000ms // make it shorter here as we want to close server quickly at the end of tests this.server.keepAliveTimeout = 1000; this.server.on('error', function(err) { console.log(err.stack); }); this.server.on('connection', function(socket) { socket.setTimeout(1500); }); } start(cb) { this.server.listen(this.port, this.hostname, cb); } stop(cb) { this.server.close(cb); } router(req, res) { let p = decodeURIComponent(parse(req.url).pathname); if (p === '/hello') { res.statusCode = 200; res.setHeader('Content-Type', 'text/plain'); res.end('world'); } if (p === '/plain') { res.statusCode = 200; res.setHeader('Content-Type', 'text/plain'); res.end('text'); } if (p === '/options') { res.statusCode = 200; res.setHeader('Allow', 'GET, HEAD, OPTIONS'); res.end('hello world'); } if (p === '/html') { res.statusCode = 200; res.setHeader('Content-Type', 'text/html'); res.end(''); } if (p === '/json') { res.statusCode = 200; res.setHeader('Content-Type', 'application/json'); res.end(JSON.stringify({ name: 'value' })); } if (p.startsWith('/redirect-to/3')) { res.statusCode = p.slice(13, 16); res.setHeader('Location', p.slice(17)); res.end(); } if (p === '/gzip') { res.statusCode = 200; res.setHeader('Content-Type', 'text/plain'); res.setHeader('Content-Encoding', 'gzip'); zlib.gzip('hello world', function(err, buffer) { res.end(buffer); }); } if (p === '/gzip-truncated') { 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)); }); } if (p === '/deflate') { res.statusCode = 200; res.setHeader('Content-Type', 'text/plain'); res.setHeader('Content-Encoding', 'deflate'); zlib.deflate('hello world', function(err, buffer) { res.end(buffer); }); } if (p === '/brotli') { res.statusCode = 200; res.setHeader('Content-Type', 'text/plain'); if (typeof zlib.createBrotliDecompress === 'function') { res.setHeader('Content-Encoding', 'br'); zlib.brotliCompress('hello world', function (err, buffer) { res.end(buffer); }); } } if (p === '/deflate-raw') { res.statusCode = 200; res.setHeader('Content-Type', 'text/plain'); res.setHeader('Content-Encoding', 'deflate'); zlib.deflateRaw('hello world', function(err, buffer) { res.end(buffer); }); } if (p === '/sdch') { res.statusCode = 200; res.setHeader('Content-Type', 'text/plain'); res.setHeader('Content-Encoding', 'sdch'); res.end('fake sdch string'); } if (p === '/invalid-content-encoding') { res.statusCode = 200; res.setHeader('Content-Type', 'text/plain'); res.setHeader('Content-Encoding', 'gzip'); res.end('fake gzip string'); } if (p === '/timeout') { setTimeout(function() { res.statusCode = 200; res.setHeader('Content-Type', 'text/plain'); res.end('text'); }, 1000); } if (p === '/slow') { res.statusCode = 200; res.setHeader('Content-Type', 'text/plain'); res.write('test'); setTimeout(function() { res.end('test'); }, 1000); } if (p === '/cookie') { res.statusCode = 200; res.setHeader('Set-Cookie', ['a=1', 'b=1']); res.end('cookie'); } if (p === '/size/chunk') { res.statusCode = 200; res.setHeader('Content-Type', 'text/plain'); setTimeout(function() { res.write('test'); }, 10); setTimeout(function() { res.end('test'); }, 20); } if (p === '/size/long') { res.statusCode = 200; res.setHeader('Content-Type', 'text/plain'); res.end('testtest'); } if (p === '/encoding/gbk') { res.statusCode = 200; res.setHeader('Content-Type', 'text/html'); res.end(convert('
中文
', 'gbk')); } if (p === '/encoding/gb2312') { res.statusCode = 200; res.setHeader('Content-Type', 'text/html'); res.end(convert('
中文
', 'gb2312')); } if (p === '/encoding/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'); res.end(); } if (p === '/redirect/302') { res.statusCode = 302; res.setHeader('Location', '/inspect'); res.end(); } if (p === '/redirect/303') { res.statusCode = 303; res.setHeader('Location', '/inspect'); res.end(); } if (p === '/redirect/307') { res.statusCode = 307; res.setHeader('Location', '/inspect'); res.end(); } if (p === '/redirect/308') { res.statusCode = 308; res.setHeader('Location', '/inspect'); res.end(); } if (p === '/redirect/chain') { res.statusCode = 301; res.setHeader('Location', '/redirect/301'); res.end(); } if (p === '/redirect/no-location') { res.statusCode = 301; res.end(); } if (p === '/redirect/slow') { res.statusCode = 301; res.setHeader('Location', '/redirect/301'); setTimeout(function() { res.end(); }, 1000); } if (p === '/redirect/slow-chain') { res.statusCode = 301; res.setHeader('Location', '/redirect/slow'); setTimeout(function() { res.end(); }, 10); } if (p === '/redirect/slow-stream') { res.statusCode = 301; res.setHeader('Location', '/slow'); res.end(); } if (p === '/error/400') { res.statusCode = 400; res.setHeader('Content-Type', 'text/plain'); res.end('client error'); } if (p === '/error/404') { res.statusCode = 404; res.setHeader('Content-Encoding', 'gzip'); res.end(); } if (p === '/error/500') { res.statusCode = 500; res.setHeader('Content-Type', 'text/plain'); res.end('server error'); } if (p === '/error/reset') { res.destroy(); } if (p === '/error/json') { res.statusCode = 200; res.setHeader('Content-Type', 'application/json'); res.end('invalid json'); } if (p === '/no-content') { res.statusCode = 204; res.end(); } if (p === '/no-content/gzip') { res.statusCode = 204; res.setHeader('Content-Encoding', 'gzip'); res.end(); } if (p === '/no-content/brotli') { res.statusCode = 204; res.setHeader('Content-Encoding', 'br'); res.end(); } if (p === '/not-modified') { res.statusCode = 304; res.end(); } if (p === '/not-modified/gzip') { res.statusCode = 304; res.setHeader('Content-Encoding', 'gzip'); res.end(); } if (p === '/inspect') { res.statusCode = 200; res.setHeader('Content-Type', 'application/json'); let body = ''; req.on('data', function(c) { body += c }); req.on('end', function() { res.end(JSON.stringify({ method: req.method, url: req.url, headers: req.headers, body })); }); } if (p === '/multipart') { res.statusCode = 200; res.setHeader('Content-Type', 'application/json'); const parser = new Multipart(req.headers['content-type']); let body = ''; parser.on('part', function(field, part) { body += field + '=' + part; }); parser.on('end', function() { res.end(JSON.stringify({ method: req.method, url: req.url, headers: req.headers, body: body })); }); req.pipe(parser); } if (p === '/issues/1290/ひらがな') { res.statusCode = 200; res.setHeader('Content-Type', 'text/plain'); res.end('Success'); } } } if (require.main === module) { const server = new TestServer; server.start(() => { console.log(`Server started listening at port ${server.port}`); }); } node-fetch-2.6.7/test/test.js000066400000000000000000002503001417100253500160340ustar00rootroot00000000000000 // test tools import chai from 'chai'; import chaiPromised from 'chai-as-promised'; import chaiIterator from 'chai-iterator'; import chaiString from 'chai-string'; 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 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 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"; const supportToString = ({ [Symbol.toStringTag]: 'z' }).toString() === '[object z]'; const supportStreamDestroy = 'destroy' in stream.Readable.prototype; const local = new TestServer(); const base = `http://${local.hostname}:${local.port}/`; before(done => { local.start(done); }); after(done => { local.stop(done); }); describe('node-fetch', () => { it('should return a promise', function() { 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() { const url = `${base}hello`; const old = fetch.Promise; fetch.Promise = then; expect(fetch(url)).to.be.an.instanceof(then); expect(fetch(url)).to.not.be.an.instanceof(old); fetch.Promise = old; }); it('should throw error when no promise implementation are found', function() { const url = `${base}hello`; const old = fetch.Promise; fetch.Promise = undefined; expect(() => { fetch(url) }).to.throw(Error); fetch.Promise = old; }); it('should expose Headers, Response and Request constructors', function() { 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() { 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() { 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() { 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() { 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() { 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' }); }); it('should resolve into response', function() { const url = `${base}hello`; return fetch(url).then(res => { expect(res).to.be.an.instanceof(Response); expect(res.headers).to.be.an.instanceof(Headers); expect(res.body).to.be.an.instanceof(stream.Transform); expect(res.bodyUsed).to.be.false; expect(res.url).to.equal(url); expect(res.ok).to.be.true; expect(res.status).to.equal(200); expect(res.statusText).to.equal('OK'); }); }); it('should accept plain text response', function() { const url = `${base}plain`; return fetch(url).then(res => { expect(res.headers.get('content-type')).to.equal('text/plain'); return res.text().then(result => { expect(res.bodyUsed).to.be.true; expect(result).to.be.a('string'); expect(result).to.equal('text'); }); }); }); it('should accept html response (like plain text)', function() { const url = `${base}html`; return fetch(url).then(res => { expect(res.headers.get('content-type')).to.equal('text/html'); return res.text().then(result => { expect(res.bodyUsed).to.be.true; expect(result).to.be.a('string'); expect(result).to.equal(''); }); }); }); it('should accept json response', function() { 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' }); }); }); }); it('should send request with custom headers', function() { const url = `${base}inspect`; const opts = { headers: { 'x-custom-header': 'abc' } }; return fetch(url, opts).then(res => { return res.json(); }).then(res => { expect(res.headers['x-custom-header']).to.equal('abc'); }); }); it('should accept headers instance', function() { const url = `${base}inspect`; const opts = { headers: new Headers({ 'x-custom-header': 'abc' }) }; return fetch(url, opts).then(res => { return res.json(); }).then(res => { expect(res.headers['x-custom-header']).to.equal('abc'); }); }); it('should accept custom host header', function() { const url = `${base}inspect`; const opts = { headers: { host: 'example.com' } }; return fetch(url, opts).then(res => { return res.json(); }).then(res => { expect(res.headers['host']).to.equal('example.com'); }); }); it('should accept custom HoSt header', function() { const url = `${base}inspect`; const opts = { headers: { HoSt: 'example.com' } }; return fetch(url, opts).then(res => { return res.json(); }).then(res => { expect(res.headers['host']).to.equal('example.com'); }); }); it('should follow redirect code 301', function() { const url = `${base}redirect/301`; return fetch(url).then(res => { expect(res.url).to.equal(`${base}inspect`); expect(res.status).to.equal(200); expect(res.ok).to.be.true; }); }); it('should follow redirect code 302', function() { const url = `${base}redirect/302`; return fetch(url).then(res => { expect(res.url).to.equal(`${base}inspect`); expect(res.status).to.equal(200); }); }); it('should follow redirect code 303', function() { const url = `${base}redirect/303`; return fetch(url).then(res => { expect(res.url).to.equal(`${base}inspect`); expect(res.status).to.equal(200); }); }); it('should follow redirect code 307', function() { const url = `${base}redirect/307`; return fetch(url).then(res => { expect(res.url).to.equal(`${base}inspect`); expect(res.status).to.equal(200); }); }); it('should follow redirect code 308', function() { const url = `${base}redirect/308`; return fetch(url).then(res => { expect(res.url).to.equal(`${base}inspect`); expect(res.status).to.equal(200); }); }); it('should follow redirect chain', function() { const url = `${base}redirect/chain`; return fetch(url).then(res => { expect(res.url).to.equal(`${base}inspect`); expect(res.status).to.equal(200); }); }); it('should follow POST request redirect code 301 with GET', function() { const url = `${base}redirect/301`; const opts = { method: 'POST', body: 'a=1' }; return fetch(url, opts).then(res => { expect(res.url).to.equal(`${base}inspect`); expect(res.status).to.equal(200); return res.json().then(result => { expect(result.method).to.equal('GET'); expect(result.body).to.equal(''); }); }); }); it('should follow PATCH request redirect code 301 with PATCH', function() { const url = `${base}redirect/301`; const opts = { method: 'PATCH', body: 'a=1' }; return fetch(url, opts).then(res => { expect(res.url).to.equal(`${base}inspect`); expect(res.status).to.equal(200); return res.json().then(res => { expect(res.method).to.equal('PATCH'); expect(res.body).to.equal('a=1'); }); }); }); it('should follow POST request redirect code 302 with GET', function() { const url = `${base}redirect/302`; const opts = { method: 'POST', body: 'a=1' }; return fetch(url, opts).then(res => { expect(res.url).to.equal(`${base}inspect`); expect(res.status).to.equal(200); return res.json().then(result => { expect(result.method).to.equal('GET'); expect(result.body).to.equal(''); }); }); }); it('should follow PATCH request redirect code 302 with PATCH', function() { const url = `${base}redirect/302`; const opts = { method: 'PATCH', body: 'a=1' }; return fetch(url, opts).then(res => { expect(res.url).to.equal(`${base}inspect`); expect(res.status).to.equal(200); return res.json().then(res => { expect(res.method).to.equal('PATCH'); expect(res.body).to.equal('a=1'); }); }); }); it('should follow redirect code 303 with GET', function() { const url = `${base}redirect/303`; const opts = { method: 'PUT', body: 'a=1' }; return fetch(url, opts).then(res => { expect(res.url).to.equal(`${base}inspect`); expect(res.status).to.equal(200); return res.json().then(result => { expect(result.method).to.equal('GET'); expect(result.body).to.equal(''); }); }); }); it('should follow PATCH request redirect code 307 with PATCH', function() { const url = `${base}redirect/307`; const opts = { method: 'PATCH', body: 'a=1' }; return fetch(url, opts).then(res => { expect(res.url).to.equal(`${base}inspect`); expect(res.status).to.equal(200); return res.json().then(result => { expect(result.method).to.equal('PATCH'); expect(result.body).to.equal('a=1'); }); }); }); it('should not follow non-GET redirect if body is a readable stream', function() { const url = `${base}redirect/307`; const opts = { method: 'PATCH', body: resumer().queue('a=1').end() }; return expect(fetch(url, opts)).to.eventually.be.rejected .and.be.an.instanceOf(FetchError) .and.have.property('type', 'unsupported-redirect'); }); it('should obey maximum redirect, reject case', function() { const url = `${base}redirect/chain`; const opts = { follow: 1 } return expect(fetch(url, opts)).to.eventually.be.rejected .and.be.an.instanceOf(FetchError) .and.have.property('type', 'max-redirect'); }); it('should obey redirect chain, resolve case', function() { const url = `${base}redirect/chain`; const opts = { follow: 2 } return fetch(url, opts).then(res => { expect(res.url).to.equal(`${base}inspect`); expect(res.status).to.equal(200); }); }); it('should allow not following redirect', function() { const url = `${base}redirect/301`; const opts = { follow: 0 } return expect(fetch(url, opts)).to.eventually.be.rejected .and.be.an.instanceOf(FetchError) .and.have.property('type', 'max-redirect'); }); it('should support redirect mode, manual flag', function() { const url = `${base}redirect/301`; const opts = { redirect: 'manual' }; return fetch(url, opts).then(res => { expect(res.url).to.equal(url); expect(res.status).to.equal(301); expect(res.headers.get('location')).to.equal(`${base}inspect`); }); }); it('should support redirect mode, error flag', function() { const url = `${base}redirect/301`; const opts = { redirect: 'error' }; return expect(fetch(url, opts)).to.eventually.be.rejected .and.be.an.instanceOf(FetchError) .and.have.property('type', 'no-redirect'); }); it('should support redirect mode, manual flag when there is no redirect', function() { const url = `${base}hello`; const opts = { redirect: 'manual' }; return fetch(url, opts).then(res => { expect(res.url).to.equal(url); expect(res.status).to.equal(200); expect(res.headers.get('location')).to.be.null; }); }); it('should follow redirect code 301 and keep existing headers', function() { const url = `${base}redirect/301`; const opts = { headers: new Headers({ 'x-custom-header': 'abc' }) }; return fetch(url, opts).then(res => { expect(res.url).to.equal(`${base}inspect`); return res.json(); }).then(res => { expect(res.headers['x-custom-header']).to.equal('abc'); }); }); it('should treat broken redirect as ordinary response (follow)', function() { const url = `${base}redirect/no-location`; return fetch(url).then(res => { expect(res.url).to.equal(url); expect(res.status).to.equal(301); expect(res.headers.get('location')).to.be.null; }); }); it('should treat broken redirect as ordinary response (manual)', function() { const url = `${base}redirect/no-location`; const opts = { redirect: 'manual' }; return fetch(url, opts).then(res => { expect(res.url).to.equal(url); expect(res.status).to.equal(301); expect(res.headers.get('location')).to.be.null; }); }); it('should set redirected property on response when redirect', function() { 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`; return fetch(url).then(res => { expect(res.redirected).to.be.false; }); }); it('should ignore invalid headers', function() { var headers = { 'Invalid-Header ': 'abc\r\n', 'Invalid-Header-Value': '\x07k\r\n', 'Set-Cookie': ['\x07k\r\n', '\x07kk\r\n'] }; headers = createHeadersLenient(headers); expect(headers).to.not.have.property('Invalid-Header '); expect(headers).to.not.have.property('Invalid-Header-Value'); expect(headers).to.not.have.property('Set-Cookie'); }); it('should handle client-error response', function() { const url = `${base}error/400`; return fetch(url).then(res => { expect(res.headers.get('content-type')).to.equal('text/plain'); expect(res.status).to.equal(400); expect(res.statusText).to.equal('Bad Request'); expect(res.ok).to.be.false; return res.text().then(result => { expect(res.bodyUsed).to.be.true; expect(result).to.be.a('string'); expect(result).to.equal('client error'); }); }); }); it('should handle server-error response', function() { const url = `${base}error/500`; return fetch(url).then(res => { expect(res.headers.get('content-type')).to.equal('text/plain'); expect(res.status).to.equal(500); expect(res.statusText).to.equal('Internal Server Error'); expect(res.ok).to.be.false; return res.text().then(result => { expect(res.bodyUsed).to.be.true; expect(result).to.be.a('string'); expect(result).to.equal('server error'); }); }); }); it('should handle network-error response', function() { 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() { 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() { 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' }); }); }); it('should handle no content response', function() { 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 res.text().then(result => { expect(result).to.be.a('string'); expect(result).to.be.empty; }); }); }); it('should reject when trying to parse no content response as json', function() { 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' }); }); }); it('should handle no content response with gzip encoding', function() { const url = `${base}no-content/gzip`; return fetch(url).then(res => { expect(res.status).to.equal(204); expect(res.statusText).to.equal('No Content'); expect(res.headers.get('content-encoding')).to.equal('gzip'); expect(res.ok).to.be.true; return res.text().then(result => { expect(result).to.be.a('string'); expect(result).to.be.empty; }); }); }); it('should handle not modified response', function() { const url = `${base}not-modified`; return fetch(url).then(res => { expect(res.status).to.equal(304); expect(res.statusText).to.equal('Not Modified'); expect(res.ok).to.be.false; return res.text().then(result => { expect(result).to.be.a('string'); expect(result).to.be.empty; }); }); }); it('should handle not modified response with gzip encoding', function() { const url = `${base}not-modified/gzip`; return fetch(url).then(res => { expect(res.status).to.equal(304); expect(res.statusText).to.equal('Not Modified'); expect(res.headers.get('content-encoding')).to.equal('gzip'); expect(res.ok).to.be.false; return res.text().then(result => { expect(result).to.be.a('string'); expect(result).to.be.empty; }); }); }); it('should decompress gzip response', function() { const url = `${base}gzip`; return fetch(url).then(res => { expect(res.headers.get('content-type')).to.equal('text/plain'); return res.text().then(result => { expect(result).to.be.a('string'); expect(result).to.equal('hello world'); }); }); }); it('should decompress slightly invalid gzip response', function() { const url = `${base}gzip-truncated`; return fetch(url).then(res => { expect(res.headers.get('content-type')).to.equal('text/plain'); return res.text().then(result => { expect(result).to.be.a('string'); expect(result).to.equal('hello world'); }); }); }); it('should decompress deflate response', function() { const url = `${base}deflate`; return fetch(url).then(res => { expect(res.headers.get('content-type')).to.equal('text/plain'); return res.text().then(result => { expect(result).to.be.a('string'); expect(result).to.equal('hello world'); }); }); }); it('should decompress deflate raw response from old apache server', function() { const url = `${base}deflate-raw`; return fetch(url).then(res => { expect(res.headers.get('content-type')).to.equal('text/plain'); return res.text().then(result => { expect(result).to.be.a('string'); expect(result).to.equal('hello world'); }); }); }); 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'); return res.text().then(result => { expect(result).to.be.a('string'); expect(result).to.equal('hello world'); }); }); }); 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); expect(res.statusText).to.equal('No Content'); expect(res.headers.get('content-encoding')).to.equal('br'); expect(res.ok).to.be.true; return res.text().then(result => { expect(result).to.be.a('string'); expect(result).to.be.empty; }); }); }); it('should skip decompression if unsupported', function() { const url = `${base}sdch`; return fetch(url).then(res => { expect(res.headers.get('content-type')).to.equal('text/plain'); return res.text().then(result => { expect(result).to.be.a('string'); expect(result).to.equal('fake sdch string'); }); }); }); it('should reject if response compression is invalid', function() { const url = `${base}invalid-content-encoding`; return fetch(url).then(res => { expect(res.headers.get('content-type')).to.equal('text/plain'); return expect(res.text()).to.eventually.be.rejected .and.be.an.instanceOf(FetchError) .and.have.property('code', 'Z_DATA_ERROR'); }); }); it('should handle errors on the body stream even if it is not used', function(done) { const url = `${base}invalid-content-encoding`; fetch(url) .then(res => { expect(res.status).to.equal(200); }) .catch(() => {}) .then(() => { // Wait a few ms to see if a uncaught error occurs setTimeout(() => { done(); }, 20); }); }); it('should collect handled errors on the body stream to reject if the body is used later', function() { function delay(value) { return new Promise((resolve) => { setTimeout(() => { resolve(value) }, 20); }); } const url = `${base}invalid-content-encoding`; return fetch(url).then(delay).then(res => { expect(res.headers.get('content-type')).to.equal('text/plain'); return expect(res.text()).to.eventually.be.rejected .and.be.an.instanceOf(FetchError) .and.have.property('code', 'Z_DATA_ERROR'); }); }); it('should allow disabling auto decompression', function() { const url = `${base}gzip`; const opts = { compress: false }; return fetch(url, opts).then(res => { expect(res.headers.get('content-type')).to.equal('text/plain'); return res.text().then(result => { expect(result).to.be.a('string'); expect(result).to.not.equal('hello world'); }); }); }); it('should not overwrite existing accept-encoding header when auto decompression is true', function() { const url = `${base}inspect`; const opts = { compress: true, headers: { 'Accept-Encoding': 'gzip' } }; return fetch(url, opts).then(res => res.json()).then(res => { expect(res.headers['accept-encoding']).to.equal('gzip'); }); }); it('should allow custom timeout', function() { const url = `${base}timeout`; const opts = { timeout: 20 }; return expect(fetch(url, opts)).to.eventually.be.rejected .and.be.an.instanceOf(FetchError) .and.have.property('type', 'request-timeout'); }); it('should allow custom timeout on response body', function() { const url = `${base}slow`; const opts = { timeout: 20 }; return fetch(url, opts).then(res => { expect(res.ok).to.be.true; return expect(res.text()).to.eventually.be.rejected .and.be.an.instanceOf(FetchError) .and.have.property('type', 'body-timeout'); }); }); it('should allow custom timeout on redirected requests', function() { const url = `${base}redirect/slow-chain`; const opts = { timeout: 20 }; return expect(fetch(url, opts)).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 })`]) .on('exit', () => { done(); }); }); it('should clear internal timeout on fetch redirect', function (done) { this.timeout(2000); spawn('node', ['-e', `require('./')('${base}redirect/301', { timeout: 10000 })`]) .on('exit', () => { done(); }); }); it('should clear internal timeout on fetch error', function (done) { this.timeout(2000); spawn('node', ['-e', `require('./')('${base}error/reset', { timeout: 10000 })`]) .on('exit', () => { done(); }); }); it('should support request cancellation with signal', function () { this.timeout(500); const controller = new AbortController(); const controller2 = new AbortController2(); const fetches = [ fetch(`${base}timeout`, { signal: controller.signal }), fetch(`${base}timeout`, { signal: controller2.signal }), fetch( `${base}timeout`, { method: 'POST', signal: controller.signal, headers: { 'Content-Type': 'application/json', body: JSON.stringify({ hello: 'world' }) } } ) ]; setTimeout(() => { controller.abort(); controller2.abort(); }, 100); return Promise.all(fetches.map(fetched => expect(fetched) .to.eventually.be.rejected .and.be.an.instanceOf(Error) .and.include({ type: 'aborted', name: 'AbortError', }) )); }); it('should reject immediately if signal has already been aborted', function () { const url = `${base}timeout`; const controller = new AbortController(); const opts = { signal: controller.signal }; controller.abort(); const fetched = fetch(url, opts); return expect(fetched).to.eventually.be.rejected .and.be.an.instanceOf(Error) .and.include({ type: 'aborted', name: 'AbortError', }); }); 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 controller = new AbortController(); 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 () { const controller = new AbortController(); const { signal } = controller; const promise = fetch( `${base}timeout`, { signal } ); const result = expect(promise).to.eventually.be.rejected .and.be.an.instanceof(Error) .and.have.property('name', 'AbortError') .then(() => { expect(signal.listeners.abort.length).to.equal(0); }); controller.abort(); return result; }); it('should allow redirects to be aborted', function() { const abortController = new AbortController(); const request = new Request(`${base}redirect/slow`, { signal: abortController.signal }); setTimeout(() => { abortController.abort(); }, 20); return expect(fetch(request)).to.be.eventually.rejected .and.be.an.instanceOf(Error) .and.have.property('name', 'AbortError'); }); it('should allow redirected response body to be aborted', function() { const abortController = new AbortController(); const request = new Request(`${base}redirect/slow-stream`, { signal: abortController.signal }); return expect(fetch(request).then(res => { expect(res.headers.get('content-type')).to.equal('text/plain'); const result = res.text(); abortController.abort(); return result; })).to.be.eventually.rejected .and.be.an.instanceOf(Error) .and.have.property('name', 'AbortError'); }); 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 }) .then(res => res.text()); 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, ]).then(() => { expect(signal.listeners.abort.length).to.equal(0) }); }); it('should reject response body with AbortError when aborted before stream has been read completely', () => { const controller = new AbortController(); return expect(fetch( `${base}slow`, { signal: controller.signal } )) .to.eventually.be.fulfilled .then((res) => { const promise = res.text(); controller.abort(); return expect(promise) .to.eventually.be.rejected .and.be.an.instanceof(Error) .and.have.property('name', 'AbortError'); }); }); it('should reject response body methods immediately with AbortError when aborted before stream is disturbed', () => { const controller = new AbortController(); return expect(fetch( `${base}slow`, { signal: controller.signal } )) .to.eventually.be.fulfilled .then((res) => { controller.abort(); return expect(res.text()) .to.eventually.be.rejected .and.be.an.instanceof(Error) .and.have.property('name', 'AbortError'); }); }); 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 } )) .to.eventually.be.fulfilled .then((res) => { res.body.on('error', (err) => { expect(err) .to.be.an.instanceof(Error) .and.have.property('name', 'AbortError'); done(); }); controller.abort(); }); }); (supportStreamDestroy ? it : it.skip)('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 promise = fetch( `${base}slow`, { signal: controller.signal, body, method: 'POST' } ); const result = Promise.all([ new Promise((resolve, reject) => { body.on('error', (error) => { try { expect(error).to.be.an.instanceof(Error).and.have.property('name', 'AbortError') resolve(); } catch (err) { reject(err); } }); }), expect(promise).to.eventually.be.rejected .and.be.an.instanceof(Error) .and.have.property('name', 'AbortError') ]); controller.abort(); 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: {} })) .to.be.eventually.rejected .and.be.an.instanceof(TypeError) .and.have.property('message').includes('AbortSignal'), 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) })) .to.be.eventually.rejected .and.be.an.instanceof(TypeError) .and.have.property('message').includes('AbortSignal'), ]); }); it('should set default User-Agent', function () { const url = `${base}inspect`; return fetch(url).then(res => res.json()).then(res => { expect(res.headers['user-agent']).to.startWith('node-fetch/'); }); }); it('should allow setting User-Agent', function () { const url = `${base}inspect`; const opts = { headers: { 'user-agent': 'faked' } }; return fetch(url, opts).then(res => res.json()).then(res => { expect(res.headers['user-agent']).to.equal('faked'); }); }); it('should set default Accept header', function () { 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 () { const url = `${base}inspect`; const opts = { headers: { 'accept': 'application/json' } }; return fetch(url, opts).then(res => res.json()).then(res => { expect(res.headers.accept).to.equal('application/json'); }); }); it('should allow POST request', function() { const url = `${base}inspect`; const opts = { method: 'POST' }; return fetch(url, opts).then(res => { return res.json(); }).then(res => { expect(res.method).to.equal('POST'); expect(res.headers['transfer-encoding']).to.be.undefined; expect(res.headers['content-type']).to.be.undefined; expect(res.headers['content-length']).to.equal('0'); }); }); it('should allow POST request with string body', function() { const url = `${base}inspect`; const opts = { method: 'POST', body: 'a=1' }; return fetch(url, opts).then(res => { return res.json(); }).then(res => { expect(res.method).to.equal('POST'); expect(res.body).to.equal('a=1'); expect(res.headers['transfer-encoding']).to.be.undefined; expect(res.headers['content-type']).to.equal('text/plain;charset=UTF-8'); expect(res.headers['content-length']).to.equal('3'); }); }); it('should allow POST request with buffer body', function() { const url = `${base}inspect`; const opts = { method: 'POST', body: Buffer.from('a=1', 'utf-8') }; return fetch(url, opts).then(res => { return res.json(); }).then(res => { expect(res.method).to.equal('POST'); expect(res.body).to.equal('a=1'); expect(res.headers['transfer-encoding']).to.be.undefined; expect(res.headers['content-type']).to.be.undefined; expect(res.headers['content-length']).to.equal('3'); }); }); it('should allow POST request with ArrayBuffer body', function() { const url = `${base}inspect`; const opts = { method: 'POST', body: stringToArrayBuffer('Hello, world!\n') }; return fetch(url, opts).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; expect(res.headers['content-type']).to.be.undefined; expect(res.headers['content-length']).to.equal('14'); }); }); 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(); } const url = `${base}inspect`; const opts = { method: 'POST', body: new VMUint8Array(Buffer.from('Hello, world!\n')).buffer }; return fetch(url, opts).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; expect(res.headers['content-type']).to.be.undefined; expect(res.headers['content-length']).to.equal('14'); }); }); it('should allow POST request with ArrayBufferView (Uint8Array) body', function() { const url = `${base}inspect`; const opts = { method: 'POST', body: new Uint8Array(stringToArrayBuffer('Hello, world!\n')) }; return fetch(url, opts).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; expect(res.headers['content-type']).to.be.undefined; expect(res.headers['content-length']).to.equal('14'); }); }); it('should allow POST request with ArrayBufferView (DataView) body', function() { const url = `${base}inspect`; const opts = { method: 'POST', body: new DataView(stringToArrayBuffer('Hello, world!\n')) }; return fetch(url, opts).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; expect(res.headers['content-type']).to.be.undefined; expect(res.headers['content-length']).to.equal('14'); }); }); 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(); } const url = `${base}inspect`; const opts = { method: 'POST', body: new VMUint8Array(Buffer.from('Hello, world!\n')) }; return fetch(url, opts).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; expect(res.headers['content-type']).to.be.undefined; expect(res.headers['content-length']).to.equal('14'); }); }); // 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() { const url = `${base}inspect`; const opts = { method: 'POST', body: new Uint8Array(stringToArrayBuffer('Hello, world!\n'), 7, 6) }; return fetch(url, opts).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; expect(res.headers['content-type']).to.be.undefined; expect(res.headers['content-length']).to.equal('6'); }); }); it('should allow POST request with blob body without type', function() { const url = `${base}inspect`; const opts = { method: 'POST', body: new Blob(['a=1']) }; return fetch(url, opts).then(res => { return res.json(); }).then(res => { expect(res.method).to.equal('POST'); expect(res.body).to.equal('a=1'); expect(res.headers['transfer-encoding']).to.be.undefined; expect(res.headers['content-type']).to.be.undefined; expect(res.headers['content-length']).to.equal('3'); }); }); it('should allow POST request with blob body with type', function() { const url = `${base}inspect`; const opts = { method: 'POST', body: new Blob(['a=1'], { type: 'text/plain;charset=UTF-8' }) }; return fetch(url, opts).then(res => { return res.json(); }).then(res => { expect(res.method).to.equal('POST'); expect(res.body).to.equal('a=1'); expect(res.headers['transfer-encoding']).to.be.undefined; expect(res.headers['content-type']).to.equal('text/plain;charset=utf-8'); expect(res.headers['content-length']).to.equal('3'); }); }); it('should allow POST request with readable stream as body', function() { let body = resumer().queue('a=1').end(); body = body.pipe(new stream.PassThrough()); const url = `${base}inspect`; const opts = { method: 'POST', body }; return fetch(url, opts).then(res => { return res.json(); }).then(res => { expect(res.method).to.equal('POST'); expect(res.body).to.equal('a=1'); expect(res.headers['transfer-encoding']).to.equal('chunked'); expect(res.headers['content-type']).to.be.undefined; expect(res.headers['content-length']).to.be.undefined; }); }); it('should allow POST request with form-data as body', function() { const form = new FormData(); form.append('a','1'); const url = `${base}multipart`; const opts = { method: 'POST', body: form }; return fetch(url, opts).then(res => { return res.json(); }).then(res => { expect(res.method).to.equal('POST'); expect(res.headers['content-type']).to.startWith('multipart/form-data;boundary='); expect(res.headers['content-length']).to.be.a('string'); expect(res.body).to.equal('a=1'); }); }); it('should allow POST request with form-data using stream as body', function() { const form = new FormData(); form.append('my_field', fs.createReadStream(path.join(__dirname, 'dummy.txt'))); const url = `${base}multipart`; const opts = { method: 'POST', body: form }; return fetch(url, opts).then(res => { return res.json(); }).then(res => { expect(res.method).to.equal('POST'); expect(res.headers['content-type']).to.startWith('multipart/form-data;boundary='); expect(res.headers['content-length']).to.be.undefined; expect(res.body).to.contain('my_field='); }); }); it('should allow POST request with form-data as body and custom headers', function() { const form = new FormData(); form.append('a','1'); const headers = form.getHeaders(); headers['b'] = '2'; const url = `${base}multipart`; const opts = { method: 'POST', body: form, headers }; return fetch(url, opts).then(res => { return res.json(); }).then(res => { expect(res.method).to.equal('POST'); expect(res.headers['content-type']).to.startWith('multipart/form-data; boundary='); expect(res.headers['content-length']).to.be.a('string'); expect(res.headers.b).to.equal('2'); expect(res.body).to.equal('a=1'); }); }); it('should allow POST request with object body', function() { const url = `${base}inspect`; // note that fetch simply calls tostring on an object const opts = { method: 'POST', body: { a: 1 } }; return fetch(url, opts).then(res => { return res.json(); }).then(res => { expect(res.method).to.equal('POST'); expect(res.body).to.equal('[object Object]'); expect(res.headers['content-type']).to.equal('text/plain;charset=UTF-8'); expect(res.headers['content-length']).to.equal('15'); }); }); 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); 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'); }); 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 => { 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 => { expect(text).to.equal(''); }); }); itUSP('should allow POST request with URLSearchParams as body', function() { const params = new URLSearchParams(); params.append('a','1'); const url = `${base}inspect`; const opts = { method: 'POST', body: params, }; return fetch(url, opts).then(res => { return res.json(); }).then(res => { expect(res.method).to.equal('POST'); expect(res.headers['content-type']).to.equal('application/x-www-form-urlencoded;charset=UTF-8'); expect(res.headers['content-length']).to.equal('3'); expect(res.body).to.equal('a=1'); }); }); itUSP('should still recognize URLSearchParams when extended', function() { class CustomSearchParams extends URLSearchParams {} const params = new CustomSearchParams(); params.append('a','1'); const url = `${base}inspect`; const opts = { method: 'POST', body: params, }; return fetch(url, opts).then(res => { return res.json(); }).then(res => { expect(res.method).to.equal('POST'); expect(res.headers['content-type']).to.equal('application/x-www-form-urlencoded;charset=UTF-8'); expect(res.headers['content-length']).to.equal('3'); expect(res.body).to.equal('a=1'); }); }); /* 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'); const url = `${base}inspect`; const opts = { method: 'POST', body: params, }; return fetch(url, opts).then(res => { return res.json(); }).then(res => { expect(res.method).to.equal('POST'); expect(res.headers['content-type']).to.equal('application/x-www-form-urlencoded;charset=UTF-8'); expect(res.headers['content-length']).to.equal('3'); expect(res.body).to.equal('a=1'); }); }); it('should overwrite Content-Length if possible', function() { const url = `${base}inspect`; // note that fetch simply calls tostring on an object const opts = { method: 'POST', headers: { 'Content-Length': '1000' }, body: 'a=1' }; return fetch(url, opts).then(res => { return res.json(); }).then(res => { expect(res.method).to.equal('POST'); expect(res.body).to.equal('a=1'); expect(res.headers['transfer-encoding']).to.be.undefined; expect(res.headers['content-type']).to.equal('text/plain;charset=UTF-8'); expect(res.headers['content-length']).to.equal('3'); }); }); it('should allow PUT request', function() { const url = `${base}inspect`; const opts = { method: 'PUT', body: 'a=1' }; return fetch(url, opts).then(res => { return res.json(); }).then(res => { expect(res.method).to.equal('PUT'); expect(res.body).to.equal('a=1'); }); }); it('should allow DELETE request', function() { const url = `${base}inspect`; const opts = { method: 'DELETE' }; return fetch(url, opts).then(res => { return res.json(); }).then(res => { expect(res.method).to.equal('DELETE'); }); }); it('should allow DELETE request with string body', function() { const url = `${base}inspect`; const opts = { method: 'DELETE', body: 'a=1' }; return fetch(url, opts).then(res => { return res.json(); }).then(res => { expect(res.method).to.equal('DELETE'); expect(res.body).to.equal('a=1'); expect(res.headers['transfer-encoding']).to.be.undefined; expect(res.headers['content-length']).to.equal('3'); }); }); it('should not forward secure headers to 3th party', () => { return fetch(`${base}redirect-to/302/https://httpbin.org/get`, { headers: new Headers({ cookie: 'gets=removed', cookie2: 'gets=removed', authorization: 'gets=removed', 'www-authenticate': 'gets=removed', 'other-safe-headers': 'stays', 'x-foo': 'bar' }) }).then(res => res.json()).then(json => { const headers = new Headers(json.headers); // Safe headers are not removed expect(headers.get('other-safe-headers')).to.equal('stays'); expect(headers.get('x-foo')).to.equal('bar'); // Unsafe headers should not have been sent to httpbin expect(headers.get('cookie')).to.equal(null); expect(headers.get('cookie2')).to.equal(null); expect(headers.get('www-authenticate')).to.equal(null); expect(headers.get('authorization')).to.equal(null); }); }); it('should forward secure headers to same host', () => { return fetch(`${base}redirect-to/302/${base}inspect`, { headers: new Headers({ cookie: 'is=cookie', cookie2: 'is=cookie2', authorization: 'is=authorization', 'other-safe-headers': 'stays', 'www-authenticate': 'is=www-authenticate', 'x-foo': 'bar' }) }).then(res => res.json().then(json => { const headers = new Headers(json.headers); // Safe headers are not removed expect(res.url).to.equal(`${base}inspect`); expect(headers.get('other-safe-headers')).to.equal('stays'); expect(headers.get('x-foo')).to.equal('bar'); // Unsafe headers should not have been sent to httpbin expect(headers.get('cookie')).to.equal('is=cookie'); expect(headers.get('cookie2')).to.equal('is=cookie2'); expect(headers.get('www-authenticate')).to.equal('is=www-authenticate'); expect(headers.get('authorization')).to.equal('is=authorization'); })); }); it('should allow PATCH request', function() { const url = `${base}inspect`; const opts = { method: 'PATCH', body: 'a=1' }; return fetch(url, opts).then(res => { return res.json(); }).then(res => { expect(res.method).to.equal('PATCH'); expect(res.body).to.equal('a=1'); }); }); it('should allow HEAD request', function() { const url = `${base}hello`; const opts = { method: 'HEAD' }; return fetch(url, opts).then(res => { expect(res.status).to.equal(200); expect(res.statusText).to.equal('OK'); expect(res.headers.get('content-type')).to.equal('text/plain'); expect(res.body).to.be.an.instanceof(stream.Transform); return res.text(); }).then(text => { expect(text).to.equal(''); }); }); it('should allow HEAD request with content-encoding header', function() { const url = `${base}error/404`; const opts = { method: 'HEAD' }; return fetch(url, opts).then(res => { expect(res.status).to.equal(404); expect(res.headers.get('content-encoding')).to.equal('gzip'); return res.text(); }).then(text => { expect(text).to.equal(''); }); }); it('should allow OPTIONS request', function() { const url = `${base}options`; const opts = { method: 'OPTIONS' }; return fetch(url, opts).then(res => { expect(res.status).to.equal(200); expect(res.statusText).to.equal('OK'); expect(res.headers.get('allow')).to.equal('GET, HEAD, OPTIONS'); expect(res.body).to.be.an.instanceof(stream.Transform); }); }); it('should reject decoding body twice', function() { const url = `${base}plain`; return fetch(url).then(res => { expect(res.headers.get('content-type')).to.equal('text/plain'); return res.text().then(result => { expect(res.bodyUsed).to.be.true; return expect(res.text()).to.eventually.be.rejectedWith(Error); }); }); }); it('should support maximum response size, multiple chunk', function() { const url = `${base}size/chunk`; const opts = { size: 5 }; return fetch(url, opts).then(res => { expect(res.status).to.equal(200); expect(res.headers.get('content-type')).to.equal('text/plain'); return expect(res.text()).to.eventually.be.rejected .and.be.an.instanceOf(FetchError) .and.have.property('type', 'max-size'); }); }); it('should support maximum response size, single chunk', function() { const url = `${base}size/long`; const opts = { size: 5 }; return fetch(url, opts).then(res => { expect(res.status).to.equal(200); expect(res.headers.get('content-type')).to.equal('text/plain'); return expect(res.text()).to.eventually.be.rejected .and.be.an.instanceOf(FetchError) .and.have.property('type', 'max-size'); }); }); it('should allow piping response body as stream', function() { const url = `${base}hello`; return fetch(url).then(res => { expect(res.body).to.be.an.instanceof(stream.Transform); return streamToPromise(res.body, chunk => { if (chunk === null) { return; } expect(chunk.toString()).to.equal('world'); }); }); }); it('should allow cloning a response, and use both as stream', function() { const url = `${base}hello`; return fetch(url).then(res => { const r1 = res.clone(); expect(res.body).to.be.an.instanceof(stream.Transform); expect(r1.body).to.be.an.instanceof(stream.Transform); const dataHandler = chunk => { if (chunk === null) { return; } expect(chunk.toString()).to.equal('world'); }; return Promise.all([ streamToPromise(res.body, dataHandler), streamToPromise(r1.body, dataHandler) ]); }); }); it('should allow cloning a json response and log it as text response', function() { const url = `${base}json`; return fetch(url).then(res => { const r1 = res.clone(); return Promise.all([res.json(), r1.text()]).then(results => { expect(results[0]).to.deep.equal({name: 'value'}); expect(results[1]).to.equal('{"name":"value"}'); }); }); }); it('should allow cloning a json response, and then log it as text response', function() { const url = `${base}json`; return fetch(url).then(res => { const r1 = res.clone(); return res.json().then(result => { expect(result).to.deep.equal({name: 'value'}); return r1.text().then(result => { expect(result).to.equal('{"name":"value"}'); }); }); }); }); it('should allow cloning a json response, first log as text response, then return json object', function() { const url = `${base}json`; return fetch(url).then(res => { const r1 = res.clone(); return r1.text().then(result => { expect(result).to.equal('{"name":"value"}'); return res.json().then(result => { expect(result).to.deep.equal({name: 'value'}); }); }); }); }); it('should not allow cloning a response after its been used', function() { const url = `${base}hello`; return fetch(url).then(res => res.text().then(result => { expect(() => { res.clone(); }).to.throw(Error); }) ); }); it('should allow get all responses of a header', function() { const url = `${base}cookie`; return fetch(url).then(res => { const expected = 'a=1, b=1'; expect(res.headers.get('set-cookie')).to.equal(expected); expect(res.headers.get('Set-Cookie')).to.equal(expected); }); }); it('should return all headers using raw()', function() { const url = `${base}cookie`; return fetch(url).then(res => { const expected = [ 'a=1', 'b=1' ]; expect(res.headers.raw()['set-cookie']).to.deep.equal(expected); }); }); it('should allow deleting header', function() { const url = `${base}cookie`; return fetch(url).then(res => { res.headers.delete('set-cookie'); expect(res.headers.get('set-cookie')).to.be.null; }); }); it('should send request with connection keep-alive if agent is provided', function() { const url = `${base}inspect`; const opts = { agent: new http.Agent({ keepAlive: true }) }; return fetch(url, opts).then(res => { return res.json(); }).then(res => { expect(res.headers['connection']).to.equal('keep-alive'); }); }); it('should support fetch with Request instance', function() { const url = `${base}hello`; const req = new Request(url); return fetch(req).then(res => { expect(res.url).to.equal(url); expect(res.ok).to.be.true; expect(res.status).to.equal(200); }); }); it('should support fetch with Node.js URL object', function() { const url = `${base}hello`; const urlObj = parseURL(url); const req = new Request(urlObj); return fetch(req).then(res => { expect(res.url).to.equal(url); expect(res.ok).to.be.true; expect(res.status).to.equal(200); }); }); it('should support fetch with WHATWG URL object', function() { const url = `${base}hello`; const urlObj = new URL(url); const req = new Request(urlObj); return fetch(req).then(res => { expect(res.url).to.equal(url); expect(res.ok).to.be.true; expect(res.status).to.equal(200); }); }); it('should support reading blob as text', function() { return new Response(`hello`) .blob() .then(blob => blob.text()) .then(body => { expect(body).to.equal('hello'); }); }); it('should support reading blob as arrayBuffer', function() { 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'); }); }); it('should support reading blob as stream', function() { return new Response(`hello`) .blob() .then(blob => streamToPromise(blob.stream(), data => { const str = data.toString(); expect(str).to.equal('hello'); })); }); it('should support blob round-trip', function() { const url = `${base}hello`; let length, type; return fetch(url).then(res => res.blob()).then(blob => { const url = `${base}inspect`; length = blob.size; type = blob.type; return fetch(url, { method: 'POST', body: blob }); }).then(res => res.json()).then(({body, headers}) => { expect(body).to.equal('world'); expect(headers['content-type']).to.equal(type); expect(headers['content-length']).to.equal(String(length)); }); }); it('should support overwrite Request instance', function() { const url = `${base}inspect`; const req = new Request(url, { method: 'POST', headers: { a: '1' } }); return fetch(req, { method: 'GET', headers: { a: '2' } }).then(res => { return res.json(); }).then(body => { expect(body.method).to.equal('GET'); expect(body.headers.a).to.equal('2'); }); }); it('should support arrayBuffer(), blob(), text(), json() and buffer() method in Body constructor', function() { const body = new Body('a=1'); expect(body).to.have.property('arrayBuffer'); expect(body).to.have.property('blob'); expect(body).to.have.property('text'); expect(body).to.have.property('json'); expect(body).to.have.property('buffer'); }); it('should create custom FetchError', function funcName() { const systemError = new Error('system'); systemError.code = 'ESOMEERROR'; const err = new FetchError('test message', 'test-error', systemError); expect(err).to.be.an.instanceof(Error); expect(err).to.be.an.instanceof(FetchError); expect(err.name).to.equal('FetchError'); expect(err.message).to.equal('test message'); expect(err.type).to.equal('test-error'); expect(err.code).to.equal('ESOMEERROR'); expect(err.errno).to.equal('ESOMEERROR'); // 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() { this.timeout(5000); const url = 'https://github.com/'; const opts = { method: 'HEAD' }; return fetch(url, opts).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 () { 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 textPromise = res.text(); // Ensure that `Buffer.concat` is always restored: textPromise.then(restoreBufferConcat, restoreBufferConcat); return expect(textPromise).to.eventually.be.rejected .and.be.an.instanceOf(FetchError) .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() { 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(() => { expect(called).to.equal(2); }); }); it("supports supplying a famliy option to the agent", function() { const url = `${base}redirect/301`; const families = []; const family = Symbol('family'); function lookupSpy(hostname, options, callback) { families.push(options.family) return lookup(hostname, {}, callback); } 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() { const url = `${base}inspect`; const agent = new http.Agent({ keepAlive: true }); let parsedURL; return fetch(url, { agent: function(_parsedURL) { parsedURL = _parsedURL; return agent; } }).then(res => { return res.json(); }).then(res => { // 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'); }); }); it('should calculate content length and extract content type for each body type', function () { const url = `${base}hello`; const bodyContent = 'a=1'; let streamBody = resumer().queue(bodyContent).end(); streamBody = streamBody.pipe(new stream.PassThrough()); const streamRequest = new Request(url, { method: 'POST', body: streamBody, size: 1024 }); let blobBody = new Blob([bodyContent], { type: 'text/plain' }); const blobRequest = new Request(url, { method: 'POST', body: blobBody, size: 1024 }); let formBody = new FormData(); formBody.append('a', '1'); const formRequest = new Request(url, { method: 'POST', body: formBody, size: 1024 }); let bufferBody = Buffer.from(bodyContent); const bufferRequest = new Request(url, { method: 'POST', body: bufferBody, size: 1024 }); const stringRequest = new Request(url, { method: 'POST', body: bodyContent, size: 1024 }); const nullRequest = new Request(url, { method: 'GET', body: null, size: 1024 }); expect(getTotalBytes(streamRequest)).to.be.null; expect(getTotalBytes(blobRequest)).to.equal(blobBody.size); expect(getTotalBytes(formRequest)).to.not.be.null; expect(getTotalBytes(bufferRequest)).to.equal(bufferBody.length); expect(getTotalBytes(stringRequest)).to.equal(bodyContent.length); expect(getTotalBytes(nullRequest)).to.equal(0); expect(extractContentType(streamBody)).to.be.null; expect(extractContentType(blobBody)).to.equal('text/plain'); expect(extractContentType(formBody)).to.startWith('multipart/form-data'); expect(extractContentType(bufferBody)).to.be.null; 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 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('issue #1290', function() { it('should keep query params', function() { return fetch(`${base}inspect?month=2021-09`) .then(res => res.json()) .then(json => { expect(json.url).to.equal('/inspect?month=2021-09') }) }) it('should handle escaped unicode in URLs', () => { const url = `${base}issues/1290/%E3%81%B2%E3%82%89%E3%81%8C%E3%81%AA`; return fetch(url).then((res) => { expect(res.status).to.equal(200); return res.text().then(result => { expect(result).to.equal('Success'); }); }); }); it('should handle unicode in URLs', () => { const url = `${base}issues/1290/ひらがな`; return fetch(url).then((res) => { expect(res.status).to.equal(200); return res.text().then(result => { expect(result).to.equal('Success'); }); }); }); // #1342 it('should not throw with a valid URL', () => { const url = 'https://r2---sn-n4v7sney.example.com'; new Request(url); }); });