Skip to content
Permalink

Comparing changes

Choose two branches to see what’s changed or to start a new pull request. If you need to, you can also or learn more about diff comparisons.

Open a pull request

Create a new pull request by comparing changes across two branches. If you need to, you can also . Learn more about diff comparisons here.
base repository: node-fetch/node-fetch
Failed to load repositories. Confirm that selected base ref is valid, then try again.
Loading
base: 5367fe6a978e01745e4264384a91140dc99a4bf8
Choose a base ref
...
head repository: node-fetch/node-fetch
Failed to load repositories. Confirm that selected head ref is valid, then try again.
Loading
compare: 1ef4b560a17e644a02a3bfdea7631ffeee578b35
Choose a head ref

Commits on Nov 13, 2018

  1. Clone URLSearchParams to avoid mutation (#547)

    * And make sure Request/Response set Content-Type per Fetch Spec
    * And make sure users can read the body as string via text()
    jimmywarting authored and bitinn committed Nov 13, 2018

    Verified

    This commit was created on GitHub.com and signed with GitHub’s verified signature.
    Copy the full SHA
    2d0fc68 View commit details

Commits on Nov 15, 2018

  1. Fix spelling mistake (#551)

    puckey authored and bitinn committed Nov 15, 2018

    Verified

    This commit was created on GitHub.com and signed with GitHub’s verified signature.
    Copy the full SHA
    35a4abe View commit details
  2. Unify internal body as buffer (#552)

    jimmywarting authored and bitinn committed Nov 15, 2018
    Copy the full SHA
    7d32932 View commit details

Commits on Dec 29, 2018

  1. Copy the full SHA
    1c2f07f View commit details

Commits on Jan 16, 2019

  1. Quick readme update

    bitinn authored Jan 16, 2019
    Copy the full SHA
    e996bda View commit details

Commits on Apr 15, 2019

  1. Copy the full SHA
    bee2ad8 View commit details
  2. Copy the full SHA
    0ad136d View commit details

Commits on Apr 16, 2019

  1. Copy the full SHA
    432c9b0 View commit details

Commits on Apr 26, 2019

  1. Node 12 compatibility (#614)

    * dev package bump
    * test invalid header differently as node 12 no longer accepts invalid headers in response
    * add node v10 in travis test list as node 12 has been released
    bitinn authored Apr 26, 2019
    Copy the full SHA
    05f5ac1 View commit details
  2. Adding Brotli Support (#598)

    * adding brotli support
    * support old node versions
    * better test
    Muhammet Öztürk authored and bitinn committed Apr 26, 2019
    Copy the full SHA
    2a2d438 View commit details
  3. Copy the full SHA
    cfc8e5b View commit details
  4. Copy the full SHA
    49d7760 View commit details
  5. 2.4.0 release (#616)

    * changelog update
    * package.json update
    bitinn authored Apr 26, 2019
    Copy the full SHA
    c9805a2 View commit details

Commits on Apr 27, 2019

  1. Fix Blob for older node versions and webpack. (#618)

    `Readable` isn't a named export
    mcuppi authored and bitinn committed Apr 27, 2019
    Copy the full SHA
    1a88481 View commit details
  2. 2.4.1 release (#619)

    * changelog update
    * package.json update
    bitinn authored Apr 27, 2019
    Copy the full SHA
    b3ecba5 View commit details

Commits on May 1, 2019

  1. Copy the full SHA
    a35dcd1 View commit details
  2. Copy the full SHA
    1fe1358 View commit details
  3. Copy the full SHA
    d8f5ba0 View commit details
  4. Allow third party blob implementation (#629)

    * Support making request with any blob that have stream() method
    * don't clone blob when cloning request
    * check for blob api that node-fetch uses
    bitinn authored May 1, 2019
    Copy the full SHA
    0fc414c View commit details
  5. 2.5.0 release (#630)

    * redirected property
    * changelog update
    * readme update
    * 2.5.0
    bitinn authored May 1, 2019
    Copy the full SHA
    0c2294e View commit details

Commits on May 5, 2019

  1. Allow agent option to be a function (#632)

    Enable users to return HTTP/HTTPS-specific agent based on request url
    edgraaff authored and bitinn committed May 5, 2019
    Copy the full SHA
    bf8b4e8 View commit details

Commits on May 16, 2019

  1. v2.6.0 (#638)

    * Update readme and changelog for `options.agent`
    - Fix content-length issue introduced in v2.5.0
    * More test coverage for `extractContentType`
    * Slightly improve test performance
    * `Response.url` should not return null
    * Document `Headers.raw()` usage better
    * 2.6.0
    bitinn authored May 16, 2019
    Copy the full SHA
    95286f5 View commit details

Commits on Aug 9, 2019

  1. Copy the full SHA
    086be6f View commit details

Commits on Sep 7, 2019

  1. feat: Data URI support (#659)

    Adds support for Data URIs using native methods in Node 5.10.0+
    Richienb authored Sep 7, 2019
    Copy the full SHA
    eb3a572 View commit details
  2. docs: Add Discord badge

    Richienb authored Sep 7, 2019
    Copy the full SHA
    1d5778a View commit details

Commits on Sep 16, 2019

  1. Copy the full SHA
    5535c2e View commit details

Commits on Oct 2, 2019

  1. chore: Add funding link

    Richienb authored Oct 2, 2019
    2
    Copy the full SHA
    7b13662 View commit details
  2. Copy the full SHA
    47a24a0 View commit details

Commits on Oct 7, 2019

  1. Copy the full SHA
    6a5d192 View commit details
  2. Copy the full SHA
    244e6f6 View commit details

Commits on Oct 10, 2019

  1. fix: Change error message thrown with redirect mode set to error (#653)

    The original error message does not provide enough information about what went wrong. It simply states a configuration setting.
    uwu-ara authored and Richienb committed Oct 10, 2019
    Copy the full SHA
    1e99050 View commit details

Commits on Oct 21, 2019

  1. Copy the full SHA
    8c197f8 View commit details

Commits on Sep 5, 2020

  1. Honor the size option after following a redirect and revert data ur…

    …i support
    
    Co-authored-by: Richie Bendall <richiebendall@gmail.com>
    xxczaki and Richienb authored Sep 5, 2020
    Copy the full SHA
    2358a6c View commit details
  2. update version number

    Antoni Kepinski committed Sep 5, 2020
    Copy the full SHA
    b5e2e41 View commit details

Commits on Sep 6, 2021

  1. Fix(package.json): Corrected main file path in package.json (#1274)

    * fix main configuration in package.json
    * pinned a breaking change in codecov & teeny-request
    jimmywarting authored Sep 6, 2021
    Copy the full SHA
    152214c View commit details

Commits on Sep 20, 2021

  1. fix: properly encode url with unicode characters (#1291)

    * fix: properly encode url with unicode characters
    * release: 2.6.3
    LinusU authored Sep 20, 2021
    3
    Copy the full SHA
    ace7536 View commit details

Commits on Sep 21, 2021

  1. Copy the full SHA
    18193c5 View commit details

Commits on Sep 22, 2021

  1. fix: import whatwg-url in a way compatible with ESM Node (#1303)

    * fix: import whatwg-url in a way compatible with ESM Node
    
    * release: 2.6.5
    LinusU authored Sep 22, 2021
    Copy the full SHA
    b5417ae View commit details

Commits on Oct 31, 2021

  1. fix(URL): prefer built in URL version when available and fallback to …

    …whatwg (#1352)
    
    * fix(URL): prefer built in URL version when available and fallback to whatwg
    
    * bump minor
    jimmywarting authored Oct 31, 2021
    Copy the full SHA
    f56b0c6 View commit details

Commits on Nov 5, 2021

  1. 2.x: Specify encoding as an optional peer dependency in package.json (#…

    …1310)
    
    * Specify `encoding` as an optional peer dependency
    
    * Update package.json
    
    Co-authored-by: Linus Unnebäck <linus@folkdatorn.se>
    
    Co-authored-by: Linus Unnebäck <linus@folkdatorn.se>
    ciffelia and LinusU authored Nov 5, 2021
    Copy the full SHA
    8fe5c4e View commit details

Commits on Jan 16, 2022

  1. backport of #1449 (#1453)

    * backport of #1449
    
    * bump patch version
    jimmywarting authored Jan 16, 2022
    1
    Copy the full SHA
    1ef4b56 View commit details
Showing with 786 additions and 245 deletions.
  1. +12 −0 .github/FUNDING.yml
  2. +3 −0 .gitignore
  3. +1 −0 .npmrc
  4. +1 −0 .travis.yml
  5. +50 −1 CHANGELOG.md
  6. +86 −29 README.md
  7. +3 −1 browser.js
  8. +74 −64 package.json
  9. +4 −5 rollup.config.js
  10. +25 −0 src/blob.js
  11. +52 −78 src/body.js
  12. +1 −1 src/headers.js
  13. +52 −10 src/index.js
  14. +36 −8 src/request.js
  15. +19 −5 src/response.js
  16. +47 −18 test/server.js
  17. +320 −25 test/test.js
12 changes: 12 additions & 0 deletions .github/FUNDING.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# 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
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -37,3 +37,6 @@ lib
# Ignore package manager lock files
package-lock.json
yarn.lock

# Ignore IDE
.idea
1 change: 1 addition & 0 deletions .npmrc
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
package-lock=false
1 change: 1 addition & 0 deletions .travis.yml
Original file line number Diff line number Diff line change
@@ -3,6 +3,7 @@ node_js:
- "4"
- "6"
- "8"
- "10"
- "node"
env:
- FORMDATA_VERSION=1.0.0
51 changes: 50 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -5,9 +5,58 @@ 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

- New: `AbortSignal` support, with README example.
- 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.

115 changes: 86 additions & 29 deletions README.md
Original file line number Diff line number Diff line change
@@ -5,10 +5,13 @@ node-fetch
[![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/252))
(We are looking for [v2 maintainers and collaborators](https://github.com/bitinn/node-fetch/issues/567))

[![Backers][opencollective-image]][opencollective-url]

<!-- TOC -->

@@ -29,6 +32,7 @@ A light-weight module that brings `window.fetch` to Node.js
- [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)
@@ -47,17 +51,17 @@ A light-weight module that brings `window.fetch` to Node.js

## Motivation

Instead of implementing `XMLHttpRequest` in Node.js to run browser-specific [Fetch polyfill](https://github.com/github/fetch), why not go from native `http` to `fetch` API directly? Hence `node-fetch`, minimal code for a `window.fetch` compatible API on Node.js runtime.
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.
- 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
@@ -71,16 +75,16 @@ See Matt Andrews' [isomorphic-fetch](https://github.com/matthew-andrews/isomorph
Current stable release (`2.x`)

```sh
$ npm install node-fetch --save
$ npm install node-fetch
```

## Loading and configuring the module
We suggest you load the module via `require`, pending the stabalizing of es modules in node:
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:
If you are using a Promise library other than native, set it through `fetch.Promise`:
```js
const Bluebird = require('bluebird');

@@ -89,7 +93,7 @@ fetch.Promise = Bluebird;

## Common Usage

NOTE: The documentation below is up-to-date with `2.x` releases, [see `1.x` readme](https://github.com/bitinn/node-fetch/blob/1.x/README.md), [changelog](https://github.com/bitinn/node-fetch/blob/1.x/CHANGELOG.md) and [2.x upgrade guide](UPGRADE-GUIDE.md) for the differences.
NOTE: The documentation below is up-to-date with `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
@@ -145,9 +149,9 @@ fetch('https://httpbin.org/post', { method: 'POST', body: params })
```

#### Handling exceptions
NOTE: 3xx-5xx responses are *NOT* exceptions, and should be handled in `then()`, see the next section.
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, like network errors, and operational errors which are instances of FetchError. See the [error handling document](ERROR-HANDLING.md) for more details.
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/')
@@ -185,7 +189,7 @@ fetch('https://assets-cdn.github.com/images/modules/logos_page/Octocat.png')
```

#### Buffer
If you prefer to cache binary data in full, use buffer(). (NOTE: buffer() is a `node-fetch` only API)
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');
@@ -208,6 +212,17 @@ fetch('https://github.com/')
});
```

#### 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
@@ -251,14 +266,14 @@ fetch('https://httpbin.org/post', options)

#### Request cancellation with AbortSignal

> NOTE: You may only cancel streamed requests on Node >= v8.0.0
> 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 follows:
An example of timing out a request after 150ms could be achieved as the following:

```js
import AbortContoller from 'abort-controller';
import AbortController from 'abort-controller';

const controller = new AbortController();
const timeout = setTimeout(
@@ -296,7 +311,7 @@ See [test cases](https://github.com/bitinn/node-fetch/blob/master/test/test.js)

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.
`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`.

<a id="fetch-options"></a>
### Options
@@ -317,7 +332,7 @@ The default values are shown after each option key.
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, allows custom proxy, certificate, dns lookup etc.
agent: null // http(s).Agent instance or function that returns an instance (see below)
}
```

@@ -334,6 +349,39 @@ Header | Value
`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;
}
}
}
```

<a id="class-request"></a>
### Class: Request

@@ -381,14 +429,13 @@ The following properties are not implemented in node-fetch at this moment:
- `Response.error()`
- `Response.redirect()`
- `type`
- `redirected`
- `trailer`

#### new Response([body[, options]])

<small>*(spec-compliant)*</small>

- `body` A string or [Readable stream][node-readable]
- `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).
@@ -401,6 +448,12 @@ Because Node.js does not implement service workers (for which this class was des

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

<small>*(spec-compliant)*</small>

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.

<a id="class-headers"></a>
### Class: Headers

@@ -412,7 +465,7 @@ This class allows manipulating and iterating over a set of HTTP headers. All met

- `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.
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
@@ -453,15 +506,15 @@ The following methods are not yet implemented in node-fetch at this moment:

* Node.js [`Readable` stream][node-readable]

The data 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].
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

<small>*(spec-compliant)*</small>

* `Boolean`

A boolean property for if this body has been consumed. Per spec, a consumed body cannot be used again.
A boolean property for if this body has been consumed. Per the specs, a consumed body cannot be used again.

#### body.arrayBuffer()
#### body.blob()
@@ -488,9 +541,9 @@ Consume the body and return a promise that will resolve to a Buffer.

* Returns: <code>Promise&lt;String&gt;</code>

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.
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 on 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.)
(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.)

<a id="class-fetcherror"></a>
### Class: FetchError
@@ -510,20 +563,24 @@ An Error thrown when the request is aborted in response to an `AbortSignal`'s `a

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 is currently maintained by [@TimothyGu](https://github.com/timothygu), v2 readme is written by [@jkantr](https://github.com/jkantr).
`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://img.shields.io/npm/v/node-fetch.svg?style=flat-square
[npm-image]: https://flat.badgen.net/npm/v/node-fetch
[npm-url]: https://www.npmjs.com/package/node-fetch
[travis-image]: https://img.shields.io/travis/bitinn/node-fetch.svg?style=flat-square
[travis-image]: https://flat.badgen.net/travis/bitinn/node-fetch
[travis-url]: https://travis-ci.org/bitinn/node-fetch
[codecov-image]: https://img.shields.io/codecov/c/github/bitinn/node-fetch.svg?style=flat-square
[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://packagephobia.now.sh/badge?p=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
4 changes: 3 additions & 1 deletion browser.js
Original file line number Diff line number Diff line change
@@ -16,7 +16,9 @@ var global = getGlobal();
module.exports = exports = global.fetch;

// Needed for TypeScript and Webpack.
exports.default = global.fetch.bind(global);
if (global.fetch) {
exports.default = global.fetch.bind(global);
}

exports.Headers = global.Headers;
exports.Request = global.Request;
138 changes: 74 additions & 64 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,66 +1,76 @@
{
"name": "node-fetch",
"version": "2.3.0",
"description": "A light-weight module that brings window.fetch to node.js",
"main": "lib/index",
"browser": "./browser.js",
"module": "lib/index.mjs",
"files": [
"lib/index.js",
"lib/index.mjs",
"lib/index.es.js",
"browser.js"
],
"engines": {
"node": "4.x || >=6.0.0"
},
"scripts": {
"build": "cross-env BABEL_ENV=rollup rollup -c",
"prepare": "npm run build",
"test": "cross-env BABEL_ENV=test mocha --require babel-register test/test.js",
"report": "cross-env BABEL_ENV=coverage nyc --reporter lcov --reporter text mocha -R spec test/test.js",
"coverage": "cross-env BABEL_ENV=coverage nyc --reporter json --reporter text mocha -R spec test/test.js && codecov -f coverage/coverage-final.json"
},
"repository": {
"type": "git",
"url": "https://github.com/bitinn/node-fetch.git"
},
"keywords": [
"fetch",
"http",
"promise"
],
"author": "David Frank",
"license": "MIT",
"bugs": {
"url": "https://github.com/bitinn/node-fetch/issues"
},
"homepage": "https://github.com/bitinn/node-fetch",
"devDependencies": {
"abort-controller": "^1.0.2",
"abortcontroller-polyfill": "^1.1.9",
"babel-core": "^6.26.0",
"babel-plugin-istanbul": "^4.1.5",
"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.0.0",
"cross-env": "^5.1.3",
"form-data": "^2.3.1",
"is-builtin-module": "^1.0.0",
"mocha": "^5.0.0",
"nyc": "11.9.0",
"parted": "^0.1.1",
"promise": "^8.0.1",
"resumer": "0.0.0",
"rollup": "^0.63.4",
"rollup-plugin-babel": "^3.0.3",
"string-to-arraybuffer": "^1.0.0",
"url-search-params": "^1.0.2",
"whatwg-url": "^5.0.0"
},
"dependencies": {}
"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"
}
}
9 changes: 4 additions & 5 deletions rollup.config.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
import 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: [
@@ -18,10 +21,6 @@ export default {
tweakDefault()
],
external: function (id) {
if (isBuiltin(id)) {
return true;
}
id = id.split('/').slice(0, id[0] === '@' ? 2 : 1).join('/');
return !!require('./package.json').dependencies[id];
return dependencies.includes(id) || isBuiltin(id);
}
};
25 changes: 25 additions & 0 deletions src/blob.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
// 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');

@@ -12,6 +17,7 @@ export default class Blob {
const options = arguments[1];

const buffers = [];
let size = 0;

if (blobParts) {
const a = blobParts;
@@ -30,6 +36,7 @@ export default class Blob {
} else {
buffer = Buffer.from(typeof element === 'string' ? element : String(element));
}
size += buffer.length;
buffers.push(buffer);
}
}
@@ -47,6 +54,24 @@ export default class Blob {
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;

130 changes: 52 additions & 78 deletions src/body.js
Original file line number Diff line number Diff line change
@@ -34,24 +34,25 @@ export default function Body(body, {
if (body == null) {
// body is undefined or null
body = null;
} else if (typeof body === 'string') {
// body is string
} else if (isURLSearchParams(body)) {
// body is a URLSearchParams
} else if (body instanceof Blob) {
body = 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
body = String(body);
// coerce to string then buffer
body = Buffer.from(String(body));
}
this[INTERNALS] = {
body,
@@ -148,9 +149,7 @@ Body.prototype = {
*/
textConverted() {
return consumeBody.call(this).then(buffer => convertBody(buffer, this.headers));
},


}
};

// In browsers, all properties are enumerable.
@@ -191,38 +190,25 @@ function consumeBody() {
return Body.Promise.reject(this[INTERNALS].error);
}

let body = this.body;

// body is null
if (this.body === null) {
if (body === null) {
return Body.Promise.resolve(Buffer.alloc(0));
}

// body is string
if (typeof this.body === 'string') {
return Body.Promise.resolve(Buffer.from(this.body));
}

// body is blob
if (this.body instanceof Blob) {
return Body.Promise.resolve(this.body[BUFFER]);
if (isBlob(body)) {
body = body.stream();
}

// body is buffer
if (Buffer.isBuffer(this.body)) {
return Body.Promise.resolve(this.body);
}

// body is ArrayBuffer
if (Object.prototype.toString.call(this.body) === '[object ArrayBuffer]') {
return Body.Promise.resolve(Buffer.from(this.body));
}

// body is ArrayBufferView
if (ArrayBuffer.isView(this.body)) {
return Body.Promise.resolve(Buffer.from(this.body.buffer, this.body.byteOffset, this.body.byteLength));
if (Buffer.isBuffer(body)) {
return Body.Promise.resolve(body);
}

// istanbul ignore if: should never happen
if (!(this.body instanceof Stream)) {
if (!(body instanceof Stream)) {
return Body.Promise.resolve(Buffer.alloc(0));
}

@@ -244,7 +230,7 @@ function consumeBody() {
}

// handle stream errors
this.body.on('error', err => {
body.on('error', err => {
if (err.name === 'AbortError') {
// if the request was aborted, reject with this Error
abort = true;
@@ -255,7 +241,7 @@ function consumeBody() {
}
});

this.body.on('data', chunk => {
body.on('data', chunk => {
if (abort || chunk === null) {
return;
}
@@ -270,15 +256,15 @@ function consumeBody() {
accum.push(chunk);
});

this.body.on('end', () => {
body.on('end', () => {
if (abort) {
return;
}

clearTimeout(resTimeout);

try {
resolve(Buffer.concat(accum));
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));
@@ -320,6 +306,12 @@ function convertBody(buffer, headers) {
// html4
if (!res && str) {
res = /<meta[\s]+?http-equiv=(['"])content-type\1[\s]+?content=(['"])(.+?)\2/i.exec(str);
if (!res) {
res = /<meta[\s]+?content=(['"])(.+?)\1[\s]+?http-equiv=(['"])content-type\3/i.exec(str);
if (res) {
res.pop(); // drop last quote
}
}

if (res) {
res = /charset=(.*)/i.exec(res.pop());
@@ -375,6 +367,22 @@ function isURLSearchParams(obj) {
typeof obj.sort === 'function';
}

/**
* Check if `obj` is a W3C `Blob` object (which `File` inherits from)
* @param {*} obj
* @return {boolean}
*/
function isBlob(obj) {
return typeof obj === 'object' &&
typeof obj.arrayBuffer === 'function' &&
typeof obj.type === 'string' &&
typeof obj.stream === 'function' &&
typeof obj.constructor === 'function' &&
typeof obj.constructor.name === 'string' &&
/^(Blob|File)$/.test(obj.constructor.name) &&
/^(Blob|File)$/.test(obj[Symbol.toStringTag])
}

/**
* Clone body given Res/Req instance
*
@@ -413,23 +421,19 @@ export function clone(instance) {
*
* This function assumes that instance.body is present.
*
* @param Mixed instance Response or Request instance
* @param Mixed instance Any options.body input
*/
export function extractContentType(instance) {
const {body} = instance;

// istanbul ignore if: Currently, because of a guard in Request, body
// can never be null. Included here for completeness.
export function extractContentType(body) {
if (body === null) {
// body is null
return null;
} else if (typeof body === 'string') {
// body is string
return 'text/plain;charset=UTF-8';
} else if (isURLSearchParams(body)) {
// body is a URLSearchParams
// body is a URLSearchParams
return 'application/x-www-form-urlencoded;charset=UTF-8';
} else if (body instanceof Blob) {
} else if (isBlob(body)) {
// body is blob
return body.type || null;
} else if (Buffer.isBuffer(body)) {
@@ -444,10 +448,13 @@ export function extractContentType(instance) {
} else if (typeof body.getBoundary === 'function') {
// detect form data input from form-data module
return `multipart/form-data;boundary=${body.getBoundary()}`;
} else {
} else if (body instanceof Stream) {
// body is stream
// can't really do much about this
return null;
} else {
// Body constructor defaults other things to string
return 'text/plain;charset=UTF-8';
}
}

@@ -463,28 +470,14 @@ export function extractContentType(instance) {
export function getTotalBytes(instance) {
const {body} = instance;

// istanbul ignore if: included for completion
if (body === null) {
// body is null
return 0;
} else if (typeof body === 'string') {
// body is string
return Buffer.byteLength(body);
} else if (isURLSearchParams(body)) {
// body is URLSearchParams
return Buffer.byteLength(String(body));
} else if (body instanceof Blob) {
// body is blob
} else if (isBlob(body)) {
return body.size;
} else if (Buffer.isBuffer(body)) {
// body is buffer
return body.length;
} else if (Object.prototype.toString.call(body) === '[object ArrayBuffer]') {
// body is ArrayBuffer
return body.byteLength;
} else if (ArrayBuffer.isView(body)) {
// body is ArrayBufferView
return body.byteLength;
} else if (body && typeof body.getLengthSync === 'function') {
// detect form data input from form-data module
if (body._lengthRetrievers && body._lengthRetrievers.length == 0 || // 1.x
@@ -494,7 +487,6 @@ export function getTotalBytes(instance) {
return null;
} else {
// body is stream
// can't really do much about this
return null;
}
}
@@ -511,30 +503,12 @@ export function writeToStream(dest, instance) {
if (body === null) {
// body is null
dest.end();
} else if (typeof body === 'string') {
// body is string
dest.write(body);
dest.end();
} else if (isURLSearchParams(body)) {
// body is URLSearchParams
dest.write(Buffer.from(String(body)));
dest.end();
} else if (body instanceof Blob) {
// body is blob
dest.write(body[BUFFER]);
dest.end();
} else if (isBlob(body)) {
body.stream().pipe(dest);
} else if (Buffer.isBuffer(body)) {
// body is buffer
dest.write(body);
dest.end()
} else if (Object.prototype.toString.call(body) === '[object ArrayBuffer]') {
// body is ArrayBuffer
dest.write(Buffer.from(body));
dest.end()
} else if (ArrayBuffer.isView(body)) {
// body is ArrayBufferView
dest.write(Buffer.from(body.buffer, body.byteOffset, body.byteLength));
dest.end()
} else {
// body is stream
body.pipe(dest);
2 changes: 1 addition & 1 deletion src/headers.js
Original file line number Diff line number Diff line change
@@ -10,7 +10,7 @@ const invalidHeaderCharRegex = /[^\t\x20-\x7e\x80-\xff]/;

function validateName(name) {
name = `${name}`;
if (invalidTokenRegex.test(name)) {
if (invalidTokenRegex.test(name) || name === '') {
throw new TypeError(`${name} is not a legal HTTP header name`);
}
}
62 changes: 52 additions & 10 deletions src/index.js
Original file line number Diff line number Diff line change
@@ -13,16 +13,29 @@ import https from 'https';
import zlib from 'zlib';
import Stream from 'stream';

import Body, { writeToStream, getTotalBytes } from './body';
import Response from './response';
import Headers, { createHeadersLenient } from './headers';
import Request, { getNodeRequestOptions } from './request';
import FetchError from './fetch-error';
import AbortError from './abort-error';
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 resolve_url = Url.resolve;

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
@@ -109,12 +122,24 @@ export default function fetch(url, opts) {
const location = headers.get('Location');

// HTTP fetch step 5.3
const locationURL = location === null ? null : resolve_url(request.url, location);
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(`redirect mode is set to error: ${request.url}`, 'no-redirect'));
reject(new FetchError(`uri requested responds with a redirect, redirect mode is set to error: ${request.url}`, 'no-redirect'));
finalize();
return;
case 'manual':
@@ -153,8 +178,16 @@ export default function fetch(url, opts) {
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'));
@@ -188,7 +221,8 @@ export default function fetch(url, opts) {
statusText: res.statusMessage,
headers: headers,
size: request.size,
timeout: request.timeout
timeout: request.timeout,
counter: request.counter
};

// HTTP-network fetch step 12.1.1.3
@@ -244,6 +278,14 @@ export default function fetch(url, opts) {
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);
44 changes: 36 additions & 8 deletions src/request.js
Original file line number Diff line number Diff line change
@@ -9,15 +9,38 @@

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;

/**
@@ -59,14 +82,14 @@ export default class Request {
// in order to support Node.js' Url objects; though WHATWG's URL objects
// will fall into this branch also (since their `toString()` will return
// `href` property anyway)
parsedURL = parse_url(input.href);
parsedURL = parseURL(input.href);
} else {
// coerce input to a string before attempting to parse
parsedURL = parse_url(`${input}`);
parsedURL = parseURL(`${input}`);
}
input = {};
} else {
parsedURL = parse_url(input.url);
parsedURL = parseURL(input.url);
}

let method = init.method || input.method || 'GET';
@@ -90,9 +113,9 @@ export default class Request {

const headers = new Headers(init.headers || input.headers || {});

if (init.body != null) {
const contentType = extractContentType(this);
if (contentType !== null && !headers.has('Content-Type')) {
if (inputBody != null && !headers.has('Content-Type')) {
const contentType = extractContentType(inputBody);
if (contentType) {
headers.append('Content-Type', contentType);
}
}
@@ -230,7 +253,12 @@ export function getNodeRequestOptions(request) {
headers.set('Accept-Encoding', 'gzip,deflate');
}

if (!headers.has('Connection') && !request.agent) {
let agent = request.agent;
if (typeof agent === 'function') {
agent = agent(parsedURL);
}

if (!headers.has('Connection') && !agent) {
headers.set('Connection', 'close');
}

@@ -240,6 +268,6 @@ export function getNodeRequestOptions(request) {
return Object.assign({}, parsedURL, {
method: request.method,
headers: exportNodeCompatibleHeaders(headers),
agent: request.agent
agent
});
}
24 changes: 19 additions & 5 deletions src/response.js
Original file line number Diff line number Diff line change
@@ -8,7 +8,7 @@
import http from 'http';

import Headers from './headers.js';
import Body, { clone } from './body';
import Body, { clone, extractContentType } from './body';

const INTERNALS = Symbol('Response internals');

@@ -27,17 +27,26 @@ export default class Response {
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: new Headers(opts.headers)
headers,
counter: opts.counter
};
}

get url() {
return this[INTERNALS].url;
return this[INTERNALS].url || '';
}

get status() {
@@ -51,6 +60,10 @@ export default class Response {
return this[INTERNALS].status >= 200 && this[INTERNALS].status < 300;
}

get redirected() {
return this[INTERNALS].counter > 0;
}

get statusText() {
return this[INTERNALS].statusText;
}
@@ -70,9 +83,9 @@ export default class Response {
status: this.status,
statusText: this.statusText,
headers: this.headers,
ok: this.ok
ok: this.ok,
redirected: this.redirected
});

}
}

@@ -82,6 +95,7 @@ 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 }
65 changes: 47 additions & 18 deletions test/server.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import * as http from 'http';
import { parse } from 'url';
import * as zlib from 'zlib';
import * as stream from 'stream';
import { multipart as Multipart } from 'parted';

let convert;
@@ -32,7 +31,7 @@ export default class TestServer {
}

router(req, res) {
let p = parse(req.url).pathname;
let p = decodeURIComponent(parse(req.url).pathname);

if (p === '/hello') {
res.statusCode = 200;
@@ -66,6 +65,12 @@ export default class TestServer {
}));
}

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');
@@ -94,6 +99,18 @@ export default class TestServer {
});
}

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');
@@ -117,20 +134,6 @@ export default class TestServer {
res.end('fake gzip string');
}

if (p === '/invalid-header') {
res.setHeader('Content-Type', 'text/plain');
res.writeHead(200);
// HACK: add a few invalid headers to the generated header string before
// it is actually sent to the socket.
res._header = res._header.replace(/\r\n$/, [
'Invalid-Header : abc\r\n',
'Invalid-Header-Value: \x07k\r\n',
'Set-Cookie: \x07k\r\n',
'Set-Cookie: \x07kk\r\n',
].join('') + '\r\n');
res.end('hello world\n');
}

if (p === '/timeout') {
setTimeout(function() {
res.statusCode = 200;
@@ -159,10 +162,10 @@ export default class TestServer {
res.setHeader('Content-Type', 'text/plain');
setTimeout(function() {
res.write('test');
}, 50);
}, 10);
setTimeout(function() {
res.end('test');
}, 100);
}, 20);
}

if (p === '/size/long') {
@@ -183,6 +186,12 @@ export default class TestServer {
res.end(convert('<meta http-equiv="Content-Type" content="text/html; charset=gb2312"><div>中文</div>', 'gb2312'));
}

if (p === '/encoding/gb2312-reverse') {
res.statusCode = 200;
res.setHeader('Content-Type', 'text/html');
res.end(convert('<meta content="text/html; charset=gb2312" http-equiv="Content-Type"><div>中文</div>', 'gb2312'));
}

if (p === '/encoding/shift-jis') {
res.statusCode = 200;
res.setHeader('Content-Type', 'text/html; charset=Shift-JIS');
@@ -277,6 +286,14 @@ export default class TestServer {
}, 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');
@@ -322,6 +339,12 @@ export default class TestServer {
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();
@@ -366,6 +389,12 @@ export default class TestServer {
});
req.pipe(parser);
}

if (p === '/issues/1290/ひらがな') {
res.statusCode = 200;
res.setHeader('Content-Type', 'text/plain');
res.end('Success');
}
}
}

345 changes: 320 additions & 25 deletions test/test.js

Large diffs are not rendered by default.