node-fetch
Advanced tools
Comparing version 3.0.0-beta.6-exportfix to 3.0.0
240
package.json
{ | ||
"name": "node-fetch", | ||
"version": "3.0.0-beta.6-exportfix", | ||
"description": "A light-weight module that brings window.fetch to node.js", | ||
"main": "./dist/index.cjs", | ||
"module": "./src/index.js", | ||
"sideEffects": false, | ||
"type": "module", | ||
"exports": { | ||
"import": "./src/index.js", | ||
"require": "./dist/index.cjs" | ||
}, | ||
"files": [ | ||
"src", | ||
"dist", | ||
"@types/index.d.ts" | ||
], | ||
"types": "./@types/index.d.ts", | ||
"engines": { | ||
"node": ">=10.16" | ||
}, | ||
"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" | ||
}, | ||
"repository": { | ||
"type": "git", | ||
"url": "https://github.com/node-fetch/node-fetch.git" | ||
}, | ||
"keywords": [ | ||
"fetch", | ||
"http", | ||
"promise" | ||
], | ||
"author": "David Frank", | ||
"license": "MIT", | ||
"bugs": { | ||
"url": "https://github.com/node-fetch/node-fetch/issues" | ||
}, | ||
"homepage": "https://github.com/node-fetch/node-fetch", | ||
"funding": { | ||
"type": "opencollective", | ||
"url": "https://opencollective.com/node-fetch" | ||
}, | ||
"devDependencies": { | ||
"abort-controller": "^3.0.0", | ||
"abortcontroller-polyfill": "^1.4.0", | ||
"c8": "^7.1.2", | ||
"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.3.0", | ||
"form-data": "^3.0.0", | ||
"mocha": "^7.1.2", | ||
"p-timeout": "^3.2.0", | ||
"parted": "^0.1.1", | ||
"promise": "^8.1.0", | ||
"resumer": "0.0.0", | ||
"rollup": "^2.10.8", | ||
"string-to-arraybuffer": "^1.0.2", | ||
"tsc": "^1.20150623.0", | ||
"tsd": "^0.11.0", | ||
"xo": "^0.30.0" | ||
}, | ||
"dependencies": { | ||
"data-uri-to-buffer": "^3.0.0", | ||
"fetch-blob": "^1.0.6" | ||
}, | ||
"tsd": { | ||
"cwd": "@types", | ||
"compilerOptions": { | ||
"target": "esnext", | ||
"lib": [ | ||
"es2018" | ||
], | ||
"allowSyntheticDefaultImports": true | ||
} | ||
}, | ||
"xo": { | ||
"envs": [ | ||
"node", | ||
"browser" | ||
], | ||
"rules": { | ||
"complexity": 0, | ||
"import/extensions": 0, | ||
"import/no-useless-path-segments": 0, | ||
"unicorn/import-index": 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" | ||
"name": "node-fetch", | ||
"version": "3.0.0", | ||
"description": "A light-weight module that brings Fetch API to node.js", | ||
"main": "./src/index.js", | ||
"sideEffects": false, | ||
"type": "module", | ||
"files": [ | ||
"src", | ||
"@types/index.d.ts" | ||
], | ||
"types": "./@types/index.d.ts", | ||
"engines": { | ||
"node": "^12.20.0 || ^14.13.1 || >=16.0.0" | ||
}, | ||
"scripts": { | ||
"test": "mocha", | ||
"coverage": "c8 report --reporter=text-lcov | coveralls", | ||
"test-types": "tsd", | ||
"lint": "xo" | ||
}, | ||
"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.7.1", | ||
"busboy": "^0.3.1", | ||
"c8": "^7.7.2", | ||
"chai": "^4.3.4", | ||
"chai-as-promised": "^7.1.1", | ||
"chai-iterator": "^3.0.2", | ||
"chai-string": "^1.5.0", | ||
"coveralls": "^3.1.0", | ||
"delay": "^5.0.0", | ||
"form-data": "^4.0.0", | ||
"formdata-node": "^3.5.4", | ||
"mocha": "^8.3.2", | ||
"p-timeout": "^5.0.0", | ||
"tsd": "^0.14.0", | ||
"xo": "^0.39.1" | ||
}, | ||
"dependencies": { | ||
"data-uri-to-buffer": "^3.0.1", | ||
"fetch-blob": "^3.1.2" | ||
}, | ||
"tsd": { | ||
"cwd": "@types", | ||
"compilerOptions": { | ||
"esModuleInterop": true | ||
} | ||
}, | ||
"xo": { | ||
"envs": [ | ||
"node", | ||
"browser" | ||
], | ||
"ignores": [ | ||
"example.js" | ||
], | ||
"rules": { | ||
"complexity": 0, | ||
"import/extensions": 0, | ||
"import/no-useless-path-segments": 0, | ||
"import/no-anonymous-default-export": 0, | ||
"import/no-named-as-default": 0, | ||
"unicorn/import-index": 0, | ||
"unicorn/no-array-reduce": 0, | ||
"unicorn/prefer-node-protocol": 0, | ||
"unicorn/numeric-separators-style": 0, | ||
"unicorn/explicit-length-check": 0, | ||
"capitalized-comments": 0, | ||
"@typescript-eslint/member-ordering": 0 | ||
}, | ||
"overrides": [ | ||
{ | ||
"files": "test/**/*.js", | ||
"envs": [ | ||
"node", | ||
"mocha" | ||
], | ||
"rules": { | ||
"max-nested-callbacks": 0, | ||
"no-unused-expressions": 0, | ||
"no-warning-comments": 0, | ||
"new-cap": 0, | ||
"guard-for-in": 0, | ||
"unicorn/no-array-for-each": 0, | ||
"unicorn/prevent-abbreviations": 0, | ||
"promise/prefer-await-to-then": 0, | ||
"ava/no-import-test-files": 0 | ||
} | ||
} | ||
] | ||
}, | ||
"runkitExampleFilename": "example.js" | ||
} |
415
README.md
<div align="center"> | ||
<img src="docs/media/Banner.svg" alt="Node Fetch"/> | ||
<br> | ||
<p>A light-weight module that brings <code>window.fetch</code> to Node.js.</p> | ||
<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> | ||
@@ -21,2 +21,4 @@ <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> | ||
**You might be looking for the [v2 docs](https://github.com/node-fetch/node-fetch/tree/2.x#readme)** | ||
<!-- TOC --> | ||
@@ -53,2 +55,3 @@ | ||
- [Custom highWaterMark](#custom-highwatermark) | ||
- [Insecure HTTP Parser](#insecure-http-parser) | ||
- [Class: Request](#class-request) | ||
@@ -60,2 +63,3 @@ - [new Request(input[, options])](#new-requestinput-options) | ||
- [response.redirected](#responseredirected) | ||
- [response.type](#responsetype) | ||
- [Class: Headers](#class-headers) | ||
@@ -91,5 +95,5 @@ - [new Headers([init])](#new-headersinit) | ||
- 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 promise and async functions. | ||
- 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. | ||
- 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. | ||
@@ -107,6 +111,6 @@ | ||
Current stable release (`3.x`) | ||
Current stable release (`3.x`) requires at least Node.js 12.20.0. | ||
```sh | ||
$ npm install node-fetch | ||
npm install node-fetch | ||
``` | ||
@@ -117,30 +121,24 @@ | ||
```js | ||
// CommonJS | ||
const fetch = require('node-fetch'); | ||
// ES Module | ||
import fetch from 'node-fetch'; | ||
``` | ||
If you are using a Promise library other than native, set it through `fetch.Promise`: | ||
If you want to patch the global object in node: | ||
```js | ||
const fetch = require('node-fetch'); | ||
const Bluebird = require('bluebird'); | ||
import fetch from 'node-fetch'; | ||
fetch.Promise = Bluebird; | ||
if (!globalThis.fetch) { | ||
globalThis.fetch = fetch; | ||
} | ||
``` | ||
If you want to patch the global object in node: | ||
`node-fetch` is an ESM-only module - you are not able to import it with `require`. We recommend you stay on v2 which is built with CommonJS unless you use ESM yourself. We will continue to publish critical bug fixes for it. | ||
Alternatively, you can use the async `import()` function from CommonJS to load `node-fetch` asynchronously: | ||
```js | ||
const fetch = require('node-fetch'); | ||
if (!globalThis.fetch) { | ||
globalThis.fetch = fetch; | ||
} | ||
// mod.cjs | ||
const fetch = (...args) => import('node-fetch').then(({default: fetch}) => fetch(...args)); | ||
``` | ||
For versions of Node earlier than 12, use this `globalThis` [polyfill](https://mathiasbynens.be/notes/globalthis). | ||
## Upgrading | ||
@@ -161,10 +159,8 @@ | ||
```js | ||
const fetch = require('node-fetch'); | ||
import fetch from 'node-fetch'; | ||
(async () => { | ||
const response = await fetch('https://github.com/'); | ||
const body = await response.text(); | ||
const response = await fetch('https://github.com/'); | ||
const body = await response.text(); | ||
console.log(body); | ||
})(); | ||
console.log(body); | ||
``` | ||
@@ -175,10 +171,8 @@ | ||
```js | ||
const fetch = require('node-fetch'); | ||
import fetch from 'node-fetch'; | ||
(async () => { | ||
const response = await fetch('https://api.github.com/users/github'); | ||
const json = await response.json(); | ||
const response = await fetch('https://api.github.com/users/github'); | ||
const data = await response.json(); | ||
console.log(json); | ||
})(); | ||
console.log(data); | ||
``` | ||
@@ -189,10 +183,8 @@ | ||
```js | ||
const fetch = require('node-fetch'); | ||
import fetch from 'node-fetch'; | ||
(async () => { | ||
const response = await fetch('https://httpbin.org/post', {method: 'POST', body: 'a=1'}); | ||
const json = await response.json(); | ||
const response = await fetch('https://httpbin.org/post', {method: 'POST', body: 'a=1'}); | ||
const data = await response.json(); | ||
console.log(json); | ||
})(); | ||
console.log(data); | ||
``` | ||
@@ -203,16 +195,14 @@ | ||
```js | ||
const fetch = require('node-fetch'); | ||
import fetch from 'node-fetch'; | ||
(async () => { | ||
const body = {a: 1}; | ||
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(); | ||
const response = await fetch('https://httpbin.org/post', { | ||
method: 'post', | ||
body: JSON.stringify(body), | ||
headers: {'Content-Type': 'application/json'} | ||
}); | ||
const data = await response.json(); | ||
console.log(json); | ||
})(); | ||
console.log(data); | ||
``` | ||
@@ -227,3 +217,3 @@ | ||
```js | ||
const fetch = require('node-fetch'); | ||
import fetch from 'node-fetch'; | ||
@@ -233,8 +223,6 @@ const params = new URLSearchParams(); | ||
(async () => { | ||
const response = await fetch('https://httpbin.org/post', {method: 'POST', body: params}); | ||
const json = await response.json(); | ||
const response = await fetch('https://httpbin.org/post', {method: 'POST', body: params}); | ||
const data = await response.json(); | ||
console.log(json); | ||
})(); | ||
console.log(data); | ||
``` | ||
@@ -249,6 +237,6 @@ | ||
```js | ||
const fetch = require('node-fetch'); | ||
import fetch from 'node-fetch'; | ||
try { | ||
fetch('https://domain.invalid/'); | ||
await fetch('https://domain.invalid/'); | ||
} catch (error) { | ||
@@ -264,19 +252,30 @@ console.log(error); | ||
```js | ||
const fetch = require('node-fetch'); | ||
import fetch from 'node-fetch'; | ||
const checkStatus = res => { | ||
if (res.ok) { | ||
// res.status >= 200 && res.status < 300 | ||
return res; | ||
class HTTPResponseError extends Error { | ||
constructor(response, ...args) { | ||
this.response = response; | ||
super(`HTTP Error Response: ${response.status} ${response.statusText}`, ...args); | ||
} | ||
} | ||
const checkStatus = response => { | ||
if (response.ok) { | ||
// response.status >= 200 && response.status < 300 | ||
return response; | ||
} else { | ||
throw MyCustomError(res.statusText); | ||
throw new HTTPResponseError(response); | ||
} | ||
} | ||
(async () => { | ||
const response = await fetch('https://httpbin.org/status/400'); | ||
const data = checkStatus(response); | ||
const response = await fetch('https://httpbin.org/status/400'); | ||
console.log(data); //=> MyCustomError | ||
})(); | ||
try { | ||
checkStatus(response); | ||
} catch (error) { | ||
console.error(error); | ||
const errorBody = await error.response.text(); | ||
console.error(`Error body: ${errorBody}`); | ||
} | ||
``` | ||
@@ -295,15 +294,63 @@ | ||
```js | ||
const util = require('util'); | ||
const fs = require('fs'); | ||
const streamPipeline = util.promisify(require('stream').pipeline); | ||
import {createWriteStream} from 'fs'; | ||
import {pipeline} from 'stream'; | ||
import {promisify} from 'util' | ||
import fetch from 'node-fetch'; | ||
(async () => { | ||
const response = await fetch('https://assets-cdn.github.com/images/modules/logos_page/Octocat.png'); | ||
if (response.ok) { | ||
return streamPipeline(res.body, fs.createWriteStream('./octocat.png')); | ||
const streamPipeline = promisify(pipeline); | ||
const response = await fetch('https://assets-cdn.github.com/images/modules/logos_page/Octocat.png'); | ||
if (!response.ok) throw new Error(`unexpected response ${response.statusText}`); | ||
await streamPipeline(response.body, createWriteStream('./octocat.png')); | ||
``` | ||
In Node.js 14 you can also use async iterators to read `body`; however, be careful to catch | ||
errors -- the longer a response runs, the more likely it is to encounter an error. | ||
```js | ||
import fetch from 'node-fetch'; | ||
const response = await fetch('https://httpbin.org/stream/3'); | ||
try { | ||
for await (const chunk of response.body) { | ||
console.dir(JSON.parse(chunk.toString())); | ||
} | ||
} catch (err) { | ||
console.error(err.stack); | ||
} | ||
``` | ||
throw new Error(`unexpected response ${res.statusText}`); | ||
})(); | ||
In Node.js 12 you can also use async iterators to read `body`; however, async iterators with streams | ||
did not mature until Node.js 14, so you need to do some extra work to ensure you handle errors | ||
directly from the stream and wait on it response to fully close. | ||
```js | ||
import fetch from 'node-fetch'; | ||
const read = async body => { | ||
let error; | ||
body.on('error', err => { | ||
error = err; | ||
}); | ||
for await (const chunk of body) { | ||
console.dir(JSON.parse(chunk.toString())); | ||
} | ||
return new Promise((resolve, reject) => { | ||
body.on('close', () => { | ||
error ? reject(error) : resolve(); | ||
}); | ||
}); | ||
}; | ||
try { | ||
const response = await fetch('https://httpbin.org/stream/3'); | ||
await read(response.body); | ||
} catch (err) { | ||
console.error(err.stack); | ||
} | ||
``` | ||
@@ -316,12 +363,10 @@ | ||
```js | ||
const fetch = require('node-fetch'); | ||
const fileType = require('file-type'); | ||
import fetch from 'node-fetch'; | ||
import fileType from '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); | ||
})(); | ||
const response = await fetch('https://octodex.github.com/images/Fintechtocat.png'); | ||
const buffer = await response.buffer(); | ||
const type = await fileType.fromBuffer(buffer) | ||
console.log(type); | ||
``` | ||
@@ -332,13 +377,11 @@ | ||
```js | ||
const fetch = require('node-fetch'); | ||
import fetch from 'node-fetch'; | ||
(async () => { | ||
const response = await fetch('https://github.com/'); | ||
console.log(res.ok); | ||
console.log(res.status); | ||
console.log(res.statusText); | ||
console.log(res.headers.raw()); | ||
console.log(res.headers.get('content-type')); | ||
})(); | ||
const 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')); | ||
``` | ||
@@ -351,10 +394,8 @@ | ||
```js | ||
const fetch = require('node-fetch'); | ||
import fetch from '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(res.headers.raw()['set-cookie']); | ||
})(); | ||
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']); | ||
``` | ||
@@ -365,48 +406,32 @@ | ||
```js | ||
const {createReadStream} = require('fs'); | ||
const fetch = require('node-fetch'); | ||
import {createReadStream} from 'fs'; | ||
import fetch from '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) | ||
})(); | ||
const response = await fetch('https://httpbin.org/post', {method: 'POST', body: stream}); | ||
const data = await response.json(); | ||
console.log(data) | ||
``` | ||
### Post with form-data (detect multipart) | ||
node-fetch also supports spec-compliant FormData implementations such as [formdata-polyfill](https://www.npmjs.com/package/formdata-polyfill) and [formdata-node](https://github.com/octet-stream/form-data): | ||
```js | ||
const fetch = require('node-fetch'); | ||
const FormData = require('form-data'); | ||
import fetch from 'node-fetch'; | ||
import {FormData} from 'formdata-polyfill/esm-min.js'; | ||
// Alternative package: | ||
import {FormData} from 'formdata-node'; | ||
const form = new FormData(); | ||
form.append('a', 1); | ||
form.set('greeting', 'Hello, world!'); | ||
(async () => { | ||
const response = await fetch('https://httpbin.org/post', {method: 'POST', body: form}); | ||
const json = await response.json(); | ||
console.log(json) | ||
})(); | ||
const response = await fetch('https://httpbin.org/post', {method: 'POST', body: form}); | ||
const data = await response.json(); | ||
// OR, using custom headers | ||
// NOTE: getHeaders() is non-standard API | ||
console.log(data); | ||
``` | ||
const options = { | ||
method: 'POST', | ||
body: form, | ||
headers: form.getHeaders() | ||
}; | ||
node-fetch also support form-data but it's now discouraged due to not being spec-compliant and needs workarounds to function - which we hope to remove one day | ||
(async () => { | ||
const response = await fetch('https://httpbin.org/post', options); | ||
const json = await response.json(); | ||
console.log(json) | ||
})(); | ||
``` | ||
### Request cancellation with AbortSignal | ||
@@ -419,4 +444,4 @@ | ||
```js | ||
const fetch = require('node-fetch'); | ||
const AbortController = require('abort-controller'); | ||
import fetch from 'node-fetch'; | ||
import AbortController from 'abort-controller'; | ||
@@ -428,16 +453,12 @@ const controller = new AbortController(); | ||
(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); | ||
try { | ||
const response = await fetch('https://example.com', {signal: controller.signal}); | ||
const data = await response.json(); | ||
} catch (error) { | ||
if (error instanceof fetch.AbortError) { | ||
console.log('request was aborted'); | ||
} | ||
})(); | ||
} finally { | ||
clearTimeout(timeout); | ||
} | ||
``` | ||
@@ -467,15 +488,16 @@ | ||
{ | ||
// 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 | ||
// 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. | ||
// 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`. | ||
} | ||
@@ -513,4 +535,4 @@ ``` | ||
```js | ||
const http = require('http'); | ||
const https = require('https'); | ||
import http from 'http'; | ||
import https from 'https'; | ||
@@ -544,13 +566,11 @@ const httpAgent = new http.Agent({ | ||
```js | ||
const fetch = require('node-fetch'); | ||
import fetch from '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]); | ||
}); | ||
})(); | ||
const response = await fetch('https://example.com'); | ||
const r1 = await response.clone(); | ||
const results = await Promise.all([response.json(), r1.text()]); | ||
console.log(results[0]); | ||
console.log(results[1]); | ||
``` | ||
@@ -561,14 +581,18 @@ | ||
```js | ||
const fetch = require('node-fetch'); | ||
import fetch from 'node-fetch'; | ||
(async () => { | ||
const response = await fetch('https://example.com', { | ||
// About 1MB | ||
highWaterMark: 1024 * 1024 | ||
}); | ||
return res.clone().buffer(); | ||
})(); | ||
const response = await fetch('https://example.com', { | ||
// About 1MB | ||
highWaterMark: 1024 * 1024 | ||
}); | ||
const result = await res.clone().buffer(); | ||
console.dir(result); | ||
``` | ||
#### 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> | ||
@@ -621,5 +645,2 @@ | ||
- `Response.error()` | ||
- `Response.redirect()` | ||
- `type` | ||
- `trailer` | ||
@@ -650,2 +671,8 @@ | ||
#### response.type | ||
<small>_(deviation from spec)_</small> | ||
Convenience property representing the response's type. node-fetch only supports `'default'` and `'error'` and does not make use of [filtered responses](https://fetch.spec.whatwg.org/#concept-filtered-response). | ||
<a id="class-headers"></a> | ||
@@ -667,3 +694,3 @@ | ||
// Example adapted from https://fetch.spec.whatwg.org/#example-headers-class | ||
const Headers = require('node-fetch'); | ||
import {Headers} from 'node-fetch'; | ||
@@ -759,3 +786,3 @@ const meta = { | ||
```sh | ||
$ npm install --save-dev @types/node-fetch | ||
npm install --save-dev @types/node-fetch | ||
``` | ||
@@ -771,3 +798,3 @@ | ||
| ----------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------- | | ||
| [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) | | ||
| [David Frank](https://bitinn.net/) | [Jimmy Wärting](https://jimmy.warting.se/) | [Antoni Kepinski](https://kepinski.ch) | [Richie Bendall](https://www.richie-bendall.ml/) | [Gregor Martynus](https://twitter.com/gr2m) | | ||
@@ -774,0 +801,0 @@ ###### Former |
127
src/body.js
@@ -8,9 +8,12 @@ | ||
import Stream, {finished, PassThrough} from 'stream'; | ||
import Stream, {PassThrough} from 'stream'; | ||
import {types} from 'util'; | ||
import Blob from 'fetch-blob'; | ||
import FetchError from './errors/fetch-error.js'; | ||
import {isBlob, isURLSearchParameters, isAbortError} from './utils/is.js'; | ||
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'); | ||
@@ -31,2 +34,4 @@ | ||
} = {}) { | ||
let boundary = null; | ||
if (body === null) { | ||
@@ -50,2 +55,6 @@ // Body is undefined or null | ||
// 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 { | ||
@@ -59,2 +68,3 @@ // None of the above | ||
body, | ||
boundary, | ||
disturbed: false, | ||
@@ -66,6 +76,6 @@ error: null | ||
if (body instanceof Stream) { | ||
body.on('error', err => { | ||
const error = isAbortError(err) ? | ||
err : | ||
new FetchError(`Invalid response body while trying to fetch ${this.url}: ${err.message}`, 'system', err); | ||
body.on('error', error_ => { | ||
const error = error_ instanceof FetchBaseError ? | ||
error_ : | ||
new FetchError(`Invalid response body while trying to fetch ${this.url}: ${error_.message}`, 'system', error_); | ||
this[INTERNALS].error = error; | ||
@@ -101,7 +111,6 @@ }); | ||
const ct = (this.headers && this.headers.get('content-type')) || (this[INTERNALS].body && this[INTERNALS].body.type) || ''; | ||
const buf = await consumeBody(this); | ||
const buf = await this.buffer(); | ||
return new Blob([], { | ||
type: ct.toLowerCase(), | ||
buffer: buf | ||
return new Blob([buf], { | ||
type: ct | ||
}); | ||
@@ -155,7 +164,7 @@ } | ||
* | ||
* @return Promise | ||
* @return Promise | ||
*/ | ||
const consumeBody = data => { | ||
async function consumeBody(data) { | ||
if (data[INTERNALS].disturbed) { | ||
return Body.Promise.reject(new TypeError(`body used already for: ${data.url}`)); | ||
throw new TypeError(`body used already for: ${data.url}`); | ||
} | ||
@@ -166,3 +175,3 @@ | ||
if (data[INTERNALS].error) { | ||
return Body.Promise.reject(data[INTERNALS].error); | ||
throw data[INTERNALS].error; | ||
} | ||
@@ -174,3 +183,3 @@ | ||
if (body === null) { | ||
return Body.Promise.resolve(Buffer.alloc(0)); | ||
return Buffer.alloc(0); | ||
} | ||
@@ -180,3 +189,3 @@ | ||
if (isBlob(body)) { | ||
body = body.stream(); | ||
body = Stream.Readable.from(body.stream()); | ||
} | ||
@@ -186,3 +195,3 @@ | ||
if (Buffer.isBuffer(body)) { | ||
return Body.Promise.resolve(body); | ||
return body; | ||
} | ||
@@ -192,3 +201,3 @@ | ||
if (!(body instanceof Stream)) { | ||
return Body.Promise.resolve(Buffer.alloc(0)); | ||
return Buffer.alloc(0); | ||
} | ||
@@ -200,46 +209,34 @@ | ||
let accumBytes = 0; | ||
let abort = false; | ||
return new Body.Promise((resolve, reject) => { | ||
body.on('data', chunk => { | ||
if (abort || chunk === null) { | ||
return; | ||
try { | ||
for await (const chunk of body) { | ||
if (data.size > 0 && accumBytes + chunk.length > data.size) { | ||
const error = new FetchError(`content size at ${data.url} over limit: ${data.size}`, 'max-size'); | ||
body.destroy(error); | ||
throw error; | ||
} | ||
if (data.size && accumBytes + chunk.length > data.size) { | ||
abort = true; | ||
reject(new FetchError(`content size at ${data.url} over limit: ${data.size}`, 'max-size')); | ||
return; | ||
} | ||
accumBytes += chunk.length; | ||
accum.push(chunk); | ||
}); | ||
} | ||
} catch (error) { | ||
const error_ = error instanceof FetchBaseError ? error : new FetchError(`Invalid response body while trying to fetch ${data.url}: ${error.message}`, 'system', error); | ||
throw error_; | ||
} | ||
finished(body, {writable: false}, err => { | ||
if (err) { | ||
if (isAbortError(err)) { | ||
// If the request was aborted, reject with this Error | ||
abort = true; | ||
reject(err); | ||
} else { | ||
// Other errors, such as incorrect content-encoding | ||
reject(new FetchError(`Invalid response body while trying to fetch ${data.url}: ${err.message}`, 'system', err)); | ||
} | ||
} else { | ||
if (abort) { | ||
return; | ||
} | ||
try { | ||
resolve(Buffer.concat(accum, accumBytes)); | ||
} catch (error) { | ||
// Handle streams that have accumulated too much data (issue #414) | ||
reject(new FetchError(`Could not create Buffer from response body for ${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}`); | ||
} | ||
} | ||
/** | ||
@@ -288,3 +285,3 @@ * Clone body given Res/Req instance | ||
*/ | ||
export const extractContentType = body => { | ||
export const extractContentType = (body, request) => { | ||
// Body is null or undefined | ||
@@ -320,2 +317,6 @@ if (body === null) { | ||
if (isFormData(body)) { | ||
return `multipart/form-data; boundary=${request[INTERNALS].boundary}`; | ||
} | ||
// Body is stream - can't really do much about this | ||
@@ -339,3 +340,5 @@ if (body instanceof Stream) { | ||
*/ | ||
export const getTotalBytes = ({body}) => { | ||
export const getTotalBytes = request => { | ||
const {body} = request; | ||
// Body is null or undefined | ||
@@ -361,2 +364,7 @@ if (body === null) { | ||
// Body is a spec-compliant form-data | ||
if (isFormData(body)) { | ||
return getFormDataLength(request[INTERNALS].boundary); | ||
} | ||
// Body is stream | ||
@@ -379,3 +387,3 @@ return null; | ||
// Body is Blob | ||
body.stream().pipe(dest); | ||
Stream.Readable.from(body.stream()).pipe(dest); | ||
} else if (Buffer.isBuffer(body)) { | ||
@@ -390,4 +398,1 @@ // Body is buffer | ||
}; | ||
// Expose Promise | ||
Body.Promise = global.Promise; |
@@ -0,27 +1,10 @@ | ||
import {FetchBaseError} from './base.js'; | ||
/** | ||
* Abort-error.js | ||
* | ||
* AbortError interface for cancelled requests | ||
*/ | ||
/** | ||
* Create AbortError instance | ||
* | ||
* @param String message Error message for human | ||
* @param String type Error type for machine | ||
* @param String systemError For Node.js system error | ||
* @return AbortError | ||
*/ | ||
export default class AbortError extends Error { | ||
constructor(message) { | ||
super(message); | ||
this.type = 'aborted'; | ||
this.message = message; | ||
this.name = 'AbortError'; | ||
this[Symbol.toStringTag] = 'AbortError'; | ||
// Hide custom error implementation details from end-users | ||
Error.captureStackTrace(this, this.constructor); | ||
export class AbortError extends FetchBaseError { | ||
constructor(message, type = 'aborted') { | ||
super(message, type); | ||
} | ||
} |
@@ -0,24 +1,19 @@ | ||
import {FetchBaseError} from './base.js'; | ||
/** | ||
* Fetch-error.js | ||
* | ||
* FetchError interface for operational errors | ||
*/ | ||
* @typedef {{ address?: string, code: string, dest?: string, errno: number, info?: object, message: string, path?: string, port?: number, syscall: string}} SystemError | ||
*/ | ||
/** | ||
* Create FetchError instance | ||
* | ||
* @param String message Error message for human | ||
* @param String type Error type for machine | ||
* @param Object systemError For Node.js system error | ||
* @return FetchError | ||
* FetchError interface for operational errors | ||
*/ | ||
export default class FetchError extends Error { | ||
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); | ||
this.message = message; | ||
this.type = type; | ||
this.name = 'FetchError'; | ||
this[Symbol.toStringTag] = 'FetchError'; | ||
super(message, type); | ||
// When err.type is `system`, err.erroredSysCall contains system error and err.code contains system error code | ||
@@ -28,8 +23,5 @@ if (systemError) { | ||
this.code = this.errno = systemError.code; | ||
this.erroredSysCall = systemError; | ||
this.erroredSysCall = systemError.syscall; | ||
} | ||
// Hide custom error implementation details from end-users | ||
Error.captureStackTrace(this, this.constructor); | ||
} | ||
} |
@@ -8,22 +8,26 @@ /** | ||
import {types} from 'util'; | ||
import http from 'http'; | ||
const invalidTokenRegex = /[^`\-\w!#$%&'*+.|~]/; | ||
const invalidHeaderCharRegex = /[^\t\u0020-\u007E\u0080-\u00FF]/; | ||
const validateHeaderName = typeof http.validateHeaderName === 'function' ? | ||
http.validateHeaderName : | ||
name => { | ||
if (!/^[\^`\-\w!#$%&'*+.|~]+$/.test(name)) { | ||
const error = new TypeError(`Header name must be a valid HTTP token [${name}]`); | ||
Object.defineProperty(error, 'code', {value: 'ERR_INVALID_HTTP_TOKEN'}); | ||
throw error; | ||
} | ||
}; | ||
function validateName(name) { | ||
name = String(name); | ||
if (invalidTokenRegex.test(name) || name === '') { | ||
throw new TypeError(`'${name}' is not a legal HTTP header name`); | ||
} | ||
} | ||
const validateHeaderValue = typeof http.validateHeaderValue === 'function' ? | ||
http.validateHeaderValue : | ||
(name, value) => { | ||
if (/[^\t\u0020-\u007E\u0080-\u00FF]/.test(value)) { | ||
const error = new TypeError(`Invalid character in header content ["${name}"]`); | ||
Object.defineProperty(error, 'code', {value: 'ERR_INVALID_CHAR'}); | ||
throw error; | ||
} | ||
}; | ||
function validateValue(value) { | ||
value = String(value); | ||
if (invalidHeaderCharRegex.test(value)) { | ||
throw new TypeError(`'${value}' is not a legal HTTP header value`); | ||
} | ||
} | ||
/** | ||
* @typedef {Headers | Record<string, string> | Iterable<readonly [string, string]> | Iterable<string>[]} HeadersInit | ||
* @typedef {Headers | Record<string, string> | Iterable<readonly [string, string]> | Iterable<Iterable<string>>} HeadersInit | ||
*/ | ||
@@ -95,5 +99,5 @@ | ||
result.map(([name, value]) => { | ||
validateName(name); | ||
validateValue(value); | ||
return [String(name).toLowerCase(), value]; | ||
validateHeaderName(name); | ||
validateHeaderValue(name, String(value)); | ||
return [String(name).toLowerCase(), String(value)]; | ||
}) : | ||
@@ -112,8 +116,8 @@ undefined; | ||
return (name, value) => { | ||
validateName(name); | ||
validateValue(value); | ||
validateHeaderName(name); | ||
validateHeaderValue(name, String(value)); | ||
return URLSearchParams.prototype[p].call( | ||
receiver, | ||
target, | ||
String(name).toLowerCase(), | ||
value | ||
String(value) | ||
); | ||
@@ -126,5 +130,5 @@ }; | ||
return name => { | ||
validateName(name); | ||
validateHeaderName(name); | ||
return URLSearchParams.prototype[p].call( | ||
receiver, | ||
target, | ||
String(name).toLowerCase() | ||
@@ -149,3 +153,3 @@ ); | ||
get [Symbol.toStringTag]() { | ||
return 'Headers'; | ||
return this.constructor.name; | ||
} | ||
@@ -171,5 +175,5 @@ | ||
forEach(callback) { | ||
forEach(callback, thisArg = undefined) { | ||
for (const name of this.keys()) { | ||
callback(this.get(name), name); | ||
Reflect.apply(callback, thisArg, [this.get(name), name, this]); | ||
} | ||
@@ -256,5 +260,13 @@ } | ||
}, []) | ||
.filter(([name, value]) => !(invalidTokenRegex.test(name) || invalidHeaderCharRegex.test(value))) | ||
.filter(([name, value]) => { | ||
try { | ||
validateHeaderName(name); | ||
validateHeaderValue(name, String(value)); | ||
return true; | ||
} catch { | ||
return false; | ||
} | ||
}) | ||
); | ||
} |
180
src/index.js
@@ -13,10 +13,10 @@ /** | ||
import Stream, {PassThrough, pipeline as pump} from 'stream'; | ||
import dataURIToBuffer from 'data-uri-to-buffer'; | ||
import dataUriToBuffer from 'data-uri-to-buffer'; | ||
import Body, {writeToStream, getTotalBytes} from './body.js'; | ||
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 {FetchError} from './errors/fetch-error.js'; | ||
import {AbortError} from './errors/abort-error.js'; | ||
import {isRedirect} from './utils/is-redirect.js'; | ||
@@ -26,39 +26,28 @@ | ||
const supportedSchemas = new Set(['data:', 'http:', 'https:']); | ||
/** | ||
* Fetch function | ||
* | ||
* @param Mixed url Absolute url or Request instance | ||
* @param Object opts Fetch options | ||
* @return Promise | ||
* @param {string | URL | import('./request').default} url - Absolute url or Request instance | ||
* @param {*} [options_] - Fetch options | ||
* @return {Promise<import('./response').default>} | ||
*/ | ||
export default function fetch(url, options_) { | ||
// Allow custom promise | ||
if (!fetch.Promise) { | ||
throw new Error('native promise missing, set fetch.Promise to your favorite alternative'); | ||
} | ||
// Regex for data uri | ||
const dataUriRegex = /^\s*data:([a-z]+\/[a-z]+(;[a-z-]+=[a-z-]+)?)?(;base64)?,[\w!$&',()*+;=\-.~:@/?%\s]*\s*$/i; | ||
// If valid data uri | ||
if (dataUriRegex.test(url)) { | ||
const data = dataURIToBuffer(url); | ||
const response = new Response(data, {headers: {'Content-Type': data.type}}); | ||
return fetch.Promise.resolve(response); | ||
} | ||
// If invalid data uri | ||
if (url.toString().startsWith('data:')) { | ||
const request = new Request(url, options_); | ||
return fetch.Promise.reject(new FetchError(`[${request.method}] ${request.url} invalid URL`, 'system')); | ||
} | ||
Body.Promise = fetch.Promise; | ||
// Wrap http.request into fetch | ||
return new fetch.Promise((resolve, reject) => { | ||
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; | ||
@@ -106,7 +95,31 @@ const {signal} = request; | ||
request_.on('error', err => { | ||
reject(new FetchError(`request to ${request.url} failed, reason: ${err.message}`, 'system', err)); | ||
request_.on('error', error => { | ||
reject(new FetchError(`request to ${request.url} failed, reason: ${error.message}`, 'system', error)); | ||
finalize(); | ||
}); | ||
fixResponseChunkedTransferBadEnding(request_, error => { | ||
response.body.destroy(error); | ||
}); | ||
/* c8 ignore next 18 */ | ||
if (process.version < 'v14') { | ||
// Before Node.js 14, pipeline() does not fully support async iterators and does not always | ||
// properly handle when the socket close/end events are out of order. | ||
request_.on('socket', s => { | ||
let endedWithEventsCount; | ||
s.prependListener('end', () => { | ||
endedWithEventsCount = s._eventsCount; | ||
}); | ||
s.prependListener('close', hadError => { | ||
// if end happened before close but the socket didn't emit an error, do it now | ||
if (response && endedWithEventsCount < s._eventsCount && !hadError) { | ||
const error = new Error('Premature close'); | ||
error.code = 'ERR_STREAM_PREMATURE_CLOSE'; | ||
response.body.emit('error', error); | ||
} | ||
}); | ||
}); | ||
} | ||
request_.on('response', response_ => { | ||
@@ -133,9 +146,3 @@ request_.setTimeout(0); | ||
if (locationURL !== null) { | ||
// Handle corrupted header | ||
try { | ||
headers.set('Location', locationURL); | ||
/* c8 ignore next 3 */ | ||
} catch (error) { | ||
reject(error); | ||
} | ||
headers.set('Location', locationURL); | ||
} | ||
@@ -167,7 +174,8 @@ | ||
body: request.body, | ||
signal: request.signal | ||
signal: request.signal, | ||
size: request.size | ||
}; | ||
// HTTP-redirect fetch step 9 | ||
if (response_.statusCode !== 303 && request.body && getTotalBytes(request) === null) { | ||
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')); | ||
@@ -192,3 +200,3 @@ finalize(); | ||
default: | ||
// Do nothing | ||
return reject(new TypeError(`Redirect option '${request.redirect}' is not a valid value of RequestRedirect`)); | ||
} | ||
@@ -198,11 +206,13 @@ } | ||
// Prepare response | ||
response_.once('end', () => { | ||
if (signal) { | ||
if (signal) { | ||
response_.once('end', () => { | ||
signal.removeEventListener('abort', abortAndFinalize); | ||
} | ||
}); | ||
}); | ||
} | ||
let body = pump(response_, new PassThrough(), error => { | ||
reject(error); | ||
}); | ||
let body = pump(response_, new PassThrough(), reject); | ||
// see https://github.com/nodejs/node/pull/29376 | ||
if (process.version < 'v12.10') { | ||
response_.on('aborted', abortAndFinalize); | ||
} | ||
@@ -248,5 +258,3 @@ const responseOptions = { | ||
if (codings === 'gzip' || codings === 'x-gzip') { | ||
body = pump(body, zlib.createGunzip(zlibOptions), error => { | ||
reject(error); | ||
}); | ||
body = pump(body, zlib.createGunzip(zlibOptions), reject); | ||
response = new Response(body, responseOptions); | ||
@@ -261,16 +269,6 @@ resolve(response); | ||
// a hack for old IIS and Apache servers | ||
const raw = pump(response_, new PassThrough(), error => { | ||
reject(error); | ||
}); | ||
const raw = pump(response_, new PassThrough(), reject); | ||
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); | ||
}); | ||
} | ||
body = (chunk[0] & 0x0F) === 0x08 ? pump(body, zlib.createInflate(), reject) : pump(body, zlib.createInflateRaw(), reject); | ||
@@ -285,5 +283,3 @@ response = new Response(body, responseOptions); | ||
if (codings === 'br') { | ||
body = pump(body, zlib.createBrotliDecompress(), error => { | ||
reject(error); | ||
}); | ||
body = pump(body, zlib.createBrotliDecompress(), reject); | ||
response = new Response(body, responseOptions); | ||
@@ -303,3 +299,43 @@ resolve(response); | ||
// Expose Promise | ||
fetch.Promise = global.Promise; | ||
function fixResponseChunkedTransferBadEnding(request, errorCallback) { | ||
const LAST_CHUNK = Buffer.from('0\r\n\r\n'); | ||
let isChunkedTransfer = false; | ||
let properLastChunkReceived = false; | ||
let previousChunk; | ||
request.on('response', response => { | ||
const {headers} = response; | ||
isChunkedTransfer = headers['transfer-encoding'] === 'chunked' && !headers['content-length']; | ||
}); | ||
request.on('socket', socket => { | ||
const onSocketClose = () => { | ||
if (isChunkedTransfer && !properLastChunkReceived) { | ||
const error = new Error('Premature close'); | ||
error.code = 'ERR_STREAM_PREMATURE_CLOSE'; | ||
errorCallback(error); | ||
} | ||
}; | ||
socket.prependListener('close', onSocketClose); | ||
request.on('abort', () => { | ||
socket.removeListener('close', onSocketClose); | ||
}); | ||
socket.on('data', buf => { | ||
properLastChunkReceived = Buffer.compare(buf.slice(-5), LAST_CHUNK) === 0; | ||
// Sometimes final 0-length chunk and end of message code are in separate packets | ||
if (!properLastChunkReceived && previousChunk) { | ||
properLastChunkReceived = ( | ||
Buffer.compare(previousChunk.slice(-3), LAST_CHUNK.slice(0, 3)) === 0 && | ||
Buffer.compare(buf.slice(-2), LAST_CHUNK.slice(3)) === 0 | ||
); | ||
} | ||
previousChunk = buf; | ||
}); | ||
}); | ||
} |
@@ -32,24 +32,6 @@ | ||
/** | ||
* Wrapper around `new URL` to handle relative URLs (https://github.com/nodejs/node/issues/12682) | ||
* | ||
* @param {string} urlStr | ||
* @return {void} | ||
*/ | ||
const parseURL = urlString => { | ||
/* | ||
Check whether the URL is absolute or not | ||
Scheme: https://tools.ietf.org/html/rfc3986#section-3.1 | ||
Absolute URL: https://tools.ietf.org/html/rfc3986#section-4.3 | ||
*/ | ||
if (/^[a-zA-Z][a-zA-Z\d+\-.]*:/.exec(urlString)) { | ||
return new URL(urlString); | ||
} | ||
throw new TypeError('Only absolute URLs are supported'); | ||
}; | ||
/** | ||
* Request class | ||
* | ||
* Ref: https://fetch.spec.whatwg.org/#request-class | ||
* | ||
* @param Mixed input Url or Request instance | ||
@@ -63,16 +45,7 @@ * @param Object init Custom options | ||
// Normalize input and force URL to be encoded as UTF-8 (https://github.com/bitinn/node-fetch/issues/245) | ||
// Normalize input and force URL to be encoded as UTF-8 (https://github.com/node-fetch/node-fetch/issues/245) | ||
if (isRequest(input)) { | ||
parsedURL = parseURL(input.url); | ||
parsedURL = new URL(input.url); | ||
} else { | ||
if (input && input.href) { | ||
// In order to support Node.js' Url objects; though WHATWG's URL objects | ||
// will fall into this branch also (since their `toString()` will return | ||
// `href` property anyway) | ||
parsedURL = parseURL(input.href); | ||
} else { | ||
// Coerce input to a string before attempting to parse | ||
parsedURL = parseURL(`${input}`); | ||
} | ||
parsedURL = new URL(input); | ||
input = {}; | ||
@@ -103,3 +76,3 @@ } | ||
if (inputBody !== null && !headers.has('Content-Type')) { | ||
const contentType = extractContentType(inputBody); | ||
const contentType = extractContentType(inputBody, this); | ||
if (contentType) { | ||
@@ -117,4 +90,5 @@ headers.append('Content-Type', contentType); | ||
if (signal !== null && !isAbortSignal(signal)) { | ||
throw new TypeError('Expected signal to be an instanceof AbortSignal'); | ||
// eslint-disable-next-line no-eq-null, eqeqeq | ||
if (signal != null && !isAbortSignal(signal)) { | ||
throw new TypeError('Expected signal to be an instanceof AbortSignal or EventTarget'); | ||
} | ||
@@ -136,2 +110,3 @@ | ||
this.highWaterMark = init.highWaterMark || input.highWaterMark || 16384; | ||
this.insecureHTTPParser = init.insecureHTTPParser || input.insecureHTTPParser || false; | ||
} | ||
@@ -197,6 +172,2 @@ | ||
if (!/^https?:$/.test(parsedURL.protocol)) { | ||
throw new TypeError('Only HTTP(S) protocols are supported'); | ||
} | ||
// HTTP-network-or-cache fetch steps 2.4-2.7 | ||
@@ -210,3 +181,4 @@ let contentLengthValue = null; | ||
const totalBytes = getTotalBytes(request); | ||
if (typeof totalBytes === 'number') { | ||
// Set Content-Length if totalBytes is a number (that is not NaN) | ||
if (typeof totalBytes === 'number' && !Number.isNaN(totalBytes)) { | ||
contentLengthValue = String(totalBytes); | ||
@@ -257,2 +229,3 @@ } | ||
headers: headers[Symbol.for('nodejs.util.inspect.custom')](), | ||
insecureHTTPParser: request.insecureHTTPParser, | ||
agent | ||
@@ -259,0 +232,0 @@ }; |
@@ -16,2 +16,4 @@ /** | ||
* | ||
* Ref: https://fetch.spec.whatwg.org/#response-class | ||
* | ||
* @param Stream body Readable stream | ||
@@ -25,3 +27,5 @@ * @param Object opts Response options | ||
const status = options.status || 200; | ||
// eslint-disable-next-line no-eq-null, eqeqeq, no-negated-condition | ||
const status = options.status != null ? options.status : 200; | ||
const headers = new Headers(options.headers); | ||
@@ -37,2 +41,3 @@ | ||
this[INTERNALS] = { | ||
type: 'default', | ||
url: options.url, | ||
@@ -47,2 +52,6 @@ status, | ||
get type() { | ||
return this[INTERNALS].type; | ||
} | ||
get url() { | ||
@@ -86,2 +95,3 @@ return this[INTERNALS].url || ''; | ||
return new Response(clone(this, this.highWaterMark), { | ||
type: this.type, | ||
url: this.url, | ||
@@ -115,2 +125,8 @@ status: this.status, | ||
static error() { | ||
const response = new Response(null, {status: 0, statusText: ''}); | ||
response[INTERNALS].type = 'error'; | ||
return response; | ||
} | ||
get [Symbol.toStringTag]() { | ||
@@ -122,2 +138,3 @@ return 'Response'; | ||
Object.defineProperties(Response.prototype, { | ||
type: {enumerable: true}, | ||
url: {enumerable: true}, | ||
@@ -131,2 +148,1 @@ status: {enumerable: true}, | ||
}); | ||
@@ -31,3 +31,3 @@ /** | ||
/** | ||
* Check if `obj` is a W3C `Blob` object (which `File` inherits from) | ||
* Check if `object` is a W3C `Blob` object (which `File` inherits from) | ||
* | ||
@@ -49,16 +49,25 @@ * @param {*} obj | ||
/** | ||
* Check if `obj` is an instance of AbortSignal. | ||
* Check if `obj` is a spec-compliant `FormData` object | ||
* | ||
* @param {*} obj | ||
* @param {*} object | ||
* @return {boolean} | ||
*/ | ||
export const isAbortSignal = object => { | ||
export function isFormData(object) { | ||
return ( | ||
typeof object === 'object' && | ||
object[NAME] === 'AbortSignal' | ||
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 AbortError. | ||
* Check if `obj` is an instance of AbortSignal. | ||
* | ||
@@ -68,4 +77,10 @@ * @param {*} obj | ||
*/ | ||
export const isAbortError = object => { | ||
return object[NAME] === 'AbortError'; | ||
export const isAbortSignal = object => { | ||
return ( | ||
typeof object === 'object' && ( | ||
object[NAME] === 'AbortSignal' || | ||
object[NAME] === 'EventTarget' | ||
) | ||
); | ||
}; | ||
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
Deprecated
MaintenanceThe maintainer of the package marked it as deprecated. This could indicate that a single version should not be used, or that the package is no longer maintained and any new vulnerabilities will not be fixed.
Found 1 instance in 1 package
No v1
QualityPackage is not semver >=1. This means it is not stable and does not support ^ ranges.
Found 1 instance in 1 package
16
0
0
785
5
75904
1505
+ Addedfetch-blob@3.2.0(transitive)
+ Addednode-domexception@1.0.0(transitive)
+ Addedweb-streams-polyfill@3.3.3(transitive)
- Removedfetch-blob@1.0.7(transitive)
Updateddata-uri-to-buffer@^3.0.1
Updatedfetch-blob@^3.1.2