node-fetch
Advanced tools
Comparing version 3.0.0-beta.6 to 3.0.0-beta.7
@@ -5,8 +5,15 @@ /// <reference types="node" /> | ||
import {Agent} from 'http'; | ||
import {AbortSignal} from 'abort-controller'; | ||
import Blob from 'fetch-blob'; | ||
import { Agent } from 'http'; | ||
import { URL, URLSearchParams } from 'url' | ||
import Blob = require('fetch-blob'); | ||
type HeadersInit = Headers | string[][] | Record<string, string>; | ||
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>>; | ||
/** | ||
@@ -19,34 +26,32 @@ * This Fetch API interface allows you to perform various actions on HTTP request and response headers. | ||
* */ | ||
interface Headers { | ||
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: ( | ||
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; | ||
): void; | ||
[Symbol.iterator]: () => IterableIterator<[string, string]>; | ||
[Symbol.iterator](): IterableIterator<[string, string]>; | ||
/** | ||
* Returns an iterator allowing to go through all key/value pairs contained in this object. | ||
*/ | ||
entries: () => IterableIterator<[string, string]>; | ||
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>; | ||
keys(): IterableIterator<string>; | ||
/** | ||
* Returns an iterator allowing to go through all values of the key/value pairs contained in this object. | ||
*/ | ||
values: () => IterableIterator<string>; | ||
values(): IterableIterator<string>; | ||
/** Node-fetch extension */ | ||
raw: () => Record<string, string[]>; | ||
raw(): Record<string, string[]>; | ||
} | ||
declare var Headers: { | ||
prototype: Headers; | ||
new (init?: HeadersInit): Headers; | ||
}; | ||
@@ -99,19 +104,22 @@ interface RequestInit { | ||
| string; | ||
interface Body { | ||
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>; | ||
buffer(): Promise<Buffer>; | ||
arrayBuffer(): Promise<ArrayBuffer>; | ||
blob(): Promise<Blob>; | ||
json(): Promise<unknown>; | ||
text(): Promise<string>; | ||
} | ||
declare var Body: { | ||
prototype: Body; | ||
new (body?: BodyInit, opts?: {size?: number}): Body; | ||
}; | ||
type RequestRedirect = 'error' | 'follow' | 'manual'; | ||
interface Request extends Body { | ||
type RequestInfo = string | Body; | ||
declare class Request extends Body { | ||
constructor(input: RequestInfo, init?: RequestInit); | ||
/** | ||
@@ -137,11 +145,8 @@ * 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 url: string; | ||
clone: () => Request; | ||
clone(): Request; | ||
} | ||
type RequestInfo = string | Body; | ||
declare var Request: { | ||
prototype: Request; | ||
new (input: RequestInfo, init?: RequestInit): Request; | ||
}; | ||
interface Response extends Body { | ||
declare class Response extends Body { | ||
constructor(body?: BodyInit | null, init?: ResponseInit); | ||
readonly headers: Headers; | ||
@@ -153,17 +158,8 @@ readonly ok: boolean; | ||
readonly url: string; | ||
clone: () => Response; | ||
clone(): Response; | ||
} | ||
declare var Response: { | ||
prototype: Response; | ||
new (body?: BodyInit | null, init?: ResponseInit): Response; | ||
}; | ||
declare class FetchError extends Error { | ||
constructor(message: string, type: string, systemError?: object); | ||
declare function fetch(url: RequestInfo, init?: RequestInit): Promise<Response>; | ||
declare namespace fetch { | ||
function isRedirect(code: number): boolean; | ||
} | ||
interface FetchError extends Error { | ||
name: 'FetchError'; | ||
@@ -175,8 +171,4 @@ [Symbol.toStringTag]: 'FetchError'; | ||
} | ||
declare var FetchError: { | ||
prototype: FetchError; | ||
new (message: string, type: string, systemError?: object): FetchError; | ||
}; | ||
export class AbortError extends Error { | ||
declare class AbortError extends Error { | ||
type: string; | ||
@@ -187,3 +179,31 @@ name: 'AbortError'; | ||
export {Headers, Request, Response, FetchError}; | ||
export default fetch; | ||
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; |
263
package.json
{ | ||
"name": "node-fetch", | ||
"version": "3.0.0-beta.6", | ||
"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-beta.7", | ||
"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.17" | ||
}, | ||
"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" | ||
], | ||
"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", | ||
"busboy": "^0.3.1", | ||
"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", | ||
"formdata-node": "^2.2.0", | ||
"mocha": "^8.0.0", | ||
"p-timeout": "^3.2.0", | ||
"parted": "^0.1.1", | ||
"rollup": "^2.15.0", | ||
"string-to-arraybuffer": "^1.0.2", | ||
"tsd": "^0.11.0", | ||
"xo": "^0.32.0" | ||
}, | ||
"dependencies": { | ||
"data-uri-to-buffer": "^3.0.1", | ||
"fetch-blob": "^2.0.0" | ||
}, | ||
"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, | ||
"node/no-unsupported-features/node-builtins": [ | ||
"error", | ||
{ | ||
"ignores": [ | ||
"stream.Readable.from" | ||
] | ||
} | ||
] | ||
}, | ||
"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" | ||
} |
@@ -88,5 +88,5 @@ <div align="center"> | ||
- 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. | ||
@@ -120,11 +120,2 @@ | ||
If you are using a Promise library other than native, set it through `fetch.Promise`: | ||
```js | ||
const fetch = require('node-fetch'); | ||
const Bluebird = require('bluebird'); | ||
fetch.Promise = Bluebird; | ||
``` | ||
If you want to patch the global object in node: | ||
@@ -291,6 +282,6 @@ | ||
if (response.ok) { | ||
return streamPipeline(res.body, fs.createWriteStream('./octocat.png')); | ||
return streamPipeline(response.body, fs.createWriteStream('./octocat.png')); | ||
} | ||
throw new Error(`unexpected response ${res.statusText}`); | ||
throw new Error(`unexpected response ${response.statusText}`); | ||
})(); | ||
@@ -324,7 +315,7 @@ ``` | ||
console.log(res.ok); | ||
console.log(res.status); | ||
console.log(res.statusText); | ||
console.log(res.headers.raw()); | ||
console.log(res.headers.get('content-type')); | ||
console.log(response.ok); | ||
console.log(response.status); | ||
console.log(response.statusText); | ||
console.log(response.headers.raw()); | ||
console.log(response.headers.get('content-type')); | ||
})(); | ||
@@ -344,3 +335,3 @@ ``` | ||
// Returns an array of values, instead of a string of comma-separated values | ||
console.log(res.headers.raw()['set-cookie']); | ||
console.log(response.headers.raw()['set-cookie']); | ||
})(); | ||
@@ -398,2 +389,16 @@ ``` | ||
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 | ||
@@ -464,3 +469,4 @@ | ||
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. | ||
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`. | ||
} | ||
@@ -556,2 +562,7 @@ ``` | ||
#### 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> | ||
@@ -648,3 +659,3 @@ | ||
// Example adapted from https://fetch.spec.whatwg.org/#example-headers-class | ||
const Headers = require('node-fetch'); | ||
const { Headers } = require('node-fetch'); | ||
@@ -651,0 +662,0 @@ const meta = { |
120
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, | ||
@@ -67,3 +77,3 @@ error: null | ||
body.on('error', err => { | ||
const error = isAbortError(err) ? | ||
const error = err instanceof FetchBaseError ? | ||
err : | ||
@@ -101,7 +111,6 @@ new FetchError(`Invalid response body while trying to fetch ${this.url}: ${err.message}`, 'system', err); | ||
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); | ||
} | ||
@@ -185,3 +194,3 @@ | ||
if (Buffer.isBuffer(body)) { | ||
return Body.Promise.resolve(body); | ||
return body; | ||
} | ||
@@ -191,3 +200,3 @@ | ||
if (!(body instanceof Stream)) { | ||
return Body.Promise.resolve(Buffer.alloc(0)); | ||
return Buffer.alloc(0); | ||
} | ||
@@ -199,46 +208,38 @@ | ||
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 err = new FetchError(`content size at ${data.url} over limit: ${data.size}`, 'max-size'); | ||
body.destroy(err); | ||
throw err; | ||
} | ||
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) { | ||
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); | ||
} | ||
} | ||
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}`); | ||
} | ||
} | ||
/** | ||
@@ -287,3 +288,3 @@ * Clone body given Res/Req instance | ||
*/ | ||
export const extractContentType = body => { | ||
export const extractContentType = (body, request) => { | ||
// Body is null or undefined | ||
@@ -319,2 +320,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 | ||
@@ -338,3 +343,5 @@ if (body instanceof Stream) { | ||
*/ | ||
export const getTotalBytes = ({body}) => { | ||
export const getTotalBytes = request => { | ||
const {body} = request; | ||
// Body is null or undefined | ||
@@ -360,2 +367,7 @@ if (body === null) { | ||
// Body is a spec-compliant form-data | ||
if (isFormData(body)) { | ||
return getFormDataLength(request[INTERNALS].boundary); | ||
} | ||
// Body is stream | ||
@@ -389,3 +401,1 @@ return null; | ||
// 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 err = new TypeError(`Header name must be a valid HTTP token [${name}]`); | ||
Object.defineProperty(err, 'code', {value: 'ERR_INVALID_HTTP_TOKEN'}); | ||
throw err; | ||
} | ||
}; | ||
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 err = new TypeError(`Invalid character in header content ["${name}"]`); | ||
Object.defineProperty(err, 'code', {value: 'ERR_INVALID_CHAR'}); | ||
throw err; | ||
} | ||
}; | ||
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, | ||
String(name).toLowerCase(), | ||
value | ||
String(value) | ||
); | ||
@@ -126,3 +130,3 @@ }; | ||
return name => { | ||
validateName(name); | ||
validateHeaderName(name); | ||
return URLSearchParams.prototype[p].call( | ||
@@ -149,3 +153,3 @@ receiver, | ||
get [Symbol.toStringTag]() { | ||
return 'Headers'; | ||
return this.constructor.name; | ||
} | ||
@@ -255,5 +259,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; | ||
} | ||
}) | ||
); | ||
} |
@@ -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>} | ||
*/ | ||
const 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; | ||
@@ -169,3 +158,3 @@ const {signal} = request; | ||
// 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')); | ||
@@ -204,2 +193,6 @@ finalize(); | ||
}); | ||
// see https://github.com/nodejs/node/pull/29376 | ||
if (process.version < 'v12.10') { | ||
response_.on('aborted', abortAndFinalize); | ||
} | ||
@@ -295,7 +288,2 @@ const responseOptions = { | ||
}); | ||
}; | ||
export default fetch; | ||
// Expose Promise | ||
fetch.Promise = global.Promise; | ||
} |
@@ -32,22 +32,2 @@ | ||
/** | ||
* 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 | ||
@@ -63,16 +43,7 @@ * | ||
// 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 +74,3 @@ } | ||
if (inputBody !== null && !headers.has('Content-Type')) { | ||
const contentType = extractContentType(inputBody); | ||
const contentType = extractContentType(inputBody, this); | ||
if (contentType) { | ||
@@ -135,2 +106,3 @@ headers.append('Content-Type', contentType); | ||
this.highWaterMark = init.highWaterMark || input.highWaterMark || 16384; | ||
this.insecureHTTPParser = init.insecureHTTPParser || input.insecureHTTPParser || false; | ||
} | ||
@@ -196,6 +168,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 | ||
@@ -209,3 +177,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); | ||
@@ -256,2 +225,3 @@ } | ||
headers: headers[Symbol.for('nodejs.util.inspect.custom')](), | ||
insecureHTTPParser: request.insecureHTTPParser, | ||
agent | ||
@@ -258,0 +228,0 @@ }; |
@@ -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,8 @@ * @param {*} obj | ||
*/ | ||
export const isAbortError = object => { | ||
return object[NAME] === 'AbortError'; | ||
export const isAbortSignal = object => { | ||
return ( | ||
typeof object === 'object' && | ||
object[NAME] === 'AbortSignal' | ||
); | ||
}; | ||
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
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
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
19
18
0
769
9
188893
2746
+ Addedfetch-blob@2.1.2(transitive)
- Removedfetch-blob@1.0.7(transitive)
Updateddata-uri-to-buffer@^3.0.1
Updatedfetch-blob@^2.0.0