This commit is contained in:
Lukian LEIZOUR 2022-11-26 15:56:34 +01:00
parent 70e2f7a8aa
commit 008d2f30d7
675 changed files with 189892 additions and 0 deletions

1
node_modules/ky-universal/browser.js generated vendored Normal file
View file

@ -0,0 +1 @@
export {default} from 'ky';

3
node_modules/ky-universal/index.d.ts generated vendored Normal file
View file

@ -0,0 +1,3 @@
import ky from 'ky';
export = ky;

33
node_modules/ky-universal/index.js generated vendored Normal file
View file

@ -0,0 +1,33 @@
'use strict';
const fetch = require('node-fetch');
const AbortController = require('abort-controller');
const TEN_MEGABYTES = 1000 * 1000 * 10;
if (!global.fetch) {
global.fetch = (url, options) => fetch(url, {highWaterMark: TEN_MEGABYTES, ...options});
}
if (!global.Headers) {
global.Headers = fetch.Headers;
}
if (!global.Request) {
global.Request = fetch.Request;
}
if (!global.Response) {
global.Response = fetch.Response;
}
if (!global.AbortController) {
global.AbortController = AbortController;
}
if (!global.ReadableStream) {
try {
global.ReadableStream = require('web-streams-polyfill/ponyfill/es2018');
} catch (_) {}
}
module.exports = require('ky/umd');

9
node_modules/ky-universal/license generated vendored Normal file
View file

@ -0,0 +1,9 @@
MIT License
Copyright (c) Sindre Sorhus <sindresorhus@gmail.com> (sindresorhus.com)
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.

View file

@ -0,0 +1,202 @@
/// <reference types="node" />
/* eslint-disable no-var, import/no-mutable-exports */
import { Agent } from 'http';
import { URL, URLSearchParams } from 'url'
import Blob = require('fetch-blob');
type AbortSignal = {
readonly aborted: boolean;
addEventListener(type: "abort", listener: (this: AbortSignal) => void): void;
removeEventListener(type: "abort", listener: (this: AbortSignal) => void): void;
};
type HeadersInit = Headers | Record<string, string> | Iterable<readonly [string, string]> | Iterable<Iterable<string>>;
/**
* This Fetch API interface allows you to perform various actions on HTTP request and response headers.
* These actions include retrieving, setting, adding to, and removing.
* A Headers object has an associated header list, which is initially empty and consists of zero or more name and value pairs.
* You can add to this using methods like append() (see Examples.)
* In all methods of this interface, header names are matched by case-insensitive byte sequence.
* */
declare class Headers {
constructor(init?: HeadersInit);
append(name: string, value: string): void;
delete(name: string): void;
get(name: string): string | null;
has(name: string): boolean;
set(name: string, value: string): void;
forEach(
callbackfn: (value: string, key: string, parent: Headers) => void,
thisArg?: any
): void;
[Symbol.iterator](): IterableIterator<[string, string]>;
/**
* Returns an iterator allowing to go through all key/value pairs contained in this object.
*/
entries(): IterableIterator<[string, string]>;
/**
* Returns an iterator allowing to go through all keys of the key/value pairs contained in this object.
*/
keys(): IterableIterator<string>;
/**
* Returns an iterator allowing to go through all values of the key/value pairs contained in this object.
*/
values(): IterableIterator<string>;
/** Node-fetch extension */
raw(): Record<string, string[]>;
}
interface RequestInit {
/**
* A BodyInit object or null to set request's body.
*/
body?: BodyInit | null;
/**
* A Headers object, an object literal, or an array of two-item arrays to set request's headers.
*/
headers?: HeadersInit;
/**
* A string to set request's method.
*/
method?: string;
/**
* A string indicating whether request follows redirects, results in an error upon encountering a redirect, or returns the redirect (in an opaque fashion). Sets request's redirect.
*/
redirect?: RequestRedirect;
/**
* An AbortSignal to set request's signal.
*/
signal?: AbortSignal | null;
// Node-fetch extensions to the whatwg/fetch spec
agent?: Agent | ((parsedUrl: URL) => Agent);
compress?: boolean;
counter?: number;
follow?: number;
hostname?: string;
port?: number;
protocol?: string;
size?: number;
highWaterMark?: number;
}
interface ResponseInit {
headers?: HeadersInit;
status?: number;
statusText?: string;
}
type BodyInit =
| Blob
| Buffer
| URLSearchParams
| NodeJS.ReadableStream
| string;
type BodyType = { [K in keyof Body]: Body[K] };
declare class Body {
constructor(body?: BodyInit, opts?: { size?: number });
readonly body: NodeJS.ReadableStream | null;
readonly bodyUsed: boolean;
readonly size: number;
buffer(): Promise<Buffer>;
arrayBuffer(): Promise<ArrayBuffer>;
blob(): Promise<Blob>;
json(): Promise<unknown>;
text(): Promise<string>;
}
type RequestRedirect = 'error' | 'follow' | 'manual';
type RequestInfo = string | Body;
declare class Request extends Body {
constructor(input: RequestInfo, init?: RequestInit);
/**
* Returns a Headers object consisting of the headers associated with request. Note that headers added in the network layer by the user agent will not be accounted for in this object, e.g., the "Host" header.
*/
readonly headers: Headers;
/**
* Returns request's HTTP method, which is "GET" by default.
*/
readonly method: string;
/**
* Returns the redirect mode associated with request, which is a string indicating how redirects for the request will be handled during fetching. A request will follow redirects by default.
*/
readonly redirect: RequestRedirect;
/**
* Returns the signal associated with request, which is an AbortSignal object indicating whether or not request has been aborted, and its abort event handler.
*/
readonly signal: AbortSignal;
/**
* Returns the URL of request as a string.
*/
readonly url: string;
clone(): Request;
}
declare class Response extends Body {
constructor(body?: BodyInit | null, init?: ResponseInit);
readonly headers: Headers;
readonly ok: boolean;
readonly redirected: boolean;
readonly status: number;
readonly statusText: string;
readonly url: string;
clone(): Response;
}
declare class FetchError extends Error {
constructor(message: string, type: string, systemError?: object);
name: 'FetchError';
[Symbol.toStringTag]: 'FetchError';
type: string;
code?: string;
errno?: string;
}
declare class AbortError extends Error {
type: string;
name: 'AbortError';
[Symbol.toStringTag]: 'AbortError';
}
declare function fetch(url: RequestInfo, init?: RequestInit): Promise<Response>;
declare class fetch {
static default: typeof fetch;
}
declare namespace fetch {
export function isRedirect(code: number): boolean;
export {
HeadersInit,
Headers,
RequestInit,
RequestRedirect,
RequestInfo,
Request,
BodyInit,
ResponseInit,
Response,
FetchError,
AbortError
};
export interface Body extends BodyType { }
}
export = fetch;

View file

@ -0,0 +1,22 @@
The MIT License (MIT)
Copyright (c) 2016 - 2020 Node Fetch Team
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.

View file

@ -0,0 +1,769 @@
<div align="center">
<img src="docs/media/Banner.svg" alt="Node Fetch"/>
<br>
<p>A light-weight module that brings <a href="https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API">Fetch API</a> to Node.js.</p>
<a href="https://github.com/node-fetch/node-fetch/actions"><img src="https://github.com/node-fetch/node-fetch/workflows/CI/badge.svg?branch=master" alt="Build status"></a>
<a href="https://coveralls.io/github/node-fetch/node-fetch"><img src="https://img.shields.io/coveralls/github/node-fetch/node-fetch" alt="Coverage status"></a>
<a href="https://packagephobia.now.sh/result?p=node-fetch"><img src="https://badgen.net/packagephobia/install/node-fetch" alt="Current version"></a>
<a href="https://www.npmjs.com/package/node-fetch"><img src="https://img.shields.io/npm/v/node-fetch" alt="Install size"></a>
<a href="https://github.com/sindresorhus/awesome-nodejs"><img src="https://awesome.re/mentioned-badge.svg" alt="Mentioned in Awesome Node.js"></a>
<a href="https://discord.gg/Zxbndcm"><img src="https://img.shields.io/discord/619915844268326952?color=%237289DA&label=Discord" alt="Discord"></a>
<br>
<br>
<b>Consider supporting us on our Open Collective:</b>
<br>
<br>
<a href="https://opencollective.com/node-fetch"><img src="https://opencollective.com/node-fetch/donate/button.png?color=blue" alt="Open Collective"></a>
</div>
---
<!-- TOC -->
- [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)
- [Upgrading](#upgrading)
- [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)
- [Handling cookies](#handling-cookies)
- [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)
- [Default Headers](#default-headers)
- [Custom Agent](#custom-agent)
- [Custom highWaterMark](#custom-highwatermark)
- [Insecure HTTP Parser](#insecure-http-parser)
- [Class: Request](#class-request)
- [new Request(input[, options])](#new-requestinput-options)
- [Class: Response](#class-response)
- [new Response([body[, options]])](#new-responsebody-options)
- [response.ok](#responseok)
- [response.redirected](#responseredirected)
- [Class: Headers](#class-headers)
- [new Headers([init])](#new-headersinit)
- [Interface: Body](#interface-body)
- [body.body](#bodybody)
- [body.bodyUsed](#bodybodyused)
- [body.arrayBuffer()](#bodyarraybuffer)
- [body.blob()](#bodyblob)
- [body.json()](#bodyjson)
- [body.text()](#bodytext)
- [body.buffer()](#bodybuffer)
- [Class: FetchError](#class-fetcherror)
- [Class: AbortError](#class-aborterror)
- [TypeScript](#typescript)
- [Acknowledgement](#acknowledgement)
- [Team](#team)
- [Former](#former)
- [License](#license)
<!-- /TOC -->
## 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 Jason Miller's [isomorphic-unfetch](https://www.npmjs.com/package/isomorphic-unfetch) 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 and async functions.
- Use native Node streams for body, on both request and response.
- Decode content encoding (gzip/deflate/brotli) properly, and convert string output (such as `res.text()` and `res.json()`) to UTF-8 automatically.
- Useful extensions such as redirect limit, response size limit, [explicit errors][error-handling.md] for troubleshooting.
## Difference from client-side fetch
- See known differences:
- [As of v3.x](docs/v3-LIMITS.md)
- [As of v2.x](docs/v2-LIMITS.md)
- If you happen to use a missing feature that `window.fetch` offers, feel free to open an issue.
- Pull requests are welcomed too!
## Installation
Current stable release (`3.x`)
```sh
$ npm install node-fetch
```
## Loading and configuring the module
```js
// CommonJS
const fetch = require('node-fetch');
// ES Module
import fetch from 'node-fetch';
```
If you want to patch the global object in node:
```js
const fetch = require('node-fetch');
if (!globalThis.fetch) {
globalThis.fetch = fetch;
}
```
For versions of Node earlier than 12, use this `globalThis` [polyfill](https://mathiasbynens.be/notes/globalthis).
## Upgrading
Using an old version of node-fetch? Check out the following files:
- [2.x to 3.x upgrade guide](docs/v3-UPGRADE-GUIDE.md)
- [1.x to 2.x upgrade guide](docs/v2-UPGRADE-GUIDE.md)
- [Changelog](docs/CHANGELOG.md)
## Common Usage
NOTE: The documentation below is up-to-date with `3.x` releases, if you are using an older version, please check how to [upgrade](#upgrading).
### Plain text or HTML
```js
const fetch = require('node-fetch');
(async () => {
const response = await fetch('https://github.com/');
const body = await response.text();
console.log(body);
})();
```
### JSON
```js
const fetch = require('node-fetch');
(async () => {
const response = await fetch('https://api.github.com/users/github');
const json = await response.json();
console.log(json);
})();
```
### Simple Post
```js
const fetch = require('node-fetch');
(async () => {
const response = await fetch('https://httpbin.org/post', {method: 'POST', body: 'a=1'});
const json = await response.json();
console.log(json);
})();
```
### Post with JSON
```js
const fetch = require('node-fetch');
(async () => {
const body = {a: 1};
const response = await fetch('https://httpbin.org/post', {
method: 'post',
body: JSON.stringify(body),
headers: {'Content-Type': 'application/json'}
});
const json = await response.json();
console.log(json);
})();
```
### Post with form parameters
`URLSearchParams` is available on the global object in Node.js as of v10.0.0. See [official documentation](https://nodejs.org/api/url.html#url_class_urlsearchparams) for more usage methods.
NOTE: The `Content-Type` header is only set automatically to `x-www-form-urlencoded` when an instance of `URLSearchParams` is given as such:
```js
const fetch = require('node-fetch');
const params = new URLSearchParams();
params.append('a', 1);
(async () => {
const response = await fetch('https://httpbin.org/post', {method: 'POST', body: params});
const json = await response.json();
console.log(json);
})();
```
### Handling exceptions
NOTE: 3xx-5xx responses are _NOT_ exceptions, and should be handled in `then()`, see the next section.
Wrapping the fetch function into a `try/catch` block will catch _all_ exceptions, such as errors originating from node core libraries, like network errors, and operational errors which are instances of FetchError. See the [error handling document][error-handling.md] for more details.
```js
const fetch = require('node-fetch');
try {
fetch('https://domain.invalid/');
} catch (error) {
console.log(error);
}
```
### Handling client and server errors
It is common to create a helper function to check that the response contains no client (4xx) or server (5xx) error responses:
```js
const fetch = require('node-fetch');
const checkStatus = res => {
if (res.ok) {
// res.status >= 200 && res.status < 300
return res;
} else {
throw MyCustomError(res.statusText);
}
}
(async () => {
const response = await fetch('https://httpbin.org/status/400');
const data = checkStatus(response);
console.log(data); //=> MyCustomError
})();
```
### Handling cookies
Cookies are not stored by default. However, cookies can be extracted and passed by manipulating request and response headers. See [Extract Set-Cookie Header](#extract-set-cookie-header) for details.
## Advanced Usage
### Streams
The "Node.js way" is to use streams when possible. You can pipe `res.body` to another stream. This example uses [stream.pipeline](https://nodejs.org/api/stream.html#stream_stream_pipeline_streams_callback) to attach stream error handlers and wait for the download to complete.
```js
const util = require('util');
const fs = require('fs');
const streamPipeline = util.promisify(require('stream').pipeline);
(async () => {
const response = await fetch('https://assets-cdn.github.com/images/modules/logos_page/Octocat.png');
if (response.ok) {
return streamPipeline(response.body, fs.createWriteStream('./octocat.png'));
}
throw new Error(`unexpected response ${response.statusText}`);
})();
```
### Buffer
If you prefer to cache binary data in full, use buffer(). (NOTE: buffer() is a `node-fetch` only API)
```js
const fetch = require('node-fetch');
const fileType = require('file-type');
(async () => {
const response = await fetch('https://octodex.github.com/images/Fintechtocat.png');
const buffer = await response.buffer();
const type = fileType.fromBuffer(buffer)
console.log(type);
})();
```
### Accessing Headers and other Meta data
```js
const fetch = require('node-fetch');
(async () => {
const response = await fetch('https://github.com/');
console.log(response.ok);
console.log(response.status);
console.log(response.statusText);
console.log(response.headers.raw());
console.log(response.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
const fetch = require('node-fetch');
(async () => {
const response = await fetch('https://example.com');
// Returns an array of values, instead of a string of comma-separated values
console.log(response.headers.raw()['set-cookie']);
})();
```
### Post data using a file stream
```js
const {createReadStream} = require('fs');
const fetch = require('node-fetch');
const stream = createReadStream('input.txt');
(async () => {
const response = await fetch('https://httpbin.org/post', {method: 'POST', body: stream});
const json = await response.json();
console.log(json)
})();
```
### Post with form-data (detect multipart)
```js
const fetch = require('node-fetch');
const FormData = require('form-data');
const form = new FormData();
form.append('a', 1);
(async () => {
const response = await fetch('https://httpbin.org/post', {method: 'POST', body: form});
const json = await response.json();
console.log(json)
})();
// OR, using custom headers
// NOTE: getHeaders() is non-standard API
const options = {
method: 'POST',
body: form,
headers: form.getHeaders()
};
(async () => {
const response = await fetch('https://httpbin.org/post', options);
const json = await response.json();
console.log(json)
})();
```
node-fetch also supports spec-compliant FormData implementations such as [formdata-node](https://github.com/octet-stream/form-data):
```js
const fetch = require('node-fetch');
const FormData = require('formdata-node');
const form = new FormData();
form.set('greeting', 'Hello, world!');
fetch('https://httpbin.org/post', {method: 'POST', body: form})
.then(res => res.json())
.then(json => console.log(json));
```
### Request cancellation with AbortSignal
You may cancel requests with `AbortController`. A suggested implementation is [`abort-controller`](https://www.npmjs.com/package/abort-controller).
An example of timing out a request after 150ms could be achieved as the following:
```js
const fetch = require('node-fetch');
const AbortController = require('abort-controller');
const controller = new AbortController();
const timeout = setTimeout(() => {
controller.abort();
}, 150);
(async () => {
try {
const response = await fetch('https://example.com', {signal: controller.signal});
const data = await response.json();
useData(data);
} catch (error) {
if (error.name === 'AbortError') {
console.log('request was aborted');
}
} finally {
clearTimeout(timeout);
}
})();
```
See [test cases](https://github.com/node-fetch/node-fetch/blob/master/test/) 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: <code>Promise&lt;[Response](#class-response)&gt;</code>
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`.
<a id="fetch-options"></a>
### 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
compress: true, // support gzip/deflate content encoding. false to disable
size: 0, // maximum response body size in bytes. 0 to disable
agent: null, // http(s).Agent instance or function that returns an instance (see below)
highWaterMark: 16384, // the maximum number of bytes to store in the internal buffer before ceasing to read from the underlying resource.
insecureHTTPParser: false // Use an insecure HTTP parser that accepts invalid HTTP headers when `true`.
}
```
#### Default Headers
If no values are set, the following request headers will be sent automatically:
| Header | Value |
| ------------------- | ------------------------------------------------------ |
| `Accept-Encoding` | `gzip,deflate,br` _(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` |
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 http = require('http');
const https = require('https');
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="custom-highWaterMark"></a>
#### Custom highWaterMark
Stream on Node.js have a smaller internal buffer size (16kB, aka `highWaterMark`) from client-side browsers (>1MB, not consistent across browsers). Because of that, when you are writing an isomorphic app and using `res.clone()`, it will hang with large response in Node.
The recommended way to fix this problem is to resolve cloned response in parallel:
```js
const fetch = require('node-fetch');
(async () => {
const response = await fetch('https://example.com');
const r1 = await response.clone();
return Promise.all([res.json(), r1.text()]).then(results => {
console.log(results[0]);
console.log(results[1]);
});
})();
```
If for some reason you don't like the solution above, since `3.x` you are able to modify the `highWaterMark` option:
```js
const fetch = require('node-fetch');
(async () => {
const response = await fetch('https://example.com', {
// About 1MB
highWaterMark: 1024 * 1024
});
return res.clone().buffer();
})();
```
#### Insecure HTTP Parser
Passed through to the `insecureHTTPParser` option on http(s).request. See [`http.request`](https://nodejs.org/api/http.html#http_http_request_url_options_callback) for more information.
<a id="class-request"></a>
### 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`
- `highWaterMark`
See [options](#fetch-options) for exact meaning of these extensions.
#### new Request(input[, options])
<small>_(spec-compliant)_</small>
- `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.
<a id="class-response"></a>
### 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]])
<small>_(spec-compliant)_</small>
- `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
<small>_(spec-compliant)_</small>
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
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])
<small>_(spec-compliant)_</small>
- `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 { Headers } = require('node-fetch');
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);
```
<a id="iface-body"></a>
### 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
<small>_(deviation from spec)_</small>
- 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 the specs, a consumed body cannot be used again.
#### body.arrayBuffer()
#### body.blob()
#### body.json()
#### body.text()
<small>_(spec-compliant)_</small>
- Returns: `Promise`
Consume the body and return a promise that will resolve to one of these formats.
#### body.buffer()
<small>_(node-fetch extension)_</small>
- Returns: `Promise<Buffer>`
Consume the body and return a promise that will resolve to a Buffer.
<a id="class-fetcherror"></a>
### Class: FetchError
<small>_(node-fetch extension)_</small>
An operational error in the fetching process. See [ERROR-HANDLING.md][] for more info.
<a id="class-aborterror"></a>
### Class: AbortError
<small>_(node-fetch extension)_</small>
An Error thrown when the request is aborted in response to an `AbortSignal`'s `abort` event. It has a `name` property of `AbortError`. See [ERROR-HANDLING.MD][] for more info.
## TypeScript
**Since `3.x` types are bundled with `node-fetch`, so you don't need to install any additional packages.**
For older versions please use the type definitions from [DefinitelyTyped](https://github.com/DefinitelyTyped/DefinitelyTyped):
```sh
$ npm install --save-dev @types/node-fetch
```
## Acknowledgement
Thanks to [github/fetch](https://github.com/github/fetch) for providing a solid implementation reference.
## Team
| [![David Frank](https://github.com/bitinn.png?size=100)](https://github.com/bitinn) | [![Jimmy Wärting](https://github.com/jimmywarting.png?size=100)](https://github.com/jimmywarting) | [![Antoni Kepinski](https://github.com/xxczaki.png?size=100)](https://github.com/xxczaki) | [![Richie Bendall](https://github.com/Richienb.png?size=100)](https://github.com/Richienb) | [![Gregor Martynus](https://github.com/gr2m.png?size=100)](https://github.com/gr2m) |
| ----------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------- |
| [David Frank](https://bitinn.net/) | [Jimmy Wärting](https://jimmy.warting.se/) | [Antoni Kepinski](https://kepinski.me) | [Richie Bendall](https://www.richie-bendall.ml/) | [Gregor Martynus](https://twitter.com/gr2m) |
###### Former
- [Timothy Gu](https://github.com/timothygu)
- [Jared Kantrowitz](https://github.com/jkantr)
## License
[MIT](LICENSE.md)
[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
[error-handling.md]: https://github.com/node-fetch/node-fetch/blob/master/docs/ERROR-HANDLING.md

File diff suppressed because it is too large Load diff

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,139 @@
{
"name": "node-fetch",
"version": "3.0.0-beta.9",
"description": "A light-weight module that brings Fetch API to node.js",
"main": "./dist/index.cjs",
"module": "./src/index.js",
"sideEffects": false,
"type": "module",
"exports": {
".": {
"import": "./src/index.js",
"require": "./dist/index.cjs"
},
"./package.json": "./package.json"
},
"files": [
"src",
"dist",
"@types/index.d.ts"
],
"types": "./@types/index.d.ts",
"engines": {
"node": "^10.17 || >=12.3"
},
"scripts": {
"build": "rollup -c",
"test": "node --experimental-modules node_modules/c8/bin/c8 --reporter=html --reporter=lcov --reporter=text --check-coverage node --experimental-modules node_modules/mocha/bin/mocha",
"coverage": "c8 report --reporter=text-lcov | coveralls",
"test-types": "tsd",
"lint": "xo",
"prepublishOnly": "node ./test/commonjs/test-artifact.js"
},
"repository": {
"type": "git",
"url": "https://github.com/node-fetch/node-fetch.git"
},
"keywords": [
"fetch",
"http",
"promise",
"request",
"curl",
"wget",
"xhr",
"whatwg"
],
"author": "David Frank",
"license": "MIT",
"bugs": {
"url": "https://github.com/node-fetch/node-fetch/issues"
},
"homepage": "https://github.com/node-fetch/node-fetch",
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/node-fetch"
},
"devDependencies": {
"abort-controller": "^3.0.0",
"abortcontroller-polyfill": "^1.5.0",
"busboy": "^0.3.1",
"c8": "^7.3.0",
"chai": "^4.2.0",
"chai-as-promised": "^7.1.1",
"chai-iterator": "^3.0.2",
"chai-string": "^1.5.0",
"coveralls": "^3.1.0",
"delay": "^4.4.0",
"form-data": "^3.0.0",
"formdata-node": "^2.4.0",
"mocha": "^8.1.3",
"p-timeout": "^3.2.0",
"rollup": "^2.26.10",
"tsd": "^0.13.1",
"xo": "^0.33.1"
},
"dependencies": {
"data-uri-to-buffer": "^3.0.1",
"fetch-blob": "^2.1.1"
},
"esm": {
"sourceMap": true,
"cjs": false
},
"tsd": {
"cwd": "@types",
"compilerOptions": {
"target": "esnext",
"lib": [
"es2018"
],
"allowSyntheticDefaultImports": false,
"esModuleInterop": false
}
},
"xo": {
"envs": [
"node",
"browser"
],
"rules": {
"complexity": 0,
"import/extensions": 0,
"import/no-useless-path-segments": 0,
"import/no-anonymous-default-export": 0,
"unicorn/import-index": 0,
"unicorn/no-reduce": 0,
"capitalized-comments": 0
},
"ignores": [
"dist",
"@types"
],
"overrides": [
{
"files": "test/**/*.js",
"envs": [
"node",
"mocha"
],
"rules": {
"max-nested-callbacks": 0,
"no-unused-expressions": 0,
"new-cap": 0,
"guard-for-in": 0,
"unicorn/prevent-abbreviations": 0,
"promise/prefer-await-to-then": 0,
"ava/no-import-test-files": 0
}
},
{
"files": "example.js",
"rules": {
"import/no-extraneous-dependencies": 0
}
}
]
},
"runkitExampleFilename": "example.js"
}

View file

@ -0,0 +1,384 @@
/**
* Body.js
*
* Body interface provides common methods for Request and Response
*/
import Stream, {PassThrough} from 'stream';
import {types} from 'util';
import Blob from 'fetch-blob';
import {FetchError} from './errors/fetch-error.js';
import {FetchBaseError} from './errors/base.js';
import {formDataIterator, getBoundary, getFormDataLength} from './utils/form-data.js';
import {isBlob, isURLSearchParameters, isFormData} from './utils/is.js';
const INTERNALS = Symbol('Body internals');
/**
* Body mixin
*
* Ref: https://fetch.spec.whatwg.org/#body
*
* @param Stream body Readable stream
* @param Object opts Response options
* @return Void
*/
export default class Body {
constructor(body, {
size = 0
} = {}) {
let boundary = null;
if (body === null) {
// Body is undefined or null
body = null;
} else if (isURLSearchParameters(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 (types.isAnyArrayBuffer(body)) {
// 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 if (isFormData(body)) {
// Body is an instance of formdata-node
boundary = `NodeFetchFormDataBoundary${getBoundary()}`;
body = Stream.Readable.from(formDataIterator(body, boundary));
} else {
// None of the above
// coerce to string then buffer
body = Buffer.from(String(body));
}
this[INTERNALS] = {
body,
boundary,
disturbed: false,
error: null
};
this.size = size;
if (body instanceof Stream) {
body.on('error', err => {
const error = err instanceof FetchBaseError ?
err :
new FetchError(`Invalid response body while trying to fetch ${this.url}: ${err.message}`, 'system', err);
this[INTERNALS].error = error;
});
}
}
get body() {
return this[INTERNALS].body;
}
get bodyUsed() {
return this[INTERNALS].disturbed;
}
/**
* Decode response as ArrayBuffer
*
* @return Promise
*/
async arrayBuffer() {
const {buffer, byteOffset, byteLength} = await consumeBody(this);
return buffer.slice(byteOffset, byteOffset + byteLength);
}
/**
* Return raw response as Blob
*
* @return Promise
*/
async blob() {
const ct = (this.headers && this.headers.get('content-type')) || (this[INTERNALS].body && this[INTERNALS].body.type) || '';
const buf = await this.buffer();
return new Blob([buf], {
type: ct
});
}
/**
* Decode response as json
*
* @return Promise
*/
async json() {
const buffer = await consumeBody(this);
return JSON.parse(buffer.toString());
}
/**
* Decode response as text
*
* @return Promise
*/
async text() {
const buffer = await consumeBody(this);
return buffer.toString();
}
/**
* Decode response as buffer (non-spec api)
*
* @return Promise
*/
buffer() {
return consumeBody(this);
}
}
// 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}
});
/**
* Consume and convert an entire Body to a Buffer.
*
* Ref: https://fetch.spec.whatwg.org/#concept-body-consume-body
*
* @return Promise
*/
async function consumeBody(data) {
if (data[INTERNALS].disturbed) {
throw new TypeError(`body used already for: ${data.url}`);
}
data[INTERNALS].disturbed = true;
if (data[INTERNALS].error) {
throw data[INTERNALS].error;
}
let {body} = data;
// Body is null
if (body === null) {
return Buffer.alloc(0);
}
// Body is blob
if (isBlob(body)) {
body = body.stream();
}
// Body is buffer
if (Buffer.isBuffer(body)) {
return body;
}
/* c8 ignore next 3 */
if (!(body instanceof Stream)) {
return Buffer.alloc(0);
}
// Body is stream
// get ready to actually consume the body
const accum = [];
let accumBytes = 0;
try {
for await (const chunk of body) {
if (data.size > 0 && accumBytes + chunk.length > data.size) {
const err = new FetchError(`content size at ${data.url} over limit: ${data.size}`, 'max-size');
body.destroy(err);
throw err;
}
accumBytes += chunk.length;
accum.push(chunk);
}
} catch (error) {
if (error instanceof FetchBaseError) {
throw error;
} else {
// Other errors, such as incorrect content-encoding
throw new FetchError(`Invalid response body while trying to fetch ${data.url}: ${error.message}`, 'system', error);
}
}
if (body.readableEnded === true || body._readableState.ended === true) {
try {
if (accum.every(c => typeof c === 'string')) {
return Buffer.from(accum.join(''));
}
return Buffer.concat(accum, accumBytes);
} catch (error) {
throw new FetchError(`Could not create Buffer from response body for ${data.url}: ${error.message}`, 'system', error);
}
} else {
throw new FetchError(`Premature close of server response while trying to fetch ${data.url}`);
}
}
/**
* Clone body given Res/Req instance
*
* @param Mixed instance Response or Request instance
* @param String highWaterMark highWaterMark for both PassThrough body streams
* @return Mixed
*/
export const clone = (instance, highWaterMark) => {
let p1;
let p2;
let {body} = instance;
// Don't allow cloning a used body
if (instance.bodyUsed) {
throw new Error('cannot clone body after it is used');
}
// Check that body is a stream and not form-data object
// note: we can't clone the form-data object without having it as a dependency
if ((body instanceof Stream) && (typeof body.getBoundary !== 'function')) {
// Tee instance body
p1 = new PassThrough({highWaterMark});
p2 = new PassThrough({highWaterMark});
body.pipe(p1);
body.pipe(p2);
// Set instance body to teed body and return the other teed body
instance[INTERNALS].body = p1;
body = p2;
}
return body;
};
/**
* Performs the operation "extract a `Content-Type` value from |object|" as
* specified in the specification:
* https://fetch.spec.whatwg.org/#concept-bodyinit-extract
*
* This function assumes that instance.body is present.
*
* @param {any} body Any options.body input
* @returns {string | null}
*/
export const extractContentType = (body, request) => {
// Body is null or undefined
if (body === null) {
return null;
}
// Body is string
if (typeof body === 'string') {
return 'text/plain;charset=UTF-8';
}
// Body is a URLSearchParams
if (isURLSearchParameters(body)) {
return 'application/x-www-form-urlencoded;charset=UTF-8';
}
// Body is blob
if (isBlob(body)) {
return body.type || null;
}
// Body is a Buffer (Buffer, ArrayBuffer or ArrayBufferView)
if (Buffer.isBuffer(body) || types.isAnyArrayBuffer(body) || ArrayBuffer.isView(body)) {
return null;
}
// Detect form data input from form-data module
if (body && typeof body.getBoundary === 'function') {
return `multipart/form-data;boundary=${body.getBoundary()}`;
}
if (isFormData(body)) {
return `multipart/form-data; boundary=${request[INTERNALS].boundary}`;
}
// Body is stream - can't really do much about this
if (body instanceof Stream) {
return null;
}
// Body constructor defaults other things to string
return 'text/plain;charset=UTF-8';
};
/**
* The Fetch Standard treats this as if "total bytes" is a property on the body.
* For us, we have to explicitly get it with a function.
*
* ref: https://fetch.spec.whatwg.org/#concept-body-total-bytes
*
* @param {any} obj.body Body object from the Body instance.
* @returns {number | null}
*/
export const getTotalBytes = request => {
const {body} = request;
// Body is null or undefined
if (body === null) {
return 0;
}
// Body is Blob
if (isBlob(body)) {
return body.size;
}
// Body is Buffer
if (Buffer.isBuffer(body)) {
return body.length;
}
// Detect form data input from form-data module
if (body && typeof body.getLengthSync === 'function') {
return body.hasKnownLength && body.hasKnownLength() ? body.getLengthSync() : null;
}
// Body is a spec-compliant form-data
if (isFormData(body)) {
return getFormDataLength(request[INTERNALS].boundary);
}
// Body is stream
return null;
};
/**
* Write a Body to a Node.js WritableStream (e.g. http.Request) object.
*
* @param {Stream.Writable} dest The stream to write to.
* @param obj.body Body object from the Body instance.
* @returns {void}
*/
export const writeToStream = (dest, {body}) => {
if (body === null) {
// Body is null
dest.end();
} else if (isBlob(body)) {
// Body is Blob
body.stream().pipe(dest);
} else if (Buffer.isBuffer(body)) {
// Body is buffer
dest.write(body);
dest.end();
} else {
// Body is stream
body.pipe(dest);
}
};

View file

@ -0,0 +1,10 @@
import {FetchBaseError} from './base.js';
/**
* AbortError interface for cancelled requests
*/
export class AbortError extends FetchBaseError {
constructor(message, type = 'aborted') {
super(message, type);
}
}

View file

@ -0,0 +1,20 @@
'use strict';
export class FetchBaseError extends Error {
constructor(message, type) {
super(message);
// Hide custom error implementation details from end-users
Error.captureStackTrace(this, this.constructor);
this.type = type;
}
get name() {
return this.constructor.name;
}
get [Symbol.toStringTag]() {
return this.constructor.name;
}
}

View file

@ -0,0 +1,26 @@
import {FetchBaseError} from './base.js';
/**
* @typedef {{ address?: string, code: string, dest?: string, errno: number, info?: object, message: string, path?: string, port?: number, syscall: string}} SystemError
*/
/**
* FetchError interface for operational errors
*/
export class FetchError extends FetchBaseError {
/**
* @param {string} message - Error message for human
* @param {string} [type] - Error type for machine
* @param {SystemError} [systemError] - For Node.js system error
*/
constructor(message, type, systemError) {
super(message, type);
// When err.type is `system`, err.erroredSysCall contains system error and err.code contains system error code
if (systemError) {
// eslint-disable-next-line no-multi-assign
this.code = this.errno = systemError.code;
this.erroredSysCall = systemError.syscall;
}
}
}

View file

@ -0,0 +1,265 @@
/**
* Headers.js
*
* Headers class offers convenient helpers
*/
import {types} from 'util';
import http from 'http';
const validateHeaderName = typeof http.validateHeaderName === 'function' ?
http.validateHeaderName :
name => {
if (!/^[\^`\-\w!#$%&'*+.|~]+$/.test(name)) {
const err = new TypeError(`Header name must be a valid HTTP token [${name}]`);
Object.defineProperty(err, 'code', {value: 'ERR_INVALID_HTTP_TOKEN'});
throw err;
}
};
const validateHeaderValue = typeof http.validateHeaderValue === 'function' ?
http.validateHeaderValue :
(name, value) => {
if (/[^\t\u0020-\u007E\u0080-\u00FF]/.test(value)) {
const err = new TypeError(`Invalid character in header content ["${name}"]`);
Object.defineProperty(err, 'code', {value: 'ERR_INVALID_CHAR'});
throw err;
}
};
/**
* @typedef {Headers | Record<string, string> | Iterable<readonly [string, string]> | Iterable<Iterable<string>>} HeadersInit
*/
/**
* This Fetch API interface allows you to perform various actions on HTTP request and response headers.
* These actions include retrieving, setting, adding to, and removing.
* A Headers object has an associated header list, which is initially empty and consists of zero or more name and value pairs.
* You can add to this using methods like append() (see Examples.)
* In all methods of this interface, header names are matched by case-insensitive byte sequence.
*
*/
export default class Headers extends URLSearchParams {
/**
* Headers class
*
* @constructor
* @param {HeadersInit} [init] - Response headers
*/
constructor(init) {
// Validate and normalize init object in [name, value(s)][]
/** @type {string[][]} */
let result = [];
if (init instanceof Headers) {
const raw = init.raw();
for (const [name, values] of Object.entries(raw)) {
result.push(...values.map(value => [name, value]));
}
} else if (init == null) { // eslint-disable-line no-eq-null, eqeqeq
// No op
} else if (typeof init === 'object' && !types.isBoxedPrimitive(init)) {
const method = init[Symbol.iterator];
// eslint-disable-next-line no-eq-null, eqeqeq
if (method == null) {
// Record<ByteString, ByteString>
result.push(...Object.entries(init));
} else {
if (typeof method !== 'function') {
throw new TypeError('Header pairs must be iterable');
}
// Sequence<sequence<ByteString>>
// Note: per spec we have to first exhaust the lists then process them
result = [...init]
.map(pair => {
if (
typeof pair !== 'object' || types.isBoxedPrimitive(pair)
) {
throw new TypeError('Each header pair must be an iterable object');
}
return [...pair];
}).map(pair => {
if (pair.length !== 2) {
throw new TypeError('Each header pair must be a name/value tuple');
}
return [...pair];
});
}
} else {
throw new TypeError('Failed to construct \'Headers\': The provided value is not of type \'(sequence<sequence<ByteString>> or record<ByteString, ByteString>)');
}
// Validate and lowercase
result =
result.length > 0 ?
result.map(([name, value]) => {
validateHeaderName(name);
validateHeaderValue(name, String(value));
return [String(name).toLowerCase(), String(value)];
}) :
undefined;
super(result);
// Returning a Proxy that will lowercase key names, validate parameters and sort keys
// eslint-disable-next-line no-constructor-return
return new Proxy(this, {
get(target, p, receiver) {
switch (p) {
case 'append':
case 'set':
return (name, value) => {
validateHeaderName(name);
validateHeaderValue(name, String(value));
return URLSearchParams.prototype[p].call(
receiver,
String(name).toLowerCase(),
String(value)
);
};
case 'delete':
case 'has':
case 'getAll':
return name => {
validateHeaderName(name);
return URLSearchParams.prototype[p].call(
receiver,
String(name).toLowerCase()
);
};
case 'keys':
return () => {
target.sort();
return new Set(URLSearchParams.prototype.keys.call(target)).keys();
};
default:
return Reflect.get(target, p, receiver);
}
}
/* c8 ignore next */
});
}
get [Symbol.toStringTag]() {
return this.constructor.name;
}
toString() {
return Object.prototype.toString.call(this);
}
get(name) {
const values = this.getAll(name);
if (values.length === 0) {
return null;
}
let value = values.join(', ');
if (/^content-encoding$/i.test(name)) {
value = value.toLowerCase();
}
return value;
}
forEach(callback) {
for (const name of this.keys()) {
callback(this.get(name), name);
}
}
* values() {
for (const name of this.keys()) {
yield this.get(name);
}
}
/**
* @type {() => IterableIterator<[string, string]>}
*/
* entries() {
for (const name of this.keys()) {
yield [name, this.get(name)];
}
}
[Symbol.iterator]() {
return this.entries();
}
/**
* Node-fetch non-spec method
* returning all headers and their values as array
* @returns {Record<string, string[]>}
*/
raw() {
return [...this.keys()].reduce((result, key) => {
result[key] = this.getAll(key);
return result;
}, {});
}
/**
* For better console.log(headers) and also to convert Headers into Node.js Request compatible format
*/
[Symbol.for('nodejs.util.inspect.custom')]() {
return [...this.keys()].reduce((result, key) => {
const values = this.getAll(key);
// Http.request() only supports string as Host header.
// This hack makes specifying custom Host header possible.
if (key === 'host') {
result[key] = values[0];
} else {
result[key] = values.length > 1 ? values : values[0];
}
return result;
}, {});
}
}
/**
* Re-shaping object for Web IDL tests
* Only need to do it for overridden methods
*/
Object.defineProperties(
Headers.prototype,
['get', 'entries', 'forEach', 'values'].reduce((result, property) => {
result[property] = {enumerable: true};
return result;
}, {})
);
/**
* Create a Headers object from an http.IncomingMessage.rawHeaders, ignoring those that do
* not conform to HTTP grammar productions.
* @param {import('http').IncomingMessage['rawHeaders']} headers
*/
export function fromRawHeaders(headers = []) {
return new Headers(
headers
// Split into pairs
.reduce((result, value, index, array) => {
if (index % 2 === 0) {
result.push(array.slice(index, index + 2));
}
return result;
}, [])
.filter(([name, value]) => {
try {
validateHeaderName(name);
validateHeaderValue(name, String(value));
return true;
} catch {
return false;
}
})
);
}

View file

@ -0,0 +1,285 @@
/**
* 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 http from 'http';
import https from 'https';
import zlib from 'zlib';
import Stream, {PassThrough, pipeline as pump} from 'stream';
import dataUriToBuffer from 'data-uri-to-buffer';
import {writeToStream} from './body.js';
import Response from './response.js';
import Headers, {fromRawHeaders} from './headers.js';
import Request, {getNodeRequestOptions} from './request.js';
import {FetchError} from './errors/fetch-error.js';
import {AbortError} from './errors/abort-error.js';
import {isRedirect} from './utils/is-redirect.js';
export {Headers, Request, Response, FetchError, AbortError, isRedirect};
const supportedSchemas = new Set(['data:', 'http:', 'https:']);
/**
* Fetch function
*
* @param {string | URL | import('./request').default} url - Absolute url or Request instance
* @param {*} [options_] - Fetch options
* @return {Promise<import('./response').default>}
*/
export default async function fetch(url, options_) {
return new Promise((resolve, reject) => {
// Build request object
const request = new Request(url, options_);
const options = getNodeRequestOptions(request);
if (!supportedSchemas.has(options.protocol)) {
throw new TypeError(`node-fetch cannot load ${url}. URL scheme "${options.protocol.replace(/:$/, '')}" is not supported.`);
}
if (options.protocol === 'data:') {
const data = dataUriToBuffer(request.url);
const response = new Response(data, {headers: {'Content-Type': data.typeFull}});
resolve(response);
return;
}
// Wrap http.request into fetch
const send = (options.protocol === 'https:' ? https : http).request;
const {signal} = request;
let response = null;
const abort = () => {
const error = new AbortError('The operation was aborted.');
reject(error);
if (request.body && request.body instanceof Stream.Readable) {
request.body.destroy(error);
}
if (!response || !response.body) {
return;
}
response.body.emit('error', error);
};
if (signal && signal.aborted) {
abort();
return;
}
const abortAndFinalize = () => {
abort();
finalize();
};
// Send request
const request_ = send(options);
if (signal) {
signal.addEventListener('abort', abortAndFinalize);
}
const finalize = () => {
request_.abort();
if (signal) {
signal.removeEventListener('abort', abortAndFinalize);
}
};
request_.on('error', err => {
reject(new FetchError(`request to ${request.url} failed, reason: ${err.message}`, 'system', err));
finalize();
});
request_.on('response', response_ => {
request_.setTimeout(0);
const headers = fromRawHeaders(response_.rawHeaders);
// HTTP fetch step 5
if (isRedirect(response_.statusCode)) {
// HTTP fetch step 5.2
const location = headers.get('Location');
// HTTP fetch step 5.3
const locationURL = location === null ? null : new URL(location, request.url);
// 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);
/* c8 ignore next 3 */
} catch (error) {
reject(error);
}
}
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 requestOptions = {
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,
size: request.size
};
// HTTP-redirect fetch step 9
if (response_.statusCode !== 303 && request.body && options_.body instanceof Stream.Readable) {
reject(new FetchError('Cannot follow redirect with body being a readable stream', 'unsupported-redirect'));
finalize();
return;
}
// HTTP-redirect fetch step 11
if (response_.statusCode === 303 || ((response_.statusCode === 301 || response_.statusCode === 302) && request.method === 'POST')) {
requestOptions.method = 'GET';
requestOptions.body = undefined;
requestOptions.headers.delete('content-length');
}
// HTTP-redirect fetch step 15
resolve(fetch(new Request(locationURL, requestOptions)));
finalize();
return;
}
default:
// Do nothing
}
}
// Prepare response
response_.once('end', () => {
if (signal) {
signal.removeEventListener('abort', abortAndFinalize);
}
});
let body = pump(response_, new PassThrough(), error => {
reject(error);
});
// see https://github.com/nodejs/node/pull/29376
if (process.version < 'v12.10') {
response_.on('aborted', abortAndFinalize);
}
const responseOptions = {
url: request.url,
status: response_.statusCode,
statusText: response_.statusMessage,
headers,
size: request.size,
counter: request.counter,
highWaterMark: request.highWaterMark
};
// 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 || response_.statusCode === 204 || response_.statusCode === 304) {
response = new Response(body, responseOptions);
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 = pump(body, zlib.createGunzip(zlibOptions), error => {
reject(error);
});
response = new Response(body, responseOptions);
resolve(response);
return;
}
// For deflate
if (codings === 'deflate' || codings === 'x-deflate') {
// Handle the infamous raw deflate response from old servers
// a hack for old IIS and Apache servers
const raw = pump(response_, new PassThrough(), error => {
reject(error);
});
raw.once('data', chunk => {
// See http://stackoverflow.com/questions/37519828
if ((chunk[0] & 0x0F) === 0x08) {
body = pump(body, zlib.createInflate(), error => {
reject(error);
});
} else {
body = pump(body, zlib.createInflateRaw(), error => {
reject(error);
});
}
response = new Response(body, responseOptions);
resolve(response);
});
return;
}
// For br
if (codings === 'br') {
body = pump(body, zlib.createBrotliDecompress(), error => {
reject(error);
});
response = new Response(body, responseOptions);
resolve(response);
return;
}
// Otherwise, use response as-is
response = new Response(body, responseOptions);
resolve(response);
});
writeToStream(request_, request);
});
}

View file

@ -0,0 +1,224 @@
/**
* 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 {format as formatUrl} from 'url';
import Headers from './headers.js';
import Body, {clone, extractContentType, getTotalBytes} from './body.js';
import {isAbortSignal} from './utils/is.js';
import {getSearch} from './utils/get-search.js';
const INTERNALS = Symbol('Request internals');
/**
* Check if `obj` is an instance of Request.
*
* @param {*} obj
* @return {boolean}
*/
const isRequest = object => {
return (
typeof object === 'object' &&
typeof object[INTERNALS] === 'object'
);
};
/**
* Request class
*
* @param Mixed input Url or Request instance
* @param Object init Custom options
* @return Void
*/
export default class Request extends Body {
constructor(input, init = {}) {
let parsedURL;
// Normalize input and force URL to be encoded as UTF-8 (https://github.com/node-fetch/node-fetch/issues/245)
if (isRequest(input)) {
parsedURL = new URL(input.url);
} else {
parsedURL = new URL(input);
input = {};
}
let method = init.method || input.method || 'GET';
method = method.toUpperCase();
// eslint-disable-next-line no-eq-null, eqeqeq
if (((init.body != null || isRequest(input)) && input.body !== null) &&
(method === 'GET' || method === 'HEAD')) {
throw new TypeError('Request with GET/HEAD method cannot have body');
}
const inputBody = init.body ?
init.body :
(isRequest(input) && input.body !== null ?
clone(input) :
null);
super(inputBody, {
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, this);
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 ? (input.follow === undefined ? 20 : input.follow) : init.follow;
this.compress = init.compress === undefined ? (input.compress === undefined ? true : input.compress) : init.compress;
this.counter = init.counter || input.counter || 0;
this.agent = init.agent || input.agent;
this.highWaterMark = init.highWaterMark || input.highWaterMark || 16384;
this.insecureHTTPParser = init.insecureHTTPParser || input.insecureHTTPParser || false;
}
get method() {
return this[INTERNALS].method;
}
get url() {
return formatUrl(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);
}
get [Symbol.toStringTag]() {
return 'Request';
}
}
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 const getNodeRequestOptions = request => {
const {parsedURL} = request[INTERNALS];
const headers = new Headers(request[INTERNALS].headers);
// Fetch step 1.3
if (!headers.has('Accept')) {
headers.set('Accept', '*/*');
}
// 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);
// Set Content-Length if totalBytes is a number (that is not NaN)
if (typeof totalBytes === 'number' && !Number.isNaN(totalBytes)) {
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');
}
// HTTP-network-or-cache fetch step 2.15
if (request.compress && !headers.has('Accept-Encoding')) {
headers.set('Accept-Encoding', 'gzip,deflate,br');
}
let {agent} = request;
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
const search = getSearch(parsedURL);
// Manually spread the URL object instead of spread syntax
const requestOptions = {
path: parsedURL.pathname + search,
pathname: parsedURL.pathname,
hostname: parsedURL.hostname,
protocol: parsedURL.protocol,
port: parsedURL.port,
hash: parsedURL.hash,
search: parsedURL.search,
query: parsedURL.query,
href: parsedURL.href,
method: request.method,
headers: headers[Symbol.for('nodejs.util.inspect.custom')](),
insecureHTTPParser: request.insecureHTTPParser,
agent
};
return requestOptions;
};

View file

@ -0,0 +1,124 @@
/**
* Response.js
*
* Response class provides content decoding
*/
import Headers from './headers.js';
import Body, {clone, extractContentType} from './body.js';
import {isRedirect} from './utils/is-redirect.js';
const INTERNALS = Symbol('Response internals');
/**
* Response class
*
* @param Stream body Readable stream
* @param Object opts Response options
* @return Void
*/
export default class Response extends Body {
constructor(body = null, options = {}) {
super(body, options);
const status = options.status || 200;
const headers = new Headers(options.headers);
if (body !== null && !headers.has('Content-Type')) {
const contentType = extractContentType(body);
if (contentType) {
headers.append('Content-Type', contentType);
}
}
this[INTERNALS] = {
url: options.url,
status,
statusText: options.statusText || '',
headers,
counter: options.counter,
highWaterMark: options.highWaterMark
};
}
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;
}
get highWaterMark() {
return this[INTERNALS].highWaterMark;
}
/**
* Clone this response
*
* @return Response
*/
clone() {
return new Response(clone(this, this.highWaterMark), {
url: this.url,
status: this.status,
statusText: this.statusText,
headers: this.headers,
ok: this.ok,
redirected: this.redirected,
size: this.size
});
}
/**
* @param {string} url The URL that the new response is to originate from.
* @param {number} status An optional status code for the response (e.g., 302.)
* @returns {Response} A Response object.
*/
static redirect(url, status = 302) {
if (!isRedirect(status)) {
throw new RangeError('Failed to execute "redirect" on "response": Invalid status code');
}
return new Response(null, {
headers: {
location: new URL(url).toString()
},
status
});
}
get [Symbol.toStringTag]() {
return 'Response';
}
}
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}
});

View file

@ -0,0 +1,82 @@
import {randomBytes} from 'crypto';
import {isBlob} from './is.js';
const carriage = '\r\n';
const dashes = '-'.repeat(2);
const carriageLength = Buffer.byteLength(carriage);
/**
* @param {string} boundary
*/
const getFooter = boundary => `${dashes}${boundary}${dashes}${carriage.repeat(2)}`;
/**
* @param {string} boundary
* @param {string} name
* @param {*} field
*
* @return {string}
*/
function getHeader(boundary, name, field) {
let header = '';
header += `${dashes}${boundary}${carriage}`;
header += `Content-Disposition: form-data; name="${name}"`;
if (isBlob(field)) {
header += `; filename="${field.name}"${carriage}`;
header += `Content-Type: ${field.type || 'application/octet-stream'}`;
}
return `${header}${carriage.repeat(2)}`;
}
/**
* @return {string}
*/
export const getBoundary = () => randomBytes(8).toString('hex');
/**
* @param {FormData} form
* @param {string} boundary
*/
export async function * formDataIterator(form, boundary) {
for (const [name, value] of form) {
yield getHeader(boundary, name, value);
if (isBlob(value)) {
yield * value.stream();
} else {
yield value;
}
yield carriage;
}
yield getFooter(boundary);
}
/**
* @param {FormData} form
* @param {string} boundary
*/
export function getFormDataLength(form, boundary) {
let length = 0;
for (const [name, value] of form) {
length += Buffer.byteLength(getHeader(boundary, name, value));
if (isBlob(value)) {
length += value.size;
} else {
length += Buffer.byteLength(String(value));
}
length += carriageLength;
}
length += Buffer.byteLength(getFooter(boundary));
return length;
}

View file

@ -0,0 +1,9 @@
export const getSearch = parsedURL => {
if (parsedURL.search) {
return parsedURL.search;
}
const lastOffset = parsedURL.href.length - 1;
const hash = parsedURL.hash || (parsedURL.href[lastOffset] === '#' ? '#' : '');
return parsedURL.href[lastOffset - hash.length] === '?' ? '?' : '';
};

View file

@ -0,0 +1,11 @@
const redirectStatus = new Set([301, 302, 303, 307, 308]);
/**
* Redirect code matching
*
* @param {number} code - Status code
* @return {boolean}
*/
export const isRedirect = code => {
return redirectStatus.has(code);
};

View file

@ -0,0 +1,81 @@
/**
* Is.js
*
* Object type checks.
*/
const NAME = Symbol.toStringTag;
/**
* Check if `obj` is a URLSearchParams object
* ref: https://github.com/node-fetch/node-fetch/issues/296#issuecomment-307598143
*
* @param {*} obj
* @return {boolean}
*/
export const isURLSearchParameters = object => {
return (
typeof object === 'object' &&
typeof object.append === 'function' &&
typeof object.delete === 'function' &&
typeof object.get === 'function' &&
typeof object.getAll === 'function' &&
typeof object.has === 'function' &&
typeof object.set === 'function' &&
typeof object.sort === 'function' &&
object[NAME] === 'URLSearchParams'
);
};
/**
* Check if `object` is a W3C `Blob` object (which `File` inherits from)
*
* @param {*} obj
* @return {boolean}
*/
export const isBlob = object => {
return (
typeof object === 'object' &&
typeof object.arrayBuffer === 'function' &&
typeof object.type === 'string' &&
typeof object.stream === 'function' &&
typeof object.constructor === 'function' &&
/^(Blob|File)$/.test(object[NAME])
);
};
/**
* Check if `obj` is a spec-compliant `FormData` object
*
* @param {*} object
* @return {boolean}
*/
export function isFormData(object) {
return (
typeof object === 'object' &&
typeof object.append === 'function' &&
typeof object.set === 'function' &&
typeof object.get === 'function' &&
typeof object.getAll === 'function' &&
typeof object.delete === 'function' &&
typeof object.keys === 'function' &&
typeof object.values === 'function' &&
typeof object.entries === 'function' &&
typeof object.constructor === 'function' &&
object[NAME] === 'FormData'
);
}
/**
* Check if `obj` is an instance of AbortSignal.
*
* @param {*} obj
* @return {boolean}
*/
export const isAbortSignal = object => {
return (
typeof object === 'object' &&
object[NAME] === 'AbortSignal'
);
};

79
node_modules/ky-universal/package.json generated vendored Normal file
View file

@ -0,0 +1,79 @@
{
"name": "ky-universal",
"version": "0.8.2",
"description": "Use Ky in both Node.js and browsers",
"license": "MIT",
"repository": "sindresorhus/ky-universal",
"funding": "https://github.com/sindresorhus/ky-universal?sponsor=1",
"author": {
"name": "Sindre Sorhus",
"email": "sindresorhus@gmail.com",
"url": "sindresorhus.com"
},
"engines": {
"node": ">=10.17"
},
"scripts": {
"test": "xo && ava"
},
"files": [
"index.js",
"index.d.ts",
"browser.js"
],
"keywords": [
"ky",
"universal",
"isomorphic",
"browser",
"browsers",
"node",
"react",
"vue",
"ssr",
"fetch",
"request",
"requests",
"http",
"https",
"fetching",
"get",
"url",
"curl",
"wget",
"net",
"network",
"ajax",
"api",
"rest",
"xhr",
"browser",
"got",
"axios",
"node-fetch"
],
"dependencies": {
"abort-controller": "^3.0.0",
"node-fetch": "3.0.0-beta.9"
},
"devDependencies": {
"ava": "^2.4.0",
"ky": "^0.17.0",
"xo": "^0.25.3"
},
"peerDependencies": {
"ky": ">=0.17.0",
"web-streams-polyfill": ">=2.0.0"
},
"peerDependenciesMeta": {
"web-streams-polyfill": {
"optional": true
}
},
"browser": "browser.js",
"ava": {
"register": [
"."
]
}
}

120
node_modules/ky-universal/readme.md generated vendored Normal file
View file

@ -0,0 +1,120 @@
# ky-universal [![Build Status](https://travis-ci.com/sindresorhus/ky-universal.svg?branch=master)](https://travis-ci.com/sindresorhus/ky-universal)
> Use Ky in both Node.js and browsers
[Ky](https://github.com/sindresorhus/ky) is made for browsers, but this package makes it possible to use it in Node.js too, by polyfilling most of the required browser APIs using [`node-fetch`](https://github.com/bitinn/node-fetch) and [`abort-controller`](https://github.com/mysticatea/abort-controller).
This package can be useful for:
- Isomorphic code
- Web apps (React, Vue.js, etc.) that use server-side rendering (SSR)
- Testing browser libraries using a Node.js test runner
**Note:** Before opening an issue, make sure it's an issue with Ky and not its polyfills. Generally, if something works in the browser, but not in Node.js, it's an issue with `node-fetch` or `abort-controller`.
Keep in mind that Ky targets [modern browsers](https://github.com/sindresorhus/ky#browser-support) when used in the browser. For older browsers, you will need to transpile and use a [`fetch` polyfill](https://github.com/github/fetch).
**If you only target Node.js, I would strongly recommend using [Got](https://github.com/sindresorhus/got) instead.**
## Install
```
$ npm install ky ky-universal
```
*Note that you also need to install `ky`.*
## Usage
```js
const ky = require('ky-universal');
(async () => {
const parsed = await ky('https://httpbin.org/json').json();
// …
})();
```
## `ReadableStream` support
For [`ReadableStream`](https://developer.mozilla.org/en-US/docs/Web/API/ReadableStream) support, also install [`web-streams-polyfill`](https://github.com/MattiasBuelens/web-streams-polyfill):
```
$ npm install web-streams-polyfill
```
You can then use it normally:
```js
const ky = require('ky-universal');
(async () => {
const {body} = await ky('https://httpbin.org/bytes/16');
const {value} = await body.getReader().read();
const result = new TextDecoder('utf-8').decode(value);
// …
})();
```
## API
The API is exactly the same as the [Ky API](https://github.com/sindresorhus/ky#api).
## FAQ
#### How do I use this with a web app (React, Vue.js, etc.) that uses server-side rendering (SSR)?
Use it like you would use Ky:
```js
import ky from 'ky-universal';
(async () => {
const parsed = await ky('https://httpbin.org/json').json();
// …
})();
```
Webpack will ensure the polyfills are only included and used when the app is rendered on the server-side.
#### How do I test a browser library that uses Ky in AVA?
Put the following in package.json:
```json
{
"ava": {
"require": [
"ky-universal"
]
}
}
```
The library that uses Ky will now *just work* in AVA tests.
#### `clone()` hangs with a large response in Node - What should I do?
Streams in Node.js have a smaller internal buffer size (16 kB, aka `highWaterMark`) than browsers (>1 MB, not consistent across browsers). When using Ky, the default `highWaterMark` is set to 10 MB, so you shouldn't encounter many issues related to that.
However, you can specify a custom `highWaterMark` if needed:
```js
import ky from 'ky-universal';
(async () => {
const response = await ky('https://example.com', {
// 20 MB
highWaterMark: 1000 * 1000 * 20
});
const data = await response.clone().buffer();
})();
```
## Related
- [ky](https://github.com/sindresorhus/ky) - Tiny and elegant HTTP client based on the browser Fetch API
- [got](https://github.com/sindresorhus/got) - Simplified HTTP requests in Node.js