node-fetch
Advanced tools
Comparing version 3.0.0-beta.9 to 3.0.0-beta.10
{ | ||
"name": "node-fetch", | ||
"version": "3.0.0-beta.9", | ||
"version": "3.0.0-beta.10", | ||
"description": "A light-weight module that brings Fetch API to node.js", | ||
"main": "./dist/index.cjs", | ||
"module": "./src/index.js", | ||
"main": "./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" | ||
@@ -23,11 +14,9 @@ ], | ||
"engines": { | ||
"node": "^10.17 || >=12.3" | ||
"node": "^12.20.0 || ^14.13.1 || >=16.0.0" | ||
}, | ||
"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", | ||
"test": "mocha", | ||
"coverage": "c8 report --reporter=text-lcov | coveralls", | ||
"test-types": "tsd", | ||
"lint": "xo", | ||
"prepublishOnly": "node ./test/commonjs/test-artifact.js" | ||
"lint": "xo" | ||
}, | ||
@@ -60,6 +49,6 @@ "repository": { | ||
"abort-controller": "^3.0.0", | ||
"abortcontroller-polyfill": "^1.5.0", | ||
"abortcontroller-polyfill": "^1.7.1", | ||
"busboy": "^0.3.1", | ||
"c8": "^7.3.0", | ||
"chai": "^4.2.0", | ||
"c8": "^7.7.2", | ||
"chai": "^4.3.4", | ||
"chai-as-promised": "^7.1.1", | ||
@@ -69,28 +58,18 @@ "chai-iterator": "^3.0.2", | ||
"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" | ||
"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": "^2.1.1" | ||
"fetch-blob": "^3.1.2" | ||
}, | ||
"esm": { | ||
"sourceMap": true, | ||
"cjs": false | ||
}, | ||
"tsd": { | ||
"cwd": "@types", | ||
"compilerOptions": { | ||
"target": "esnext", | ||
"lib": [ | ||
"es2018" | ||
], | ||
"allowSyntheticDefaultImports": false, | ||
"esModuleInterop": false | ||
"esModuleInterop": true | ||
} | ||
@@ -103,2 +82,5 @@ }, | ||
], | ||
"ignores": [ | ||
"example.js" | ||
], | ||
"rules": { | ||
@@ -109,10 +91,11 @@ "complexity": 0, | ||
"import/no-anonymous-default-export": 0, | ||
"import/no-named-as-default": 0, | ||
"unicorn/import-index": 0, | ||
"unicorn/no-reduce": 0, | ||
"capitalized-comments": 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 | ||
}, | ||
"ignores": [ | ||
"dist", | ||
"@types" | ||
], | ||
"overrides": [ | ||
@@ -128,4 +111,6 @@ { | ||
"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, | ||
@@ -135,8 +120,2 @@ "promise/prefer-await-to-then": 0, | ||
} | ||
}, | ||
{ | ||
"files": "example.js", | ||
"rules": { | ||
"import/no-extraneous-dependencies": 0 | ||
} | ||
} | ||
@@ -143,0 +122,0 @@ ] |
400
README.md
<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> | ||
<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> | ||
@@ -59,2 +59,3 @@ <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> | ||
- [response.redirected](#responseredirected) | ||
- [response.type](#responsetype) | ||
- [Class: Headers](#class-headers) | ||
@@ -105,6 +106,6 @@ - [new Headers([init])](#new-headersinit) | ||
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 | ||
``` | ||
@@ -115,6 +116,2 @@ | ||
```js | ||
// CommonJS | ||
const fetch = require('node-fetch'); | ||
// ES Module | ||
import fetch from 'node-fetch'; | ||
@@ -126,11 +123,9 @@ ``` | ||
```js | ||
const fetch = require('node-fetch'); | ||
import fetch from 'node-fetch'; | ||
if (!globalThis.fetch) { | ||
globalThis.fetch = fetch; | ||
globalThis.fetch = fetch; | ||
} | ||
``` | ||
For versions of Node earlier than 12, use this `globalThis` [polyfill](https://mathiasbynens.be/notes/globalthis). | ||
## Upgrading | ||
@@ -151,10 +146,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); | ||
``` | ||
@@ -165,10 +158,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); | ||
``` | ||
@@ -179,10 +170,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); | ||
``` | ||
@@ -193,16 +182,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); | ||
``` | ||
@@ -217,3 +204,3 @@ | ||
```js | ||
const fetch = require('node-fetch'); | ||
import fetch from 'node-fetch'; | ||
@@ -223,8 +210,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); | ||
``` | ||
@@ -239,6 +224,6 @@ | ||
```js | ||
const fetch = require('node-fetch'); | ||
import fetch from 'node-fetch'; | ||
try { | ||
fetch('https://domain.invalid/'); | ||
await fetch('https://domain.invalid/'); | ||
} catch (error) { | ||
@@ -254,19 +239,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}`); | ||
} | ||
``` | ||
@@ -285,15 +281,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(response.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 ${response.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); | ||
} | ||
``` | ||
@@ -306,12 +350,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); | ||
``` | ||
@@ -322,13 +364,11 @@ | ||
```js | ||
const fetch = require('node-fetch'); | ||
import fetch from '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')); | ||
})(); | ||
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')); | ||
``` | ||
@@ -341,10 +381,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(response.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']); | ||
``` | ||
@@ -355,62 +393,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 | ||
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) | ||
})(); | ||
console.log(data); | ||
``` | ||
node-fetch also supports spec-compliant FormData implementations such as [formdata-node](https://github.com/octet-stream/form-data): | ||
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 | ||
```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 | ||
@@ -423,4 +431,4 @@ | ||
```js | ||
const fetch = require('node-fetch'); | ||
const AbortController = require('abort-controller'); | ||
import fetch from 'node-fetch'; | ||
import AbortController from 'abort-controller'; | ||
@@ -432,16 +440,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); | ||
} | ||
``` | ||
@@ -471,16 +475,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. | ||
insecureHTTPParser: false // Use an insecure HTTP parser that accepts invalid HTTP headers when `true`. | ||
// 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`. | ||
} | ||
@@ -518,4 +522,4 @@ ``` | ||
```js | ||
const http = require('http'); | ||
const https = require('https'); | ||
import http from 'http'; | ||
import https from 'https'; | ||
@@ -549,13 +553,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]); | ||
``` | ||
@@ -566,12 +568,11 @@ | ||
```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); | ||
``` | ||
@@ -631,5 +632,2 @@ | ||
- `Response.error()` | ||
- `Response.redirect()` | ||
- `type` | ||
- `trailer` | ||
@@ -660,2 +658,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> | ||
@@ -677,3 +681,3 @@ | ||
// Example adapted from https://fetch.spec.whatwg.org/#example-headers-class | ||
const { Headers } = require('node-fetch'); | ||
import {Headers} from 'node-fetch'; | ||
@@ -769,3 +773,3 @@ const meta = { | ||
```sh | ||
$ npm install --save-dev @types/node-fetch | ||
npm install --save-dev @types/node-fetch | ||
``` | ||
@@ -781,3 +785,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) | | ||
@@ -784,0 +788,0 @@ ###### Former |
@@ -72,6 +72,6 @@ | ||
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); | ||
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; | ||
@@ -181,3 +181,3 @@ }); | ||
if (isBlob(body)) { | ||
body = body.stream(); | ||
body = Stream.Readable.from(body.stream()); | ||
} | ||
@@ -203,5 +203,5 @@ | ||
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; | ||
const error = new FetchError(`content size at ${data.url} over limit: ${data.size}`, 'max-size'); | ||
body.destroy(error); | ||
throw error; | ||
} | ||
@@ -213,8 +213,4 @@ | ||
} 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); | ||
} | ||
const error_ = error instanceof FetchBaseError ? error : new FetchError(`Invalid response body while trying to fetch ${data.url}: ${error.message}`, 'system', error); | ||
throw error_; | ||
} | ||
@@ -378,3 +374,3 @@ | ||
// Body is Blob | ||
body.stream().pipe(dest); | ||
Stream.Readable.from(body.stream()).pipe(dest); | ||
} else if (Buffer.isBuffer(body)) { | ||
@@ -389,2 +385,1 @@ // Body is buffer | ||
}; | ||
@@ -1,3 +0,1 @@ | ||
'use strict'; | ||
export class FetchBaseError extends Error { | ||
@@ -20,2 +18,1 @@ constructor(message, type) { | ||
} | ||
@@ -14,5 +14,5 @@ /** | ||
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 error = new TypeError(`Header name must be a valid HTTP token [${name}]`); | ||
Object.defineProperty(error, 'code', {value: 'ERR_INVALID_HTTP_TOKEN'}); | ||
throw error; | ||
} | ||
@@ -25,5 +25,5 @@ }; | ||
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; | ||
const error = new TypeError(`Invalid character in header content ["${name}"]`); | ||
Object.defineProperty(error, 'code', {value: 'ERR_INVALID_CHAR'}); | ||
throw error; | ||
} | ||
@@ -119,3 +119,3 @@ }; | ||
return URLSearchParams.prototype[p].call( | ||
receiver, | ||
target, | ||
String(name).toLowerCase(), | ||
@@ -132,3 +132,3 @@ String(value) | ||
return URLSearchParams.prototype[p].call( | ||
receiver, | ||
target, | ||
String(name).toLowerCase() | ||
@@ -174,5 +174,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]); | ||
} | ||
@@ -179,0 +179,0 @@ } |
100
src/index.js
@@ -93,7 +93,31 @@ /** | ||
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_ => { | ||
@@ -120,9 +144,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); | ||
} | ||
@@ -179,3 +197,3 @@ | ||
default: | ||
// Do nothing | ||
return reject(new TypeError(`Redirect option '${request.redirect}' is not a valid value of RequestRedirect`)); | ||
} | ||
@@ -185,11 +203,9 @@ } | ||
// 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 | ||
@@ -239,5 +255,3 @@ if (process.version < 'v12.10') { | ||
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); | ||
@@ -252,16 +266,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); | ||
@@ -276,5 +280,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); | ||
@@ -293,1 +295,29 @@ resolve(response); | ||
} | ||
function fixResponseChunkedTransferBadEnding(request, errorCallback) { | ||
const LAST_CHUNK = Buffer.from('0\r\n'); | ||
let socket; | ||
request.on('socket', s => { | ||
socket = s; | ||
}); | ||
request.on('response', response => { | ||
const {headers} = response; | ||
if (headers['transfer-encoding'] === 'chunked' && !headers['content-length']) { | ||
let properLastChunkReceived = false; | ||
socket.on('data', buf => { | ||
properLastChunkReceived = Buffer.compare(buf.slice(-3), LAST_CHUNK) === 0; | ||
}); | ||
socket.prependListener('close', () => { | ||
if (!properLastChunkReceived) { | ||
const error = new Error('Premature close'); | ||
error.code = 'ERR_STREAM_PREMATURE_CLOSE'; | ||
errorCallback(error); | ||
} | ||
}); | ||
} | ||
}); | ||
} |
@@ -34,2 +34,4 @@ | ||
* | ||
* Ref: https://fetch.spec.whatwg.org/#request-class | ||
* | ||
* @param Mixed input Url or Request instance | ||
@@ -86,4 +88,5 @@ * @param Object init Custom options | ||
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'); | ||
} | ||
@@ -90,0 +93,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}, | ||
}); | ||
@@ -70,7 +70,3 @@ import {randomBytes} from 'crypto'; | ||
if (isBlob(value)) { | ||
length += value.size; | ||
} else { | ||
length += Buffer.byteLength(String(value)); | ||
} | ||
length += isBlob(value) ? value.size : Buffer.byteLength(String(value)); | ||
@@ -77,0 +73,0 @@ length += carriageLength; |
@@ -77,6 +77,8 @@ /** | ||
return ( | ||
typeof object === 'object' && | ||
object[NAME] === 'AbortSignal' | ||
typeof object === 'object' && ( | ||
object[NAME] === 'AbortSignal' || | ||
object[NAME] === 'EventTarget' | ||
) | ||
); | ||
}; | ||
New author
Supply chain riskA new npm collaborator published a version of the package for the first time. New collaborators are usually benign additions to a project, but do indicate a change to the security surface area of a package.
Found 1 instance in 1 package
16
774
5
74835
16
1494
+ Addedfetch-blob@3.2.0(transitive)
+ Addednode-domexception@1.0.0(transitive)
+ Addedweb-streams-polyfill@3.3.3(transitive)
- Removedfetch-blob@2.1.2(transitive)
Updatedfetch-blob@^3.1.2