From 8b4f4aa694d29693998b89d549c84473f22e4255 Mon Sep 17 00:00:00 2001 From: Nick K Date: Tue, 9 Apr 2019 00:01:32 +0300 Subject: [PATCH 001/157] Add minimal formdata-node support. --- package.json | 1 + src/body.js | 7 ++++++- test/test.js | 23 +++++++++++++++++++++++ 3 files changed, 30 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index 3d591b0e1..be09435e0 100644 --- a/package.json +++ b/package.json @@ -50,6 +50,7 @@ "codecov": "^3.0.0", "cross-env": "^5.1.3", "form-data": "^2.3.1", + "formdata-node": "^1.5.2", "is-builtin-module": "^1.0.0", "mocha": "^5.0.0", "nyc": "11.9.0", diff --git a/src/body.js b/src/body.js index 90cbcabfa..9beafdba1 100644 --- a/src/body.js +++ b/src/body.js @@ -5,7 +5,7 @@ * Body interface provides common methods for Request and Response */ -import Stream from 'stream'; +import Stream, { Readable } from 'stream'; import Blob, { BUFFER } from './blob.js'; import FetchError from './fetch-error.js'; @@ -50,6 +50,9 @@ export default function Body(body, { body = Buffer.from(body.buffer, body.byteOffset, body.byteLength); } else if (body instanceof Stream) { // body is stream + } else if (body.stream instanceof Readable && typeof body.boundary === 'string') { + // body is an instance of formdata-node + body = body.stream } else { // none of the above // coerce to string then buffer @@ -422,6 +425,8 @@ export function extractContentType(body) { } else if (typeof body.getBoundary === 'function') { // detect form data input from form-data module return `multipart/form-data;boundary=${body.getBoundary()}`; + } else if (body.stream instanceof Readable && typeof body.boundary === "string") { + return `multipart/form-data;boundary=${body.boundary}`; } else if (body instanceof Stream) { // body is stream // can't really do much about this diff --git a/test/test.js b/test/test.js index 78301a4b7..2b0a04178 100644 --- a/test/test.js +++ b/test/test.js @@ -7,6 +7,7 @@ import chaiString from 'chai-string'; import then from 'promise'; import resumer from 'resumer'; import FormData from 'form-data'; +import FormDataNode from "formdata-node"; import stringToArrayBuffer from 'string-to-arraybuffer'; import URLSearchParams_Polyfill from 'url-search-params'; import { URL } from 'whatwg-url'; @@ -1347,6 +1348,28 @@ describe('node-fetch', () => { }); }); + it('should support formdata-node as POST body', function() { + const form = new FormDataNode(); + + form.set('field', "some text"); + form.set('file', fs.createReadStream(path.join(__dirname, 'dummy.txt'))); + + const url = `${base}multipart`; + const opts = { + method: 'POST', + body: form + }; + + return fetch(url, opts) + .then(res => res.json()) + .then(res => { + expect(res.method).to.equal('POST'); + expect(res.headers['content-type']).to.equal(`multipart/form-data;boundary=${form.boundary}`); + expect(res.body).to.contain('field='); + expect(res.body).to.contain('file='); + }); + }); + it('should allow POST request with object body', function() { const url = `${base}inspect`; // note that fetch simply calls tostring on an object From 45fde57d41ca576f679638dbdc94243394d049f1 Mon Sep 17 00:00:00 2001 From: Nick K Date: Tue, 9 Apr 2019 00:20:47 +0300 Subject: [PATCH 002/157] Fix for string literal in formdata-node checks. --- src/body.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/body.js b/src/body.js index 9beafdba1..78fc1c3e7 100644 --- a/src/body.js +++ b/src/body.js @@ -425,7 +425,7 @@ export function extractContentType(body) { } else if (typeof body.getBoundary === 'function') { // detect form data input from form-data module return `multipart/form-data;boundary=${body.getBoundary()}`; - } else if (body.stream instanceof Readable && typeof body.boundary === "string") { + } else if (body.stream instanceof Readable && typeof body.boundary === 'string') { return `multipart/form-data;boundary=${body.boundary}`; } else if (body instanceof Stream) { // body is stream From 92311a72467d28ea92ba06c15631979c99bf3cb1 Mon Sep 17 00:00:00 2001 From: Nick K Date: Tue, 9 Apr 2019 11:39:18 +0300 Subject: [PATCH 003/157] Add an example of formdata-node usage. --- README.md | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/README.md b/README.md index e900c9719..8f997040c 100644 --- a/README.md +++ b/README.md @@ -249,6 +249,25 @@ fetch('https://httpbin.org/post', options) .then(json => console.log(json)); ``` +node-fetch support [formdata-node](https://github.com/octet-stream/form-data) as an alternative: + +```js +import FormData from 'formdata-node'; + +const form = new FormData(); + +form.set('greeting', 'Hello, world!'); + +const options = { + method: 'POST', + body: form +}; + +fetch('https://httpbin.org/post', options) + .then(res => res.json()) + .then(json => console.log(json)); +``` + #### Request cancellation with AbortSignal > NOTE: You may only cancel streamed requests on Node >= v8.0.0 From b4d11ffe07c80aa7c44594c5bb2ab89fe04b621b Mon Sep 17 00:00:00 2001 From: Nick K Date: Tue, 9 Apr 2019 13:41:11 +0300 Subject: [PATCH 004/157] Fix imprort of the stream.Readable. --- src/body.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/body.js b/src/body.js index 78fc1c3e7..3412a975b 100644 --- a/src/body.js +++ b/src/body.js @@ -5,7 +5,7 @@ * Body interface provides common methods for Request and Response */ -import Stream, { Readable } from 'stream'; +import Stream from 'stream'; import Blob, { BUFFER } from './blob.js'; import FetchError from './fetch-error.js'; @@ -50,7 +50,7 @@ export default function Body(body, { body = Buffer.from(body.buffer, body.byteOffset, body.byteLength); } else if (body instanceof Stream) { // body is stream - } else if (body.stream instanceof Readable && typeof body.boundary === 'string') { + } else if (body.stream instanceof Stream.Readable && typeof body.boundary === 'string') { // body is an instance of formdata-node body = body.stream } else { @@ -425,7 +425,7 @@ export function extractContentType(body) { } else if (typeof body.getBoundary === 'function') { // detect form data input from form-data module return `multipart/form-data;boundary=${body.getBoundary()}`; - } else if (body.stream instanceof Readable && typeof body.boundary === 'string') { + } else if (body.stream instanceof Stream.Readable && typeof body.boundary === 'string') { return `multipart/form-data;boundary=${body.boundary}`; } else if (body instanceof Stream) { // body is stream From f947a9f5afcaf941c7f04817a0f7227b906a2f4a Mon Sep 17 00:00:00 2001 From: Richie Bendall Date: Sun, 8 Sep 2019 03:53:17 +1200 Subject: [PATCH 005/157] feat: Migrate TypeScript types (#669) --- package.json | 4 +- types/externals.d.ts | 21 +++++ types/index.d.ts | 210 +++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 234 insertions(+), 1 deletion(-) create mode 100644 types/externals.d.ts create mode 100644 types/index.d.ts diff --git a/package.json b/package.json index 8e5c883b2..2cad048a1 100644 --- a/package.json +++ b/package.json @@ -5,11 +5,13 @@ "main": "lib/index", "browser": "./browser.js", "module": "lib/index.mjs", + "types": "./types/index.d.ts", "files": [ "lib/index.js", "lib/index.mjs", "lib/index.es.js", - "browser.js" + "browser.js", + "types/*.d.ts" ], "engines": { "node": "4.x || >=6.0.0" diff --git a/types/externals.d.ts b/types/externals.d.ts new file mode 100644 index 000000000..fb9466bf3 --- /dev/null +++ b/types/externals.d.ts @@ -0,0 +1,21 @@ +// `AbortSignal` is defined here to prevent a dependency on a particular +// implementation like the `abort-controller` package, and to avoid requiring +// the `dom` library in `tsconfig.json`. + +export interface AbortSignal { + aborted: boolean; + + addEventListener: (type: "abort", listener: ((this: AbortSignal, event: any) => any), options?: boolean | { + capture?: boolean, + once?: boolean, + passive?: boolean + }) => void; + + removeEventListener: (type: "abort", listener: ((this: AbortSignal, event: any) => any), options?: boolean | { + capture?: boolean + }) => void; + + dispatchEvent: (event: any) => boolean; + + onabort?: null | ((this: AbortSignal, event: any) => void); +} \ No newline at end of file diff --git a/types/index.d.ts b/types/index.d.ts new file mode 100644 index 000000000..b3d739aa6 --- /dev/null +++ b/types/index.d.ts @@ -0,0 +1,210 @@ +// Type definitions for node-fetch 2.5 +// Project: https://github.com/bitinn/node-fetch +// Definitions by: Torsten Werner +// Niklas Lindgren +// Vinay Bedre +// Antonio Román +// Andrew Leedham +// Jason Li +// Brandon Wilson +// Steve Faulkner +// Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped + +/// + +import { Agent } from "http"; +import { URLSearchParams, URL } from "url"; +import { AbortSignal } from "./externals"; + +export class Request extends Body { + constructor(input: string | { href: string } | Request, init?: RequestInit); + clone(): Request; + context: RequestContext; + headers: Headers; + method: string; + redirect: RequestRedirect; + referrer: string; + url: string; + + // node-fetch extensions to the whatwg/fetch spec + agent?: Agent | ((parsedUrl: URL) => Agent); + compress: boolean; + counter: number; + follow: number; + hostname: string; + port?: number; + protocol: string; + size: number; + timeout: number; +} + +export interface RequestInit { + // whatwg/fetch standard options + body?: BodyInit; + headers?: HeadersInit; + method?: string; + redirect?: RequestRedirect; + signal?: AbortSignal | null; + + // node-fetch extensions + agent?: Agent | ((parsedUrl: URL) => Agent); // =null http.Agent instance, allows custom proxy, certificate etc. + compress?: boolean; // =true support gzip/deflate content encoding. false to disable + follow?: number; // =20 maximum redirect count. 0 to not follow redirect + size?: number; // =0 maximum response body size in bytes. 0 to disable + timeout?: number; // =0 req/res timeout in ms, it resets on redirect. 0 to disable (OS limit applies) + + // node-fetch does not support mode, cache or credentials options +} + +export type RequestContext = + "audio" + | "beacon" + | "cspreport" + | "download" + | "embed" + | "eventsource" + | "favicon" + | "fetch" + | "font" + | "form" + | "frame" + | "hyperlink" + | "iframe" + | "image" + | "imageset" + | "import" + | "internal" + | "location" + | "manifest" + | "object" + | "ping" + | "plugin" + | "prefetch" + | "script" + | "serviceworker" + | "sharedworker" + | "style" + | "subresource" + | "track" + | "video" + | "worker" + | "xmlhttprequest" + | "xslt"; +export type RequestMode = "cors" | "no-cors" | "same-origin"; +export type RequestRedirect = "error" | "follow" | "manual"; +export type RequestCredentials = "omit" | "include" | "same-origin"; + +export type RequestCache = + "default" + | "force-cache" + | "no-cache" + | "no-store" + | "only-if-cached" + | "reload"; + +export class Headers implements Iterable<[string, string]> { + constructor(init?: HeadersInit); + forEach(callback: (value: string, name: string) => void): void; + append(name: string, value: string): void; + delete(name: string): void; + get(name: string): string | null; + getAll(name: string): string[]; + has(name: string): boolean; + raw(): { [k: string]: string[] }; + set(name: string, value: string): void; + + // Iterator methods + entries(): Iterator<[string, string]>; + keys(): Iterator; + values(): Iterator<[string]>; + [Symbol.iterator](): Iterator<[string, string]>; +} + +type BlobPart = ArrayBuffer | ArrayBufferView | Blob | string; + +interface BlobOptions { + type?: string; + endings?: "transparent" | "native"; +} + +export class Blob { + constructor(blobParts?: BlobPart[], options?: BlobOptions); + readonly type: string; + readonly size: number; + slice(start?: number, end?: number): Blob; +} + +export class Body { + constructor(body?: any, opts?: { size?: number; timeout?: number }); + arrayBuffer(): Promise; + blob(): Promise; + body: NodeJS.ReadableStream; + bodyUsed: boolean; + buffer(): Promise; + json(): Promise; + size: number; + text(): Promise; + textConverted(): Promise; + timeout: number; +} + +export class FetchError extends Error { + name: "FetchError"; + constructor(message: string, type: string, systemError?: string); + type: string; + code?: string; + errno?: string; +} + +export class Response extends Body { + constructor(body?: BodyInit, init?: ResponseInit); + static error(): Response; + static redirect(url: string, status: number): Response; + clone(): Response; + headers: Headers; + ok: boolean; + redirected: boolean; + status: number; + statusText: string; + type: ResponseType; + url: string; +} + +export type ResponseType = + "basic" + | "cors" + | "default" + | "error" + | "opaque" + | "opaqueredirect"; + +export interface ResponseInit { + headers?: HeadersInit; + size?: number; + status?: number; + statusText?: string; + timeout?: number; + url?: string; +} + +export type HeadersInit = Headers | string[][] | { [key: string]: string }; +// HeaderInit is exported to support backwards compatibility. See PR #34382 +export type HeaderInit = HeadersInit; +export type BodyInit = + ArrayBuffer + | ArrayBufferView + | NodeJS.ReadableStream + | string + | URLSearchParams; +export type RequestInfo = string | Request; + +declare function fetch( + url: RequestInfo, + init?: RequestInit +): Promise; + +declare namespace fetch { + function isRedirect(code: number): boolean; +} + +export default fetch; \ No newline at end of file From 43b2aa4cfab3bad79e87d858d97f0eb94c9f38af Mon Sep 17 00:00:00 2001 From: Richie Bendall Date: Sun, 8 Sep 2019 06:06:55 +1200 Subject: [PATCH 006/157] style: Introduce linting via XO --- browser.js | 27 +- build/babel-plugin.js | 104 +++--- build/rollup-plugin.js | 29 +- package.json | 19 +- rollup.config.js | 39 ++- src/abort-error.js | 4 +- src/blob.js | 24 +- src/body.js | 204 ++++++----- src/fetch-error.js | 6 +- src/headers.js | 22 +- src/index.js | 69 ++-- src/request.js | 57 +-- src/response.js | 10 +- test/server.js | 51 +-- test/test.js | 768 ++++++++++++++++++++++------------------- 15 files changed, 787 insertions(+), 646 deletions(-) diff --git a/browser.js b/browser.js index 0ad5de004..47f686731 100644 --- a/browser.js +++ b/browser.js @@ -1,15 +1,24 @@ -"use strict"; +'use strict'; -// ref: https://github.com/tc39/proposal-global -var getGlobal = function () { - // the only reliable means to get the global object is +// Ref: https://github.com/tc39/proposal-global +const getGlobal = function () { + // The only reliable means to get the global object is // `Function('return this')()` // However, this causes CSP violations in Chrome apps. - if (typeof self !== 'undefined') { return self; } - if (typeof window !== 'undefined') { return window; } - if (typeof global !== 'undefined') { return global; } + if (typeof self !== 'undefined') { + return self; + } + + if (typeof window !== 'undefined') { + return window; + } + + if (typeof global !== 'undefined') { + return global; + } + throw new Error('unable to locate global object'); -} +}; var global = getGlobal(); @@ -20,4 +29,4 @@ exports.default = global.fetch.bind(global); exports.Headers = global.Headers; exports.Request = global.Request; -exports.Response = global.Response; \ No newline at end of file +exports.Response = global.Response; diff --git a/build/babel-plugin.js b/build/babel-plugin.js index 8cddae954..f13727b6b 100644 --- a/build/babel-plugin.js +++ b/build/babel-plugin.js @@ -3,59 +3,59 @@ const walked = Symbol('walked'); module.exports = ({ types: t }) => ({ - visitor: { - Program: { - exit(program) { - if (program[walked]) { - return; - } + visitor: { + Program: { + exit(program) { + if (program[walked]) { + return; + } - for (let path of program.get('body')) { - if (path.isExpressionStatement()) { - const expr = path.get('expression'); - if (expr.isAssignmentExpression() && + for (const path of program.get('body')) { + if (path.isExpressionStatement()) { + const expr = path.get('expression'); + if (expr.isAssignmentExpression() && expr.get('left').matchesPattern('exports.*')) { - const prop = expr.get('left').get('property'); - if (prop.isIdentifier({ name: 'default' })) { - program.unshiftContainer('body', [ - t.expressionStatement( - t.assignmentExpression('=', - t.identifier('exports'), - t.assignmentExpression('=', - t.memberExpression( - t.identifier('module'), t.identifier('exports') - ), - expr.node.right - ) - ) - ), - t.expressionStatement( - t.callExpression( - t.memberExpression( - t.identifier('Object'), t.identifier('defineProperty')), - [ - t.identifier('exports'), - t.stringLiteral('__esModule'), - t.objectExpression([ - t.objectProperty(t.identifier('value'), t.booleanLiteral(true)) - ]) - ] - ) - ), - t.expressionStatement( - t.assignmentExpression('=', - expr.node.left, t.identifier('exports') - ) - ) - ]); - path.remove(); - } - } - } - } + const prop = expr.get('left').get('property'); + if (prop.isIdentifier({ name: 'default' })) { + program.unshiftContainer('body', [ + t.expressionStatement( + t.assignmentExpression('=', + t.identifier('exports'), + t.assignmentExpression('=', + t.memberExpression( + t.identifier('module'), t.identifier('exports') + ), + expr.node.right + ) + ) + ), + t.expressionStatement( + t.callExpression( + t.memberExpression( + t.identifier('Object'), t.identifier('defineProperty')), + [ + t.identifier('exports'), + t.stringLiteral('__esModule'), + t.objectExpression([ + t.objectProperty(t.identifier('value'), t.booleanLiteral(true)) + ]) + ] + ) + ), + t.expressionStatement( + t.assignmentExpression('=', + expr.node.left, t.identifier('exports') + ) + ) + ]); + path.remove(); + } + } + } + } - program[walked] = true; - } - } - } + program[walked] = true; + } + } + } }); diff --git a/build/rollup-plugin.js b/build/rollup-plugin.js index 36ebdc804..b9cff7aa5 100644 --- a/build/rollup-plugin.js +++ b/build/rollup-plugin.js @@ -1,18 +1,19 @@ export default function tweakDefault() { - return { - transformBundle: function (source) { - var lines = source.split('\n'); - for (var i = 0; i < lines.length; i++) { - var line = lines[i]; - var matches = /^(exports(?:\['default']|\.default)) = (.*);$/.exec(line); - if (matches) { - lines[i] = 'module.exports = exports = ' + matches[2] + ';\n' + + return { + transformBundle(source) { + const lines = source.split('\n'); + for (let i = 0; i < lines.length; i++) { + const line = lines[i]; + const matches = /^(exports(?:\['default']|\.default)) = (.*);$/.exec(line); + if (matches) { + lines[i] = 'module.exports = exports = ' + matches[2] + ';\n' + 'Object.defineProperty(exports, "__esModule", { value: true });\n' + matches[1] + ' = exports;'; - break; - } - } - return lines.join('\n'); - } - }; + break; + } + } + + return lines.join('\n'); + } + }; } diff --git a/package.json b/package.json index 2cad048a1..874f23dfa 100644 --- a/package.json +++ b/package.json @@ -21,7 +21,8 @@ "prepare": "npm run build", "test": "cross-env BABEL_ENV=test mocha --require babel-register --throw-deprecation test/test.js", "report": "cross-env BABEL_ENV=coverage nyc --reporter lcov --reporter text mocha -R spec test/test.js", - "coverage": "cross-env BABEL_ENV=coverage nyc --reporter json --reporter text mocha -R spec test/test.js && codecov -f coverage/coverage-final.json" + "coverage": "cross-env BABEL_ENV=coverage nyc --reporter json --reporter text mocha -R spec test/test.js && codecov -f coverage/coverage-final.json", + "lint": "xo src" }, "repository": { "type": "git", @@ -62,7 +63,19 @@ "rollup": "^0.63.4", "rollup-plugin-babel": "^3.0.7", "string-to-arraybuffer": "^1.0.2", - "whatwg-url": "^5.0.0" + "whatwg-url": "^5.0.0", + "xo": "^0.24.0" }, - "dependencies": {} + "dependencies": {}, + "xo": { + "rules": { + "object-curly-spacing": [ + "error", + "always" + ], + "valid-jsdoc": 0, + "no-multi-assign": 0, + "complexity": 0 + } + } } diff --git a/rollup.config.js b/rollup.config.js index a201ee455..9beb2014a 100644 --- a/rollup.config.js +++ b/rollup.config.js @@ -5,23 +5,24 @@ import tweakDefault from './build/rollup-plugin'; process.env.BABEL_ENV = 'rollup'; export default { - input: 'src/index.js', - output: [ - { file: 'lib/index.js', format: 'cjs', exports: 'named' }, - { file: 'lib/index.es.js', format: 'es', exports: 'named', intro: 'process.emitWarning("The .es.js file is deprecated. Use .mjs instead.");' }, - { file: 'lib/index.mjs', format: 'es', exports: 'named' }, - ], - plugins: [ - babel({ - runtimeHelpers: true - }), - tweakDefault() - ], - external: function (id) { - if (isBuiltin(id)) { - return true; - } - id = id.split('/').slice(0, id[0] === '@' ? 2 : 1).join('/'); - return !!require('./package.json').dependencies[id]; - } + input: 'src/index.js', + output: [ + { file: 'lib/index.js', format: 'cjs', exports: 'named' }, + { file: 'lib/index.es.js', format: 'es', exports: 'named', intro: 'process.emitWarning("The .es.js file is deprecated. Use .mjs instead.");' }, + { file: 'lib/index.mjs', format: 'es', exports: 'named' } + ], + plugins: [ + babel({ + runtimeHelpers: true + }), + tweakDefault() + ], + external(id) { + if (isBuiltin(id)) { + return true; + } + + id = id.split('/').slice(0, id[0] === '@' ? 2 : 1).join('/'); + return Boolean(require('./package.json').dependencies[id]); + } }; diff --git a/src/abort-error.js b/src/abort-error.js index cbb13caba..8e39df204 100644 --- a/src/abort-error.js +++ b/src/abort-error.js @@ -1,5 +1,5 @@ /** - * abort-error.js + * Abort-error.js * * AbortError interface for cancelled requests */ @@ -16,7 +16,7 @@ export default function AbortError(message) { this.type = 'aborted'; this.message = message; - // hide custom error implementation details from end-users + // Hide custom error implementation details from end-users Error.captureStackTrace(this, this.constructor); } diff --git a/src/blob.js b/src/blob.js index e1151a955..643bc18e5 100644 --- a/src/blob.js +++ b/src/blob.js @@ -3,8 +3,8 @@ import Stream from 'stream'; -// fix for "Readable" isn't a named export issue -const Readable = Stream.Readable; +// Fix for "Readable" isn't a named export issue +const { Readable } = Stream; export const BUFFER = Symbol('buffer'); const TYPE = Symbol('type'); @@ -36,6 +36,7 @@ export default class Blob { } else { buffer = Buffer.from(typeof element === 'string' ? element : String(element)); } + size += buffer.length; buffers.push(buffer); } @@ -43,25 +44,30 @@ export default class Blob { this[BUFFER] = Buffer.concat(buffers); - let type = options && options.type !== undefined && String(options.type).toLowerCase(); + const type = options && options.type !== undefined && String(options.type).toLowerCase(); if (type && !/[^\u0020-\u007E]/.test(type)) { this[TYPE] = type; } } + get size() { return this[BUFFER].length; } + get type() { return this[TYPE]; } + text() { - return Promise.resolve(this[BUFFER].toString()) + return Promise.resolve(this[BUFFER].toString()); } + arrayBuffer() { const buf = this[BUFFER]; const ab = buf.buffer.slice(buf.byteOffset, buf.byteOffset + buf.byteLength); return Promise.resolve(ab); } + stream() { const readable = new Readable(); readable._read = () => {}; @@ -69,15 +75,17 @@ export default class Blob { readable.push(null); return readable; } + toString() { - return '[object Blob]' + return '[object Blob]'; } + slice() { - const size = this.size; + const { size } = this; const start = arguments[0]; const end = arguments[1]; - let relativeStart, relativeEnd; + let relativeStart; let relativeEnd; if (start === undefined) { relativeStart = 0; } else if (start < 0) { @@ -85,6 +93,7 @@ export default class Blob { } else { relativeStart = Math.min(start, size); } + if (end === undefined) { relativeEnd = size; } else if (end < 0) { @@ -92,6 +101,7 @@ export default class Blob { } else { relativeEnd = Math.min(end, size); } + const span = Math.max(relativeEnd - relativeStart, 0); const buffer = this[BUFFER]; diff --git a/src/body.js b/src/body.js index 1b6eab1f8..17e014452 100644 --- a/src/body.js +++ b/src/body.js @@ -1,22 +1,24 @@ /** - * body.js + * Body.js * * Body interface provides common methods for Request and Response */ import Stream from 'stream'; -import Blob, { BUFFER } from './blob.js'; -import FetchError from './fetch-error.js'; +import Blob, { BUFFER } from './blob'; +import FetchError from './fetch-error'; let convert; -try { convert = require('encoding').convert; } catch(e) {} +try { + convert = require('encoding').convert; +} catch (error) {} const INTERNALS = Symbol('Body internals'); -// fix an issue where "PassThrough" isn't a named export for node <10 -const PassThrough = Stream.PassThrough; +// Fix an issue where "PassThrough" isn't a named export for node <10 +const { PassThrough } = Stream; /** * Body mixin @@ -32,28 +34,29 @@ export default function Body(body, { timeout = 0 } = {}) { if (body == null) { - // body is undefined or null + // Body is undefined or null body = null; } else if (isURLSearchParams(body)) { - // body is a URLSearchParams + // Body is a URLSearchParams body = Buffer.from(body.toString()); } else if (isBlob(body)) { - // body is blob + // Body is blob } else if (Buffer.isBuffer(body)) { - // body is Buffer + // Body is Buffer } else if (Object.prototype.toString.call(body) === '[object ArrayBuffer]') { - // body is ArrayBuffer + // Body is ArrayBuffer body = Buffer.from(body); } else if (ArrayBuffer.isView(body)) { - // body is ArrayBufferView + // Body is ArrayBufferView body = Buffer.from(body.buffer, body.byteOffset, body.byteLength); } else if (body instanceof Stream) { - // body is stream + // Body is stream } else { - // none of the above + // None of the above // coerce to string then buffer body = Buffer.from(String(body)); } + this[INTERNALS] = { body, disturbed: false, @@ -64,9 +67,9 @@ export default function Body(body, { if (body instanceof Stream) { body.on('error', err => { - const error = err.name === 'AbortError' - ? err - : new FetchError(`Invalid response body while trying to fetch ${this.url}: ${err.message}`, 'system', err); + const error = err.name === 'AbortError' ? + err : + new FetchError(`Invalid response body while trying to fetch ${this.url}: ${err.message}`, 'system', err); this[INTERNALS].error = error; }); } @@ -96,7 +99,7 @@ Body.prototype = { * @return Promise */ blob() { - let ct = this.headers && this.headers.get('content-type') || ''; + const ct = this.headers && this.headers.get('content-type') || ''; return consumeBody.call(this).then(buf => Object.assign( // Prevent copying new Blob([], { @@ -114,13 +117,13 @@ Body.prototype = { * @return Promise */ json() { - return consumeBody.call(this).then((buffer) => { + return consumeBody.call(this).then(buffer => { try { return JSON.parse(buffer.toString()); - } catch (err) { - return Body.Promise.reject(new FetchError(`invalid json response body at ${this.url} reason: ${err.message}`, 'invalid-json')); + } catch (error) { + return Body.Promise.reject(new FetchError(`invalid json response body at ${this.url} reason: ${error.message}`, 'invalid-json')); } - }) + }); }, /** @@ -190,19 +193,19 @@ function consumeBody() { return Body.Promise.reject(this[INTERNALS].error); } - let body = this.body; + let { body } = this; - // body is null + // Body is null if (body === null) { return Body.Promise.resolve(Buffer.alloc(0)); } - // body is blob + // Body is blob if (isBlob(body)) { body = body.stream(); } - // body is buffer + // Body is buffer if (Buffer.isBuffer(body)) { return Body.Promise.resolve(body); } @@ -212,16 +215,16 @@ function consumeBody() { return Body.Promise.resolve(Buffer.alloc(0)); } - // body is stream + // Body is stream // get ready to actually consume the body - let accum = []; + const accum = []; let accumBytes = 0; let abort = false; return new Body.Promise((resolve, reject) => { let resTimeout; - // allow timeout on slow response body + // Allow timeout on slow response body if (this.timeout) { resTimeout = setTimeout(() => { abort = true; @@ -229,14 +232,14 @@ function consumeBody() { }, this.timeout); } - // handle stream errors + // Handle stream errors body.on('error', err => { if (err.name === 'AbortError') { - // if the request was aborted, reject with this Error + // If the request was aborted, reject with this Error abort = true; reject(err); } else { - // other errors, such as incorrect content-encoding + // Other errors, such as incorrect content-encoding reject(new FetchError(`Invalid response body while trying to fetch ${this.url}: ${err.message}`, 'system', err)); } }); @@ -265,9 +268,9 @@ function consumeBody() { try { resolve(Buffer.concat(accum, accumBytes)); - } catch (err) { - // handle streams that have accumulated too much data (issue #414) - reject(new FetchError(`Could not create Buffer from response body for ${this.url}: ${err.message}`, 'system', err)); + } catch (error) { + // Handle streams that have accumulated too much data (issue #414) + reject(new FetchError(`Could not create Buffer from response body for ${this.url}: ${error.message}`, 'system', error)); } }); }); @@ -283,27 +286,27 @@ function consumeBody() { */ function convertBody(buffer, headers) { if (typeof convert !== 'function') { - throw new Error('The package `encoding` must be installed to use the textConverted() function'); + throw new TypeError('The package `encoding` must be installed to use the textConverted() function'); } const ct = headers.get('content-type'); let charset = 'utf-8'; - let res, str; + let res; let str; - // header + // Header if (ct) { res = /charset=([^;]*)/i.exec(ct); } - // no charset in content type, peek at response body for at most 1024 bytes + // No charset in content type, peek at response body for at most 1024 bytes str = buffer.slice(0, 1024).toString(); - // html5 + // Html5 if (!res && str) { res = /> + // Sequence> // Note: per spec we have to first exhaust the lists then process them const pairs = []; for (const pair of init) { if (typeof pair !== 'object' || typeof pair[Symbol.iterator] !== 'function') { throw new TypeError('Each header pair must be iterable'); } - pairs.push(Array.from(pair)); + + pairs.push([...pair]); } for (const pair of pairs) { if (pair.length !== 2) { throw new TypeError('Each header pair must be a name/value tuple'); } + this.append(pair[0], pair[1]); } } else { - // record + // Record for (const key of Object.keys(init)) { const value = init[key]; this.append(key, value); @@ -199,7 +202,7 @@ export default class Headers { if (key !== undefined) { delete this[MAP][key]; } - }; + } /** * Return raw headers (non-spec api) @@ -332,7 +335,7 @@ Object.defineProperty(HeadersIteratorPrototype, Symbol.toStringTag, { export function exportNodeCompatibleHeaders(headers) { const obj = Object.assign({ __proto__: null }, headers[MAP]); - // http.request() only supports string as Host header. This hack makes + // Http.request() only supports string as Host header. This hack makes // specifying custom Host header possible. const hostHeaderKey = find(headers[MAP], 'Host'); if (hostHeaderKey !== undefined) { @@ -355,11 +358,13 @@ export function createHeadersLenient(obj) { if (invalidTokenRegex.test(name)) { continue; } + if (Array.isArray(obj[name])) { for (const val of obj[name]) { if (invalidHeaderCharRegex.test(val)) { continue; } + if (headers[MAP][name] === undefined) { headers[MAP][name] = [val]; } else { @@ -370,5 +375,6 @@ export function createHeadersLenient(obj) { headers[MAP][name] = [obj[name]]; } } + return headers; } diff --git a/src/index.js b/src/index.js index 907f47275..c17e48f4b 100644 --- a/src/index.js +++ b/src/index.js @@ -1,6 +1,6 @@ /** - * index.js + * Index.js * * a request API compatible with window.fetch * @@ -20,8 +20,8 @@ import Request, { getNodeRequestOptions } from './request'; import FetchError from './fetch-error'; import AbortError from './abort-error'; -// fix an issue where "PassThrough", "resolve" aren't a named export for node <10 -const PassThrough = Stream.PassThrough; +// Fix an issue where "PassThrough", "resolve" aren't a named export for node <10 +const { PassThrough } = Stream; const resolve_url = Url.resolve; /** @@ -32,17 +32,16 @@ const resolve_url = Url.resolve; * @return Promise */ export default function fetch(url, opts) { - - // allow custom promise + // Allow custom promise if (!fetch.Promise) { throw new Error('native promise missing, set fetch.Promise to your favorite alternative'); } Body.Promise = fetch.Promise; - // wrap http.request into fetch + // Wrap http.request into fetch return new fetch.Promise((resolve, reject) => { - // build request object + // Build request object const request = new Request(url, opts); const options = getNodeRequestOptions(request); @@ -50,15 +49,19 @@ export default function fetch(url, opts) { const { signal } = request; let response = null; - const abort = () => { - let error = new AbortError('The user aborted a request.'); + const abort = () => { + const error = new AbortError('The user aborted a request.'); reject(error); if (request.body && request.body instanceof Stream.Readable) { request.body.destroy(error); } - if (!response || !response.body) return; + + if (!response || !response.body) { + return; + } + response.body.emit('error', error); - } + }; if (signal && signal.aborted) { abort(); @@ -68,9 +71,9 @@ export default function fetch(url, opts) { const abortAndFinalize = () => { abort(); finalize(); - } + }; - // send request + // Send request const req = send(options); let reqTimeout; @@ -80,7 +83,10 @@ export default function fetch(url, opts) { function finalize() { req.abort(); - if (signal) signal.removeEventListener('abort', abortAndFinalize); + if (signal) { + signal.removeEventListener('abort', abortAndFinalize); + } + clearTimeout(reqTimeout); } @@ -118,16 +124,17 @@ export default function fetch(url, opts) { finalize(); return; case 'manual': - // node-fetch-specific step: make manual redirect a bit easier to use by setting the Location header value to the resolved URL. + // Node-fetch-specific step: make manual redirect a bit easier to use by setting the Location header value to the resolved URL. if (locationURL !== null) { - // handle corrupted header + // Handle corrupted header try { headers.set('Location', locationURL); - } catch (err) { + } catch (error) { // istanbul ignore next: nodejs server prevent invalid response headers, we can't test this through normal request - reject(err); + reject(error); } } + break; case 'follow': // HTTP-redirect fetch step 2 @@ -177,9 +184,11 @@ export default function fetch(url, opts) { } } - // prepare response + // Prepare response res.once('end', () => { - if (signal) signal.removeEventListener('abort', abortAndFinalize); + if (signal) { + signal.removeEventListener('abort', abortAndFinalize); + } }); let body = res.pipe(new PassThrough()); @@ -187,7 +196,7 @@ export default function fetch(url, opts) { url: request.url, status: res.statusCode, statusText: res.statusMessage, - headers: headers, + headers, size: request.size, timeout: request.timeout, counter: request.counter @@ -220,7 +229,7 @@ export default function fetch(url, opts) { finishFlush: zlib.Z_SYNC_FLUSH }; - // for gzip + // For gzip if (codings == 'gzip' || codings == 'x-gzip') { body = body.pipe(zlib.createGunzip(zlibOptions)); response = new Response(body, response_options); @@ -228,25 +237,26 @@ export default function fetch(url, opts) { return; } - // for deflate + // For deflate if (codings == 'deflate' || codings == 'x-deflate') { - // handle the infamous raw deflate response from old servers + // Handle the infamous raw deflate response from old servers // a hack for old IIS and Apache servers const raw = res.pipe(new PassThrough()); raw.once('data', chunk => { - // see http://stackoverflow.com/questions/37519828 + // See http://stackoverflow.com/questions/37519828 if ((chunk[0] & 0x0F) === 0x08) { body = body.pipe(zlib.createInflate()); } else { body = body.pipe(zlib.createInflateRaw()); } + response = new Response(body, response_options); resolve(response); }); return; } - // for br + // For br if (codings == 'br' && typeof zlib.createBrotliDecompress === 'function') { body = body.pipe(zlib.createBrotliDecompress()); response = new Response(body, response_options); @@ -254,15 +264,14 @@ export default function fetch(url, opts) { return; } - // otherwise, use response as-is + // Otherwise, use response as-is response = new Response(body, response_options); resolve(response); }); writeToStream(req, request); }); - -}; +} /** * Redirect code matching @@ -272,7 +281,7 @@ export default function fetch(url, opts) { */ fetch.isRedirect = code => code === 301 || code === 302 || code === 303 || code === 307 || code === 308; -// expose Promise +// Expose Promise fetch.Promise = global.Promise; export { Headers, diff --git a/src/request.js b/src/request.js index 45a7eb7e4..2af7a3c0d 100644 --- a/src/request.js +++ b/src/request.js @@ -1,6 +1,6 @@ /** - * request.js + * Request.js * * Request class contains server only options * @@ -9,12 +9,12 @@ import Url from 'url'; import Stream from 'stream'; -import Headers, { exportNodeCompatibleHeaders } from './headers.js'; +import Headers, { exportNodeCompatibleHeaders } from './headers'; import Body, { clone, extractContentType, getTotalBytes } from './body'; const INTERNALS = Symbol('Request internals'); -// fix an issue where "format", "parse" aren't a named export for node <10 +// Fix an issue where "format", "parse" aren't a named export for node <10 const parse_url = Url.parse; const format_url = Url.format; @@ -35,11 +35,11 @@ function isRequest(input) { function isAbortSignal(signal) { const proto = ( - signal - && typeof signal === 'object' - && Object.getPrototypeOf(signal) + signal && + typeof signal === 'object' && + Object.getPrototypeOf(signal) ); - return !!(proto && proto.constructor.name === 'AbortSignal'); + return Boolean(proto && proto.constructor.name === 'AbortSignal'); } /** @@ -53,17 +53,18 @@ export default class Request { constructor(input, init = {}) { let parsedURL; - // normalize input + // Normalize input if (!isRequest(input)) { if (input && input.href) { - // in order to support Node.js' Url objects; though WHATWG's URL objects + // In order to support Node.js' Url objects; though WHATWG's URL objects // will fall into this branch also (since their `toString()` will return // `href` property anyway) parsedURL = parse_url(input.href); } else { - // coerce input to a string before attempting to parse + // Coerce input to a string before attempting to parse parsedURL = parse_url(`${input}`); } + input = {}; } else { parsedURL = parse_url(input.url); @@ -77,7 +78,7 @@ export default class Request { throw new TypeError('Request with GET/HEAD method cannot have body'); } - let inputBody = init.body != null ? + const inputBody = init.body != null ? init.body : isRequest(input) && input.body !== null ? clone(input) : @@ -97,10 +98,12 @@ export default class Request { } } - let signal = isRequest(input) - ? input.signal - : null; - if ('signal' in init) signal = init.signal + let signal = isRequest(input) ? + input.signal : + null; + if ('signal' in init) { + signal = init.signal; + } if (signal != null && !isAbortSignal(signal)) { throw new TypeError('Expected signal to be an instanceof AbortSignal'); @@ -111,16 +114,16 @@ export default class Request { redirect: init.redirect || input.redirect || 'follow', headers, parsedURL, - signal, + signal }; - // node-fetch-only options + // Node-fetch-only options this.follow = init.follow !== undefined ? init.follow : input.follow !== undefined ? - input.follow : 20; + input.follow : 20; this.compress = init.compress !== undefined ? init.compress : input.compress !== undefined ? - input.compress : true; + input.compress : true; this.counter = init.counter || input.counter || 0; this.agent = init.agent || input.agent; } @@ -170,7 +173,7 @@ Object.defineProperties(Request.prototype, { headers: { enumerable: true }, redirect: { enumerable: true }, clone: { enumerable: true }, - signal: { enumerable: true }, + signal: { enumerable: true } }); /** @@ -180,10 +183,10 @@ Object.defineProperties(Request.prototype, { * @return Object The options object to be passed to http.request */ export function getNodeRequestOptions(request) { - const parsedURL = request[INTERNALS].parsedURL; + const { parsedURL } = request[INTERNALS]; const headers = new Headers(request[INTERNALS].headers); - // fetch step 1.3 + // Fetch step 1.3 if (!headers.has('Accept')) { headers.set('Accept', '*/*'); } @@ -198,9 +201,9 @@ export function getNodeRequestOptions(request) { } if ( - request.signal - && request.body instanceof Stream.Readable - && !streamDestructionSupported + request.signal && + request.body instanceof Stream.Readable && + !streamDestructionSupported ) { throw new Error('Cancellation of streamed requests with AbortSignal is not supported in node < 8'); } @@ -210,12 +213,14 @@ export function getNodeRequestOptions(request) { if (request.body == null && /^(POST|PUT)$/i.test(request.method)) { contentLengthValue = '0'; } + if (request.body != null) { const totalBytes = getTotalBytes(request); if (typeof totalBytes === 'number') { contentLengthValue = String(totalBytes); } } + if (contentLengthValue) { headers.set('Content-Length', contentLengthValue); } @@ -230,7 +235,7 @@ export function getNodeRequestOptions(request) { headers.set('Accept-Encoding', 'gzip,deflate'); } - let agent = request.agent; + let { agent } = request; if (typeof agent === 'function') { agent = agent(parsedURL); } diff --git a/src/response.js b/src/response.js index e4801bb70..51a00c8e4 100644 --- a/src/response.js +++ b/src/response.js @@ -1,19 +1,19 @@ /** - * response.js + * Response.js * * Response class provides content decoding */ import http from 'http'; -import Headers from './headers.js'; +import Headers from './headers'; import Body, { clone, extractContentType } from './body'; const INTERNALS = Symbol('Response internals'); -// fix an issue where "STATUS_CODES" aren't a named export for node <10 -const STATUS_CODES = http.STATUS_CODES; +// Fix an issue where "STATUS_CODES" aren't a named export for node <10 +const { STATUS_CODES } = http; /** * Response class @@ -27,7 +27,7 @@ export default class Response { Body.call(this, body, opts); const status = opts.status || 200; - const headers = new Headers(opts.headers) + const headers = new Headers(opts.headers); if (body != null && !headers.has('Content-Type')) { const contentType = extractContentType(body); diff --git a/test/server.js b/test/server.js index e6aaacbf9..d28ec966c 100644 --- a/test/server.js +++ b/test/server.js @@ -5,20 +5,22 @@ import * as stream from 'stream'; import { multipart as Multipart } from 'parted'; let convert; -try { convert = require('encoding').convert; } catch(e) {} +try { + convert = require('encoding').convert; +} catch (error) {} export default class TestServer { constructor() { this.server = http.createServer(this.router); this.port = 30001; this.hostname = 'localhost'; - // node 8 default keepalive timeout is 5000ms + // Node 8 default keepalive timeout is 5000ms // make it shorter here as we want to close server quickly at the end of tests this.server.keepAliveTimeout = 1000; - this.server.on('error', function(err) { + this.server.on('error', err => { console.log(err.stack); }); - this.server.on('connection', function(socket) { + this.server.on('connection', socket => { socket.setTimeout(1500); }); } @@ -32,7 +34,7 @@ export default class TestServer { } router(req, res) { - let p = parse(req.url).pathname; + const p = parse(req.url).pathname; if (p === '/hello') { res.statusCode = 200; @@ -70,7 +72,7 @@ export default class TestServer { res.statusCode = 200; res.setHeader('Content-Type', 'text/plain'); res.setHeader('Content-Encoding', 'gzip'); - zlib.gzip('hello world', function(err, buffer) { + zlib.gzip('hello world', (err, buffer) => { res.end(buffer); }); } @@ -79,8 +81,8 @@ export default class TestServer { res.statusCode = 200; res.setHeader('Content-Type', 'text/plain'); res.setHeader('Content-Encoding', 'gzip'); - zlib.gzip('hello world', function(err, buffer) { - // truncate the CRC checksum and size check at the end of the stream + zlib.gzip('hello world', (err, buffer) => { + // Truncate the CRC checksum and size check at the end of the stream res.end(buffer.slice(0, buffer.length - 8)); }); } @@ -89,7 +91,7 @@ export default class TestServer { res.statusCode = 200; res.setHeader('Content-Type', 'text/plain'); res.setHeader('Content-Encoding', 'deflate'); - zlib.deflate('hello world', function(err, buffer) { + zlib.deflate('hello world', (err, buffer) => { res.end(buffer); }); } @@ -99,18 +101,17 @@ export default class TestServer { res.setHeader('Content-Type', 'text/plain'); if (typeof zlib.createBrotliDecompress === 'function') { res.setHeader('Content-Encoding', 'br'); - zlib.brotliCompress('hello world', function (err, buffer) { + zlib.brotliCompress('hello world', (err, buffer) => { res.end(buffer); }); } } - if (p === '/deflate-raw') { res.statusCode = 200; res.setHeader('Content-Type', 'text/plain'); res.setHeader('Content-Encoding', 'deflate'); - zlib.deflateRaw('hello world', function(err, buffer) { + zlib.deflateRaw('hello world', (err, buffer) => { res.end(buffer); }); } @@ -130,7 +131,7 @@ export default class TestServer { } if (p === '/timeout') { - setTimeout(function() { + setTimeout(() => { res.statusCode = 200; res.setHeader('Content-Type', 'text/plain'); res.end('text'); @@ -141,7 +142,7 @@ export default class TestServer { res.statusCode = 200; res.setHeader('Content-Type', 'text/plain'); res.write('test'); - setTimeout(function() { + setTimeout(() => { res.end('test'); }, 1000); } @@ -155,10 +156,10 @@ export default class TestServer { if (p === '/size/chunk') { res.statusCode = 200; res.setHeader('Content-Type', 'text/plain'); - setTimeout(function() { + setTimeout(() => { res.write('test'); }, 10); - setTimeout(function() { + setTimeout(() => { res.end('test'); }, 20); } @@ -270,7 +271,7 @@ export default class TestServer { if (p === '/redirect/slow') { res.statusCode = 301; res.setHeader('Location', '/redirect/301'); - setTimeout(function() { + setTimeout(() => { res.end(); }, 1000); } @@ -278,7 +279,7 @@ export default class TestServer { if (p === '/redirect/slow-chain') { res.statusCode = 301; res.setHeader('Location', '/redirect/slow'); - setTimeout(function() { + setTimeout(() => { res.end(); }, 10); } @@ -349,8 +350,10 @@ export default class TestServer { res.statusCode = 200; res.setHeader('Content-Type', 'application/json'); let body = ''; - req.on('data', function(c) { body += c }); - req.on('end', function() { + req.on('data', c => { + body += c; + }); + req.on('end', () => { res.end(JSON.stringify({ method: req.method, url: req.url, @@ -365,15 +368,15 @@ export default class TestServer { res.setHeader('Content-Type', 'application/json'); const parser = new Multipart(req.headers['content-type']); let body = ''; - parser.on('part', function(field, part) { + parser.on('part', (field, part) => { body += field + '=' + part; }); - parser.on('end', function() { + parser.on('end', () => { res.end(JSON.stringify({ method: req.method, url: req.url, headers: req.headers, - body: body + body })); }); req.pipe(parser); @@ -382,7 +385,7 @@ export default class TestServer { } if (require.main === module) { - const server = new TestServer; + const server = new TestServer(); server.start(() => { console.log(`Server started listening at port ${server.port}`); }); diff --git a/test/test.js b/test/test.js index 38d3ce050..289139b9c 100644 --- a/test/test.js +++ b/test/test.js @@ -1,5 +1,6 @@ -// test tools +// Test tools +import zlib from 'zlib'; import chai from 'chai'; import chaiPromised from 'chai-as-promised'; import chaiIterator from 'chai-iterator'; @@ -13,6 +14,21 @@ import { URL } from 'whatwg-url'; import { AbortController } from 'abortcontroller-polyfill/dist/abortcontroller'; import AbortController2 from 'abort-controller'; +// Test subjects +import fetch, { + FetchError, + Headers, + Request, + Response +} from '../src'; +import FetchErrorOrig from '../src/fetch-error.js'; +import HeadersOrig, { createHeadersLenient } from '../src/headers.js'; +import RequestOrig from '../src/request.js'; +import ResponseOrig from '../src/response.js'; +import Body, { getTotalBytes, extractContentType } from '../src/body.js'; +import Blob from '../src/blob.js'; +import TestServer from './server'; + const { spawn } = require('child_process'); const http = require('http'); const fs = require('fs'); @@ -28,29 +44,14 @@ const { } = vm.runInNewContext('this'); let convert; -try { convert = require('encoding').convert; } catch(e) { } +try { + convert = require('encoding').convert; +} catch (error) { } chai.use(chaiPromised); chai.use(chaiIterator); chai.use(chaiString); -const expect = chai.expect; - -import TestServer from './server'; - -// test subjects -import fetch, { - FetchError, - Headers, - Request, - Response -} from '../src/'; -import FetchErrorOrig from '../src/fetch-error.js'; -import HeadersOrig, { createHeadersLenient } from '../src/headers.js'; -import RequestOrig from '../src/request.js'; -import ResponseOrig from '../src/response.js'; -import Body, { getTotalBytes, extractContentType } from '../src/body.js'; -import Blob from '../src/blob.js'; -import zlib from "zlib"; +const { expect } = chai; const supportToString = ({ [Symbol.toStringTag]: 'z' @@ -70,14 +71,14 @@ after(done => { }); describe('node-fetch', () => { - it('should return a promise', function() { + it('should return a promise', () => { const url = `${base}hello`; const p = fetch(url); expect(p).to.be.an.instanceof(fetch.Promise); expect(p).to.have.property('then'); }); - it('should allow custom promise', function() { + it('should allow custom promise', () => { const url = `${base}hello`; const old = fetch.Promise; fetch.Promise = then; @@ -86,52 +87,52 @@ describe('node-fetch', () => { fetch.Promise = old; }); - it('should throw error when no promise implementation are found', function() { + it('should throw error when no promise implementation are found', () => { const url = `${base}hello`; const old = fetch.Promise; fetch.Promise = undefined; expect(() => { - fetch(url) + fetch(url); }).to.throw(Error); fetch.Promise = old; }); - it('should expose Headers, Response and Request constructors', function() { + it('should expose Headers, Response and Request constructors', () => { expect(FetchError).to.equal(FetchErrorOrig); expect(Headers).to.equal(HeadersOrig); expect(Response).to.equal(ResponseOrig); expect(Request).to.equal(RequestOrig); }); - (supportToString ? it : it.skip)('should support proper toString output for Headers, Response and Request objects', function() { + (supportToString ? it : it.skip)('should support proper toString output for Headers, Response and Request objects', () => { expect(new Headers().toString()).to.equal('[object Headers]'); expect(new Response().toString()).to.equal('[object Response]'); expect(new Request(base).toString()).to.equal('[object Request]'); }); - it('should reject with error if url is protocol relative', function() { + it('should reject with error if url is protocol relative', () => { const url = '//example.com/'; return expect(fetch(url)).to.eventually.be.rejectedWith(TypeError, 'Only absolute URLs are supported'); }); - it('should reject with error if url is relative path', function() { + it('should reject with error if url is relative path', () => { const url = '/some/path'; return expect(fetch(url)).to.eventually.be.rejectedWith(TypeError, 'Only absolute URLs are supported'); }); - it('should reject with error if protocol is unsupported', function() { + it('should reject with error if protocol is unsupported', () => { const url = 'ftp://example.com/'; return expect(fetch(url)).to.eventually.be.rejectedWith(TypeError, 'Only HTTP(S) protocols are supported'); }); - it('should reject with error on network failure', function() { + it('should reject with error on network failure', () => { const url = 'http://localhost:50000/'; return expect(fetch(url)).to.eventually.be.rejected .and.be.an.instanceOf(FetchError) .and.include({ type: 'system', code: 'ECONNREFUSED', errno: 'ECONNREFUSED' }); }); - it('should resolve into response', function() { + it('should resolve into response', () => { const url = `${base}hello`; return fetch(url).then(res => { expect(res).to.be.an.instanceof(Response); @@ -146,7 +147,7 @@ describe('node-fetch', () => { }); }); - it('should accept plain text response', function() { + it('should accept plain text response', () => { const url = `${base}plain`; return fetch(url).then(res => { expect(res.headers.get('content-type')).to.equal('text/plain'); @@ -158,7 +159,7 @@ describe('node-fetch', () => { }); }); - it('should accept html response (like plain text)', function() { + it('should accept html response (like plain text)', () => { const url = `${base}html`; return fetch(url).then(res => { expect(res.headers.get('content-type')).to.equal('text/html'); @@ -170,7 +171,7 @@ describe('node-fetch', () => { }); }); - it('should accept json response', function() { + it('should accept json response', () => { const url = `${base}json`; return fetch(url).then(res => { expect(res.headers.get('content-type')).to.equal('application/json'); @@ -182,7 +183,7 @@ describe('node-fetch', () => { }); }); - it('should send request with custom headers', function() { + it('should send request with custom headers', () => { const url = `${base}inspect`; const opts = { headers: { 'x-custom-header': 'abc' } @@ -194,7 +195,7 @@ describe('node-fetch', () => { }); }); - it('should accept headers instance', function() { + it('should accept headers instance', () => { const url = `${base}inspect`; const opts = { headers: new Headers({ 'x-custom-header': 'abc' }) @@ -206,7 +207,7 @@ describe('node-fetch', () => { }); }); - it('should accept custom host header', function() { + it('should accept custom host header', () => { const url = `${base}inspect`; const opts = { headers: { @@ -216,11 +217,11 @@ describe('node-fetch', () => { return fetch(url, opts).then(res => { return res.json(); }).then(res => { - expect(res.headers['host']).to.equal('example.com'); + expect(res.headers.host).to.equal('example.com'); }); }); - it('should accept custom HoSt header', function() { + it('should accept custom HoSt header', () => { const url = `${base}inspect`; const opts = { headers: { @@ -230,11 +231,11 @@ describe('node-fetch', () => { return fetch(url, opts).then(res => { return res.json(); }).then(res => { - expect(res.headers['host']).to.equal('example.com'); + expect(res.headers.host).to.equal('example.com'); }); }); - it('should follow redirect code 301', function() { + it('should follow redirect code 301', () => { const url = `${base}redirect/301`; return fetch(url).then(res => { expect(res.url).to.equal(`${base}inspect`); @@ -243,7 +244,7 @@ describe('node-fetch', () => { }); }); - it('should follow redirect code 302', function() { + it('should follow redirect code 302', () => { const url = `${base}redirect/302`; return fetch(url).then(res => { expect(res.url).to.equal(`${base}inspect`); @@ -251,7 +252,7 @@ describe('node-fetch', () => { }); }); - it('should follow redirect code 303', function() { + it('should follow redirect code 303', () => { const url = `${base}redirect/303`; return fetch(url).then(res => { expect(res.url).to.equal(`${base}inspect`); @@ -259,7 +260,7 @@ describe('node-fetch', () => { }); }); - it('should follow redirect code 307', function() { + it('should follow redirect code 307', () => { const url = `${base}redirect/307`; return fetch(url).then(res => { expect(res.url).to.equal(`${base}inspect`); @@ -267,7 +268,7 @@ describe('node-fetch', () => { }); }); - it('should follow redirect code 308', function() { + it('should follow redirect code 308', () => { const url = `${base}redirect/308`; return fetch(url).then(res => { expect(res.url).to.equal(`${base}inspect`); @@ -275,7 +276,7 @@ describe('node-fetch', () => { }); }); - it('should follow redirect chain', function() { + it('should follow redirect chain', () => { const url = `${base}redirect/chain`; return fetch(url).then(res => { expect(res.url).to.equal(`${base}inspect`); @@ -283,7 +284,7 @@ describe('node-fetch', () => { }); }); - it('should follow POST request redirect code 301 with GET', function() { + it('should follow POST request redirect code 301 with GET', () => { const url = `${base}redirect/301`; const opts = { method: 'POST', @@ -299,7 +300,7 @@ describe('node-fetch', () => { }); }); - it('should follow PATCH request redirect code 301 with PATCH', function() { + it('should follow PATCH request redirect code 301 with PATCH', () => { const url = `${base}redirect/301`; const opts = { method: 'PATCH', @@ -315,7 +316,7 @@ describe('node-fetch', () => { }); }); - it('should follow POST request redirect code 302 with GET', function() { + it('should follow POST request redirect code 302 with GET', () => { const url = `${base}redirect/302`; const opts = { method: 'POST', @@ -331,7 +332,7 @@ describe('node-fetch', () => { }); }); - it('should follow PATCH request redirect code 302 with PATCH', function() { + it('should follow PATCH request redirect code 302 with PATCH', () => { const url = `${base}redirect/302`; const opts = { method: 'PATCH', @@ -347,7 +348,7 @@ describe('node-fetch', () => { }); }); - it('should follow redirect code 303 with GET', function() { + it('should follow redirect code 303 with GET', () => { const url = `${base}redirect/303`; const opts = { method: 'PUT', @@ -363,7 +364,7 @@ describe('node-fetch', () => { }); }); - it('should follow PATCH request redirect code 307 with PATCH', function() { + it('should follow PATCH request redirect code 307 with PATCH', () => { const url = `${base}redirect/307`; const opts = { method: 'PATCH', @@ -379,7 +380,7 @@ describe('node-fetch', () => { }); }); - it('should not follow non-GET redirect if body is a readable stream', function() { + it('should not follow non-GET redirect if body is a readable stream', () => { const url = `${base}redirect/307`; const opts = { method: 'PATCH', @@ -390,38 +391,38 @@ describe('node-fetch', () => { .and.have.property('type', 'unsupported-redirect'); }); - it('should obey maximum redirect, reject case', function() { + it('should obey maximum redirect, reject case', () => { const url = `${base}redirect/chain`; const opts = { follow: 1 - } + }; return expect(fetch(url, opts)).to.eventually.be.rejected .and.be.an.instanceOf(FetchError) .and.have.property('type', 'max-redirect'); }); - it('should obey redirect chain, resolve case', function() { + it('should obey redirect chain, resolve case', () => { const url = `${base}redirect/chain`; const opts = { follow: 2 - } + }; return fetch(url, opts).then(res => { expect(res.url).to.equal(`${base}inspect`); expect(res.status).to.equal(200); }); }); - it('should allow not following redirect', function() { + it('should allow not following redirect', () => { const url = `${base}redirect/301`; const opts = { follow: 0 - } + }; return expect(fetch(url, opts)).to.eventually.be.rejected .and.be.an.instanceOf(FetchError) .and.have.property('type', 'max-redirect'); }); - it('should support redirect mode, manual flag', function() { + it('should support redirect mode, manual flag', () => { const url = `${base}redirect/301`; const opts = { redirect: 'manual' @@ -433,7 +434,7 @@ describe('node-fetch', () => { }); }); - it('should support redirect mode, error flag', function() { + it('should support redirect mode, error flag', () => { const url = `${base}redirect/301`; const opts = { redirect: 'error' @@ -443,7 +444,7 @@ describe('node-fetch', () => { .and.have.property('type', 'no-redirect'); }); - it('should support redirect mode, manual flag when there is no redirect', function() { + it('should support redirect mode, manual flag when there is no redirect', () => { const url = `${base}hello`; const opts = { redirect: 'manual' @@ -455,7 +456,7 @@ describe('node-fetch', () => { }); }); - it('should follow redirect code 301 and keep existing headers', function() { + it('should follow redirect code 301 and keep existing headers', () => { const url = `${base}redirect/301`; const opts = { headers: new Headers({ 'x-custom-header': 'abc' }) @@ -468,7 +469,7 @@ describe('node-fetch', () => { }); }); - it('should treat broken redirect as ordinary response (follow)', function() { + it('should treat broken redirect as ordinary response (follow)', () => { const url = `${base}redirect/no-location`; return fetch(url).then(res => { expect(res.url).to.equal(url); @@ -477,7 +478,7 @@ describe('node-fetch', () => { }); }); - it('should treat broken redirect as ordinary response (manual)', function() { + it('should treat broken redirect as ordinary response (manual)', () => { const url = `${base}redirect/no-location`; const opts = { redirect: 'manual' @@ -489,25 +490,25 @@ describe('node-fetch', () => { }); }); - it('should set redirected property on response when redirect', function() { + it('should set redirected property on response when redirect', () => { const url = `${base}redirect/301`; return fetch(url).then(res => { expect(res.redirected).to.be.true; }); }); - it('should not set redirected property on response without redirect', function() { - const url = `${base}hello`; + it('should not set redirected property on response without redirect', () => { + const url = `${base}hello`; return fetch(url).then(res => { expect(res.redirected).to.be.false; }); }); - it('should ignore invalid headers', function() { - var headers = { + it('should ignore invalid headers', () => { + let headers = { 'Invalid-Header ': 'abc\r\n', - 'Invalid-Header-Value': '\x07k\r\n', - 'Set-Cookie': ['\x07k\r\n', '\x07kk\r\n'] + 'Invalid-Header-Value': '\u0007k\r\n', + 'Set-Cookie': ['\u0007k\r\n', '\u0007kk\r\n'] }; headers = createHeadersLenient(headers); expect(headers).to.not.have.property('Invalid-Header '); @@ -515,7 +516,7 @@ describe('node-fetch', () => { expect(headers).to.not.have.property('Set-Cookie'); }); - it('should handle client-error response', function() { + it('should handle client-error response', () => { const url = `${base}error/400`; return fetch(url).then(res => { expect(res.headers.get('content-type')).to.equal('text/plain'); @@ -530,7 +531,7 @@ describe('node-fetch', () => { }); }); - it('should handle server-error response', function() { + it('should handle server-error response', () => { const url = `${base}error/500`; return fetch(url).then(res => { expect(res.headers.get('content-type')).to.equal('text/plain'); @@ -545,21 +546,21 @@ describe('node-fetch', () => { }); }); - it('should handle network-error response', function() { + it('should handle network-error response', () => { const url = `${base}error/reset`; return expect(fetch(url)).to.eventually.be.rejected .and.be.an.instanceOf(FetchError) .and.have.property('code', 'ECONNRESET'); }); - it('should handle DNS-error response', function() { + it('should handle DNS-error response', () => { const url = 'http://domain.invalid'; return expect(fetch(url)).to.eventually.be.rejected .and.be.an.instanceOf(FetchError) .and.have.property('code', 'ENOTFOUND'); }); - it('should reject invalid json response', function() { + it('should reject invalid json response', () => { const url = `${base}error/json`; return fetch(url).then(res => { expect(res.headers.get('content-type')).to.equal('application/json'); @@ -569,7 +570,7 @@ describe('node-fetch', () => { }); }); - it('should handle no content response', function() { + it('should handle no content response', () => { const url = `${base}no-content`; return fetch(url).then(res => { expect(res.status).to.equal(204); @@ -582,7 +583,7 @@ describe('node-fetch', () => { }); }); - it('should reject when trying to parse no content response as json', function() { + it('should reject when trying to parse no content response as json', () => { const url = `${base}no-content`; return fetch(url).then(res => { expect(res.status).to.equal(204); @@ -594,7 +595,7 @@ describe('node-fetch', () => { }); }); - it('should handle no content response with gzip encoding', function() { + it('should handle no content response with gzip encoding', () => { const url = `${base}no-content/gzip`; return fetch(url).then(res => { expect(res.status).to.equal(204); @@ -608,7 +609,7 @@ describe('node-fetch', () => { }); }); - it('should handle not modified response', function() { + it('should handle not modified response', () => { const url = `${base}not-modified`; return fetch(url).then(res => { expect(res.status).to.equal(304); @@ -621,7 +622,7 @@ describe('node-fetch', () => { }); }); - it('should handle not modified response with gzip encoding', function() { + it('should handle not modified response with gzip encoding', () => { const url = `${base}not-modified/gzip`; return fetch(url).then(res => { expect(res.status).to.equal(304); @@ -635,7 +636,7 @@ describe('node-fetch', () => { }); }); - it('should decompress gzip response', function() { + it('should decompress gzip response', () => { const url = `${base}gzip`; return fetch(url).then(res => { expect(res.headers.get('content-type')).to.equal('text/plain'); @@ -646,7 +647,7 @@ describe('node-fetch', () => { }); }); - it('should decompress slightly invalid gzip response', function() { + it('should decompress slightly invalid gzip response', () => { const url = `${base}gzip-truncated`; return fetch(url).then(res => { expect(res.headers.get('content-type')).to.equal('text/plain'); @@ -657,7 +658,7 @@ describe('node-fetch', () => { }); }); - it('should decompress deflate response', function() { + it('should decompress deflate response', () => { const url = `${base}deflate`; return fetch(url).then(res => { expect(res.headers.get('content-type')).to.equal('text/plain'); @@ -668,7 +669,7 @@ describe('node-fetch', () => { }); }); - it('should decompress deflate raw response from old apache server', function() { + it('should decompress deflate raw response from old apache server', () => { const url = `${base}deflate-raw`; return fetch(url).then(res => { expect(res.headers.get('content-type')).to.equal('text/plain'); @@ -679,8 +680,11 @@ describe('node-fetch', () => { }); }); - it('should decompress brotli response', function() { - if(typeof zlib.createBrotliDecompress !== 'function') this.skip(); + it('should decompress brotli response', function () { + if (typeof zlib.createBrotliDecompress !== 'function') { + this.skip(); + } + const url = `${base}brotli`; return fetch(url).then(res => { expect(res.headers.get('content-type')).to.equal('text/plain'); @@ -691,8 +695,11 @@ describe('node-fetch', () => { }); }); - it('should handle no content response with brotli encoding', function() { - if(typeof zlib.createBrotliDecompress !== 'function') this.skip(); + it('should handle no content response with brotli encoding', function () { + if (typeof zlib.createBrotliDecompress !== 'function') { + this.skip(); + } + const url = `${base}no-content/brotli`; return fetch(url).then(res => { expect(res.status).to.equal(204); @@ -706,7 +713,7 @@ describe('node-fetch', () => { }); }); - it('should skip decompression if unsupported', function() { + it('should skip decompression if unsupported', () => { const url = `${base}sdch`; return fetch(url).then(res => { expect(res.headers.get('content-type')).to.equal('text/plain'); @@ -717,7 +724,7 @@ describe('node-fetch', () => { }); }); - it('should reject if response compression is invalid', function() { + it('should reject if response compression is invalid', () => { const url = `${base}invalid-content-encoding`; return fetch(url).then(res => { expect(res.headers.get('content-type')).to.equal('text/plain'); @@ -727,7 +734,7 @@ describe('node-fetch', () => { }); }); - it('should handle errors on the body stream even if it is not used', function(done) { + it('should handle errors on the body stream even if it is not used', done => { const url = `${base}invalid-content-encoding`; fetch(url) .then(res => { @@ -742,12 +749,11 @@ describe('node-fetch', () => { }); }); - it('should collect handled errors on the body stream to reject if the body is used later', function() { - + it('should collect handled errors on the body stream to reject if the body is used later', () => { function delay(value) { - return new Promise((resolve) => { + return new Promise(resolve => { setTimeout(() => { - resolve(value) + resolve(value); }, 20); }); } @@ -761,7 +767,7 @@ describe('node-fetch', () => { }); }); - it('should allow disabling auto decompression', function() { + it('should allow disabling auto decompression', () => { const url = `${base}gzip`; const opts = { compress: false @@ -775,7 +781,7 @@ describe('node-fetch', () => { }); }); - it('should not overwrite existing accept-encoding header when auto decompression is true', function() { + it('should not overwrite existing accept-encoding header when auto decompression is true', () => { const url = `${base}inspect`; const opts = { compress: true, @@ -788,7 +794,7 @@ describe('node-fetch', () => { }); }); - it('should allow custom timeout', function() { + it('should allow custom timeout', () => { const url = `${base}timeout`; const opts = { timeout: 20 @@ -798,7 +804,7 @@ describe('node-fetch', () => { .and.have.property('type', 'request-timeout'); }); - it('should allow custom timeout on response body', function() { + it('should allow custom timeout on response body', () => { const url = `${base}slow`; const opts = { timeout: 20 @@ -811,7 +817,7 @@ describe('node-fetch', () => { }); }); - it('should allow custom timeout on redirected requests', function() { + it('should allow custom timeout on redirected requests', () => { const url = `${base}redirect/slow-chain`; const opts = { timeout: 20 @@ -875,12 +881,12 @@ describe('node-fetch', () => { .and.be.an.instanceOf(Error) .and.include({ type: 'aborted', - name: 'AbortError', + name: 'AbortError' }) )); }); - it('should reject immediately if signal has already been aborted', function () { + it('should reject immediately if signal has already been aborted', () => { const url = `${base}timeout`; const controller = new AbortController(); const opts = { @@ -892,11 +898,11 @@ describe('node-fetch', () => { .and.be.an.instanceOf(Error) .and.include({ type: 'aborted', - name: 'AbortError', + name: 'AbortError' }); }); - it('should clear internal timeout when request is cancelled with an AbortSignal', function(done) { + it('should clear internal timeout when request is cancelled with an AbortSignal', function (done) { this.timeout(2000); const script = ` var AbortController = require('abortcontroller-polyfill/dist/cjs-ponyfill').AbortController; @@ -906,14 +912,14 @@ describe('node-fetch', () => { { signal: controller.signal, timeout: 10000 } ); setTimeout(function () { controller.abort(); }, 20); - ` + `; spawn('node', ['-e', script]) .on('exit', () => { done(); }); }); - it('should remove internal AbortSignal event listener after request is aborted', function () { + it('should remove internal AbortSignal event listener after request is aborted', () => { const controller = new AbortController(); const { signal } = controller; const promise = fetch( @@ -930,7 +936,7 @@ describe('node-fetch', () => { return result; }); - it('should allow redirects to be aborted', function() { + it('should allow redirects to be aborted', () => { const abortController = new AbortController(); const request = new Request(`${base}redirect/slow`, { signal: abortController.signal @@ -943,7 +949,7 @@ describe('node-fetch', () => { .and.have.property('name', 'AbortError'); }); - it('should allow redirected response body to be aborted', function() { + it('should allow redirected response body to be aborted', () => { const abortController = new AbortController(); const request = new Request(`${base}redirect/slow-stream`, { signal: abortController.signal @@ -968,9 +974,9 @@ describe('node-fetch', () => { return Promise.all([ expect(fetchHtml).to.eventually.be.fulfilled.and.equal(''), expect(fetchResponseError).to.be.eventually.rejected, - expect(fetchRedirect).to.eventually.be.fulfilled, + expect(fetchRedirect).to.eventually.be.fulfilled ]).then(() => { - expect(signal.listeners.abort.length).to.equal(0) + expect(signal.listeners.abort.length).to.equal(0); }); }); @@ -981,7 +987,7 @@ describe('node-fetch', () => { { signal: controller.signal } )) .to.eventually.be.fulfilled - .then((res) => { + .then(res => { const promise = res.text(); controller.abort(); return expect(promise) @@ -998,7 +1004,7 @@ describe('node-fetch', () => { { signal: controller.signal } )) .to.eventually.be.fulfilled - .then((res) => { + .then(res => { controller.abort(); return expect(res.text()) .to.eventually.be.rejected @@ -1007,15 +1013,15 @@ describe('node-fetch', () => { }); }); - it('should emit error event to response body with an AbortError when aborted before underlying stream is closed', (done) => { + it('should emit error event to response body with an AbortError when aborted before underlying stream is closed', done => { const controller = new AbortController(); expect(fetch( `${base}slow`, { signal: controller.signal } )) .to.eventually.be.fulfilled - .then((res) => { - res.body.on('error', (err) => { + .then(res => { + res.body.on('error', err => { expect(err) .to.be.an.instanceof(Error) .and.have.property('name', 'AbortError'); @@ -1036,12 +1042,12 @@ describe('node-fetch', () => { const result = Promise.all([ new Promise((resolve, reject) => { - body.on('error', (error) => { + body.on('error', error => { try { - expect(error).to.be.an.instanceof(Error).and.have.property('name', 'AbortError') + expect(error).to.be.an.instanceof(Error).and.have.property('name', 'AbortError'); resolve(); - } catch (err) { - reject(err); + } catch (error2) { + reject(error2); } }); }), @@ -1082,18 +1088,18 @@ describe('node-fetch', () => { expect(fetch(`${base}inspect`, { signal: Object.create(null) })) .to.be.eventually.rejected .and.be.an.instanceof(TypeError) - .and.have.property('message').includes('AbortSignal'), + .and.have.property('message').includes('AbortSignal') ]); }); - it('should set default User-Agent', function () { + it('should set default User-Agent', () => { const url = `${base}inspect`; return fetch(url).then(res => res.json()).then(res => { expect(res.headers['user-agent']).to.startWith('node-fetch/'); }); }); - it('should allow setting User-Agent', function () { + it('should allow setting User-Agent', () => { const url = `${base}inspect`; const opts = { headers: { @@ -1105,18 +1111,18 @@ describe('node-fetch', () => { }); }); - it('should set default Accept header', function () { + it('should set default Accept header', () => { const url = `${base}inspect`; fetch(url).then(res => res.json()).then(res => { expect(res.headers.accept).to.equal('*/*'); }); }); - it('should allow setting Accept header', function () { + it('should allow setting Accept header', () => { const url = `${base}inspect`; const opts = { headers: { - 'accept': 'application/json' + accept: 'application/json' } }; return fetch(url, opts).then(res => res.json()).then(res => { @@ -1124,7 +1130,7 @@ describe('node-fetch', () => { }); }); - it('should allow POST request', function() { + it('should allow POST request', () => { const url = `${base}inspect`; const opts = { method: 'POST' @@ -1139,7 +1145,7 @@ describe('node-fetch', () => { }); }); - it('should allow POST request with string body', function() { + it('should allow POST request with string body', () => { const url = `${base}inspect`; const opts = { method: 'POST', @@ -1156,7 +1162,7 @@ describe('node-fetch', () => { }); }); - it('should allow POST request with buffer body', function() { + it('should allow POST request with buffer body', () => { const url = `${base}inspect`; const opts = { method: 'POST', @@ -1173,7 +1179,7 @@ describe('node-fetch', () => { }); }); - it('should allow POST request with ArrayBuffer body', function() { + it('should allow POST request with ArrayBuffer body', () => { const url = `${base}inspect`; const opts = { method: 'POST', @@ -1188,13 +1194,14 @@ describe('node-fetch', () => { }); }); - it('should allow POST request with ArrayBuffer body from a VM context', function() { + it('should allow POST request with ArrayBuffer body from a VM context', function () { // TODO: Node.js v4 doesn't support ArrayBuffer from other contexts, so we skip this test, drop this check once Node.js v4 support is not needed try { Buffer.from(new VMArrayBuffer()); - } catch (err) { + } catch (error) { this.skip(); } + const url = `${base}inspect`; const opts = { method: 'POST', @@ -1209,7 +1216,7 @@ describe('node-fetch', () => { }); }); - it('should allow POST request with ArrayBufferView (Uint8Array) body', function() { + it('should allow POST request with ArrayBufferView (Uint8Array) body', () => { const url = `${base}inspect`; const opts = { method: 'POST', @@ -1224,7 +1231,7 @@ describe('node-fetch', () => { }); }); - it('should allow POST request with ArrayBufferView (DataView) body', function() { + it('should allow POST request with ArrayBufferView (DataView) body', () => { const url = `${base}inspect`; const opts = { method: 'POST', @@ -1239,13 +1246,14 @@ describe('node-fetch', () => { }); }); - it('should allow POST request with ArrayBufferView (Uint8Array) body from a VM context', function() { + it('should allow POST request with ArrayBufferView (Uint8Array) body from a VM context', function () { // TODO: Node.js v4 doesn't support ArrayBufferView from other contexts, so we skip this test, drop this check once Node.js v4 support is not needed try { Buffer.from(new VMArrayBuffer()); - } catch (err) { + } catch (error) { this.skip(); } + const url = `${base}inspect`; const opts = { method: 'POST', @@ -1261,7 +1269,7 @@ describe('node-fetch', () => { }); // TODO: Node.js v4 doesn't support necessary Buffer API, so we skip this test, drop this check once Node.js v4 support is not needed - (Buffer.from.length === 3 ? it : it.skip)('should allow POST request with ArrayBufferView (Uint8Array, offset, length) body', function() { + (Buffer.from.length === 3 ? it : it.skip)('should allow POST request with ArrayBufferView (Uint8Array, offset, length) body', () => { const url = `${base}inspect`; const opts = { method: 'POST', @@ -1276,7 +1284,7 @@ describe('node-fetch', () => { }); }); - it('should allow POST request with blob body without type', function() { + it('should allow POST request with blob body without type', () => { const url = `${base}inspect`; const opts = { method: 'POST', @@ -1293,7 +1301,7 @@ describe('node-fetch', () => { }); }); - it('should allow POST request with blob body with type', function() { + it('should allow POST request with blob body with type', () => { const url = `${base}inspect`; const opts = { method: 'POST', @@ -1312,7 +1320,7 @@ describe('node-fetch', () => { }); }); - it('should allow POST request with readable stream as body', function() { + it('should allow POST request with readable stream as body', () => { let body = resumer().queue('a=1').end(); body = body.pipe(new stream.PassThrough()); @@ -1332,9 +1340,9 @@ describe('node-fetch', () => { }); }); - it('should allow POST request with form-data as body', function() { + it('should allow POST request with form-data as body', () => { const form = new FormData(); - form.append('a','1'); + form.append('a', '1'); const url = `${base}multipart`; const opts = { @@ -1351,7 +1359,7 @@ describe('node-fetch', () => { }); }); - it('should allow POST request with form-data using stream as body', function() { + it('should allow POST request with form-data using stream as body', () => { const form = new FormData(); form.append('my_field', fs.createReadStream(path.join(__dirname, 'dummy.txt'))); @@ -1371,12 +1379,12 @@ describe('node-fetch', () => { }); }); - it('should allow POST request with form-data as body and custom headers', function() { + it('should allow POST request with form-data as body and custom headers', () => { const form = new FormData(); - form.append('a','1'); + form.append('a', '1'); const headers = form.getHeaders(); - headers['b'] = '2'; + headers.b = '2'; const url = `${base}multipart`; const opts = { @@ -1395,9 +1403,9 @@ describe('node-fetch', () => { }); }); - it('should allow POST request with object body', function() { + it('should allow POST request with object body', () => { const url = `${base}inspect`; - // note that fetch simply calls tostring on an object + // Note that fetch simply calls tostring on an object const opts = { method: 'POST', body: { a: 1 } @@ -1414,45 +1422,45 @@ describe('node-fetch', () => { const itUSP = typeof URLSearchParams === 'function' ? it : it.skip; - itUSP('constructing a Response with URLSearchParams as body should have a Content-Type', function() { + itUSP('constructing a Response with URLSearchParams as body should have a Content-Type', () => { const params = new URLSearchParams(); const res = new Response(params); res.headers.get('Content-Type'); expect(res.headers.get('Content-Type')).to.equal('application/x-www-form-urlencoded;charset=UTF-8'); }); - itUSP('constructing a Request with URLSearchParams as body should have a Content-Type', function() { + itUSP('constructing a Request with URLSearchParams as body should have a Content-Type', () => { const params = new URLSearchParams(); const req = new Request(base, { method: 'POST', body: params }); expect(req.headers.get('Content-Type')).to.equal('application/x-www-form-urlencoded;charset=UTF-8'); }); - itUSP('Reading a body with URLSearchParams should echo back the result', function() { + itUSP('Reading a body with URLSearchParams should echo back the result', () => { const params = new URLSearchParams(); - params.append('a','1'); + params.append('a', '1'); return new Response(params).text().then(text => { expect(text).to.equal('a=1'); }); }); // Body should been cloned... - itUSP('constructing a Request/Response with URLSearchParams and mutating it should not affected body', function() { + itUSP('constructing a Request/Response with URLSearchParams and mutating it should not affected body', () => { const params = new URLSearchParams(); - const req = new Request(`${base}inspect`, { method: 'POST', body: params }) - params.append('a','1') + const req = new Request(`${base}inspect`, { method: 'POST', body: params }); + params.append('a', '1'); return req.text().then(text => { expect(text).to.equal(''); }); }); - itUSP('should allow POST request with URLSearchParams as body', function() { + itUSP('should allow POST request with URLSearchParams as body', () => { const params = new URLSearchParams(); - params.append('a','1'); + params.append('a', '1'); const url = `${base}inspect`; const opts = { method: 'POST', - body: params, + body: params }; return fetch(url, opts).then(res => { return res.json(); @@ -1464,15 +1472,15 @@ describe('node-fetch', () => { }); }); - itUSP('should still recognize URLSearchParams when extended', function() { + itUSP('should still recognize URLSearchParams when extended', () => { class CustomSearchParams extends URLSearchParams {} const params = new CustomSearchParams(); - params.append('a','1'); + params.append('a', '1'); const url = `${base}inspect`; const opts = { method: 'POST', - body: params, + body: params }; return fetch(url, opts).then(res => { return res.json(); @@ -1484,17 +1492,17 @@ describe('node-fetch', () => { }); }); - /* for 100% code coverage, checks for duck-typing-only detection + /* For 100% code coverage, checks for duck-typing-only detection * where both constructor.name and brand tests fail */ - it('should still recognize URLSearchParams when extended from polyfill', function() { + it('should still recognize URLSearchParams when extended from polyfill', () => { class CustomPolyfilledSearchParams extends URLSearchParams_Polyfill {} const params = new CustomPolyfilledSearchParams(); - params.append('a','1'); + params.append('a', '1'); const url = `${base}inspect`; const opts = { method: 'POST', - body: params, + body: params }; return fetch(url, opts).then(res => { return res.json(); @@ -1506,9 +1514,9 @@ describe('node-fetch', () => { }); }); - it('should overwrite Content-Length if possible', function() { + it('should overwrite Content-Length if possible', () => { const url = `${base}inspect`; - // note that fetch simply calls tostring on an object + // Note that fetch simply calls tostring on an object const opts = { method: 'POST', headers: { @@ -1527,7 +1535,7 @@ describe('node-fetch', () => { }); }); - it('should allow PUT request', function() { + it('should allow PUT request', () => { const url = `${base}inspect`; const opts = { method: 'PUT', @@ -1541,7 +1549,7 @@ describe('node-fetch', () => { }); }); - it('should allow DELETE request', function() { + it('should allow DELETE request', () => { const url = `${base}inspect`; const opts = { method: 'DELETE' @@ -1553,7 +1561,7 @@ describe('node-fetch', () => { }); }); - it('should allow DELETE request with string body', function() { + it('should allow DELETE request with string body', () => { const url = `${base}inspect`; const opts = { method: 'DELETE', @@ -1569,7 +1577,7 @@ describe('node-fetch', () => { }); }); - it('should allow PATCH request', function() { + it('should allow PATCH request', () => { const url = `${base}inspect`; const opts = { method: 'PATCH', @@ -1583,7 +1591,7 @@ describe('node-fetch', () => { }); }); - it('should allow HEAD request', function() { + it('should allow HEAD request', () => { const url = `${base}hello`; const opts = { method: 'HEAD' @@ -1599,7 +1607,7 @@ describe('node-fetch', () => { }); }); - it('should allow HEAD request with content-encoding header', function() { + it('should allow HEAD request with content-encoding header', () => { const url = `${base}error/404`; const opts = { method: 'HEAD' @@ -1613,7 +1621,7 @@ describe('node-fetch', () => { }); }); - it('should allow OPTIONS request', function() { + it('should allow OPTIONS request', () => { const url = `${base}options`; const opts = { method: 'OPTIONS' @@ -1626,7 +1634,7 @@ describe('node-fetch', () => { }); }); - it('should reject decoding body twice', function() { + it('should reject decoding body twice', () => { const url = `${base}plain`; return fetch(url).then(res => { expect(res.headers.get('content-type')).to.equal('text/plain'); @@ -1637,7 +1645,7 @@ describe('node-fetch', () => { }); }); - it('should support maximum response size, multiple chunk', function() { + it('should support maximum response size, multiple chunk', () => { const url = `${base}size/chunk`; const opts = { size: 5 @@ -1651,7 +1659,7 @@ describe('node-fetch', () => { }); }); - it('should support maximum response size, single chunk', function() { + it('should support maximum response size, single chunk', () => { const url = `${base}size/long`; const opts = { size: 5 @@ -1665,7 +1673,7 @@ describe('node-fetch', () => { }); }); - it('should allow piping response body as stream', function() { + it('should allow piping response body as stream', () => { const url = `${base}hello`; return fetch(url).then(res => { expect(res.body).to.be.an.instanceof(stream.Transform); @@ -1673,12 +1681,13 @@ describe('node-fetch', () => { if (chunk === null) { return; } + expect(chunk.toString()).to.equal('world'); }); }); }); - it('should allow cloning a response, and use both as stream', function() { + it('should allow cloning a response, and use both as stream', () => { const url = `${base}hello`; return fetch(url).then(res => { const r1 = res.clone(); @@ -1688,6 +1697,7 @@ describe('node-fetch', () => { if (chunk === null) { return; } + expect(chunk.toString()).to.equal('world'); }; @@ -1698,23 +1708,23 @@ describe('node-fetch', () => { }); }); - it('should allow cloning a json response and log it as text response', function() { + it('should allow cloning a json response and log it as text response', () => { const url = `${base}json`; return fetch(url).then(res => { const r1 = res.clone(); return Promise.all([res.json(), r1.text()]).then(results => { - expect(results[0]).to.deep.equal({name: 'value'}); + expect(results[0]).to.deep.equal({ name: 'value' }); expect(results[1]).to.equal('{"name":"value"}'); }); }); }); - it('should allow cloning a json response, and then log it as text response', function() { + it('should allow cloning a json response, and then log it as text response', () => { const url = `${base}json`; return fetch(url).then(res => { const r1 = res.clone(); return res.json().then(result => { - expect(result).to.deep.equal({name: 'value'}); + expect(result).to.deep.equal({ name: 'value' }); return r1.text().then(result => { expect(result).to.equal('{"name":"value"}'); }); @@ -1722,20 +1732,20 @@ describe('node-fetch', () => { }); }); - it('should allow cloning a json response, first log as text response, then return json object', function() { + it('should allow cloning a json response, first log as text response, then return json object', () => { const url = `${base}json`; return fetch(url).then(res => { const r1 = res.clone(); return r1.text().then(result => { expect(result).to.equal('{"name":"value"}'); return res.json().then(result => { - expect(result).to.deep.equal({name: 'value'}); + expect(result).to.deep.equal({ name: 'value' }); }); }); }); }); - it('should not allow cloning a response after its been used', function() { + it('should not allow cloning a response after its been used', () => { const url = `${base}hello`; return fetch(url).then(res => res.text().then(result => { @@ -1746,7 +1756,7 @@ describe('node-fetch', () => { ); }); - it('should allow get all responses of a header', function() { + it('should allow get all responses of a header', () => { const url = `${base}cookie`; return fetch(url).then(res => { const expected = 'a=1, b=1'; @@ -1755,7 +1765,7 @@ describe('node-fetch', () => { }); }); - it('should return all headers using raw()', function() { + it('should return all headers using raw()', () => { const url = `${base}cookie`; return fetch(url).then(res => { const expected = [ @@ -1767,7 +1777,7 @@ describe('node-fetch', () => { }); }); - it('should allow deleting header', function() { + it('should allow deleting header', () => { const url = `${base}cookie`; return fetch(url).then(res => { res.headers.delete('set-cookie'); @@ -1775,7 +1785,7 @@ describe('node-fetch', () => { }); }); - it('should send request with connection keep-alive if agent is provided', function() { + it('should send request with connection keep-alive if agent is provided', () => { const url = `${base}inspect`; const opts = { agent: new http.Agent({ @@ -1785,11 +1795,11 @@ describe('node-fetch', () => { return fetch(url, opts).then(res => { return res.json(); }).then(res => { - expect(res.headers['connection']).to.equal('keep-alive'); + expect(res.headers.connection).to.equal('keep-alive'); }); }); - it('should support fetch with Request instance', function() { + it('should support fetch with Request instance', () => { const url = `${base}hello`; const req = new Request(url); return fetch(req).then(res => { @@ -1799,7 +1809,7 @@ describe('node-fetch', () => { }); }); - it('should support fetch with Node.js URL object', function() { + it('should support fetch with Node.js URL object', () => { const url = `${base}hello`; const urlObj = parseURL(url); const req = new Request(urlObj); @@ -1810,7 +1820,7 @@ describe('node-fetch', () => { }); }); - it('should support fetch with WHATWG URL object', function() { + it('should support fetch with WHATWG URL object', () => { const url = `${base}hello`; const urlObj = new URL(url); const req = new Request(urlObj); @@ -1821,8 +1831,8 @@ describe('node-fetch', () => { }); }); - it('should support reading blob as text', function() { - return new Response(`hello`) + it('should support reading blob as text', () => { + return new Response('hello') .blob() .then(blob => blob.text()) .then(body => { @@ -1830,8 +1840,8 @@ describe('node-fetch', () => { }); }); - it('should support reading blob as arrayBuffer', function() { - return new Response(`hello`) + it('should support reading blob as arrayBuffer', () => { + return new Response('hello') .blob() .then(blob => blob.arrayBuffer()) .then(ab => { @@ -1840,8 +1850,8 @@ describe('node-fetch', () => { }); }); - it('should support reading blob as stream', function() { - return new Response(`hello`) + it('should support reading blob as stream', () => { + return new Response('hello') .blob() .then(blob => streamToPromise(blob.stream(), data => { const str = data.toString(); @@ -1849,10 +1859,10 @@ describe('node-fetch', () => { })); }); - it('should support blob round-trip', function() { + it('should support blob round-trip', () => { const url = `${base}hello`; - let length, type; + let length; let type; return fetch(url).then(res => res.blob()).then(blob => { const url = `${base}inspect`; @@ -1862,14 +1872,14 @@ describe('node-fetch', () => { method: 'POST', body: blob }); - }).then(res => res.json()).then(({body, headers}) => { + }).then(res => res.json()).then(({ body, headers }) => { expect(body).to.equal('world'); expect(headers['content-type']).to.equal(type); expect(headers['content-length']).to.equal(String(length)); }); }); - it('should support overwrite Request instance', function() { + it('should support overwrite Request instance', () => { const url = `${base}inspect`; const req = new Request(url, { method: 'POST', @@ -1890,7 +1900,7 @@ describe('node-fetch', () => { }); }); - it('should support arrayBuffer(), blob(), text(), json() and buffer() method in Body constructor', function() { + it('should support arrayBuffer(), blob(), text(), json() and buffer() method in Body constructor', () => { const body = new Body('a=1'); expect(body).to.have.property('arrayBuffer'); expect(body).to.have.property('blob'); @@ -1911,11 +1921,11 @@ describe('node-fetch', () => { expect(err.type).to.equal('test-error'); expect(err.code).to.equal('ESOMEERROR'); expect(err.errno).to.equal('ESOMEERROR'); - // reading the stack is quite slow (~30-50ms) + // Reading the stack is quite slow (~30-50ms) expect(err.stack).to.include('funcName').and.to.startWith(`${err.name}: ${err.message}`); }); - it('should support https request', function() { + it('should support https request', function () { this.timeout(5000); const url = 'https://github.com/'; const opts = { @@ -1927,14 +1937,16 @@ describe('node-fetch', () => { }); }); - // issue #414 - it('should reject if attempt to accumulate body stream throws', function () { + // Issue #414 + it('should reject if attempt to accumulate body stream throws', () => { let body = resumer().queue('a=1').end(); body = body.pipe(new stream.PassThrough()); const res = new Response(body); const bufferConcat = Buffer.concat; const restoreBufferConcat = () => Buffer.concat = bufferConcat; - Buffer.concat = () => { throw new Error('embedded error'); }; + Buffer.concat = () => { + throw new Error('embedded error'); + }; const textPromise = res.text(); // Ensure that `Buffer.concat` is always restored: @@ -1947,27 +1959,29 @@ describe('node-fetch', () => { .and.that.includes('embedded error'); }); - it("supports supplying a lookup function to the agent", function() { + it('supports supplying a lookup function to the agent', () => { const url = `${base}redirect/301`; let called = 0; function lookupSpy(hostname, options, callback) { called++; return lookup(hostname, options, callback); } + const agent = http.Agent({ lookup: lookupSpy }); return fetch(url, { agent }).then(() => { expect(called).to.equal(2); }); }); - it("supports supplying a famliy option to the agent", function() { + it('supports supplying a famliy option to the agent', () => { const url = `${base}redirect/301`; const families = []; const family = Symbol('family'); function lookupSpy(hostname, options, callback) { - families.push(options.family) + families.push(options.family); return lookup(hostname, {}, callback); } + const agent = http.Agent({ lookup: lookupSpy, family }); return fetch(url, { agent }).then(() => { expect(families).to.have.length(2); @@ -1976,7 +1990,7 @@ describe('node-fetch', () => { }); }); - it('should allow a function supplying the agent', function() { + it('should allow a function supplying the agent', () => { const url = `${base}inspect`; const agent = new http.Agent({ @@ -1986,21 +2000,21 @@ describe('node-fetch', () => { let parsedURL; return fetch(url, { - agent: function(_parsedURL) { + agent(_parsedURL) { parsedURL = _parsedURL; return agent; } }).then(res => { return res.json(); }).then(res => { - // the agent provider should have been called + // The agent provider should have been called expect(parsedURL.protocol).to.equal('http:'); - // the agent we returned should have been used - expect(res.headers['connection']).to.equal('keep-alive'); + // The agent we returned should have been used + expect(res.headers.connection).to.equal('keep-alive'); }); }); - it('should calculate content length and extract content type for each body type', function () { + it('should calculate content length and extract content type for each body type', () => { const url = `${base}hello`; const bodyContent = 'a=1'; @@ -2012,14 +2026,14 @@ describe('node-fetch', () => { size: 1024 }); - let blobBody = new Blob([bodyContent], { type: 'text/plain' }); + const blobBody = new Blob([bodyContent], { type: 'text/plain' }); const blobRequest = new Request(url, { method: 'POST', body: blobBody, size: 1024 }); - let formBody = new FormData(); + const formBody = new FormData(); formBody.append('a', '1'); const formRequest = new Request(url, { method: 'POST', @@ -2027,7 +2041,7 @@ describe('node-fetch', () => { size: 1024 }); - let bufferBody = Buffer.from(bodyContent); + const bufferBody = Buffer.from(bodyContent); const bufferRequest = new Request(url, { method: 'POST', body: bufferBody, @@ -2062,23 +2076,31 @@ describe('node-fetch', () => { }); }); -describe('Headers', function () { - it('should have attributes conforming to Web IDL', function () { +describe('Headers', () => { + it('should have attributes conforming to Web IDL', () => { const headers = new Headers(); expect(Object.getOwnPropertyNames(headers)).to.be.empty; const enumerableProperties = []; for (const property in headers) { enumerableProperties.push(property); } + for (const toCheck of [ - 'append', 'delete', 'entries', 'forEach', 'get', 'has', 'keys', 'set', + 'append', + 'delete', + 'entries', + 'forEach', + 'get', + 'has', + 'keys', + 'set', 'values' ]) { expect(enumerableProperties).to.contain(toCheck); } }); - it('should allow iterating through all headers with forEach', function() { + it('should allow iterating through all headers with forEach', () => { const headers = new Headers([ ['b', '2'], ['c', '4'], @@ -2093,13 +2115,13 @@ describe('Headers', function () { }); expect(result).to.deep.equal([ - ["a", "1"], - ["b", "2, 3"], - ["c", "4"] + ['a', '1'], + ['b', '2, 3'], + ['c', '4'] ]); }); - it('should allow iterating through all headers with for-of loop', function() { + it('should allow iterating through all headers with for-of loop', () => { const headers = new Headers([ ['b', '2'], ['c', '4'], @@ -2109,9 +2131,10 @@ describe('Headers', function () { expect(headers).to.be.iterable; const result = []; - for (let pair of headers) { + for (const pair of headers) { result.push(pair); } + expect(result).to.deep.equal([ ['a', '1'], ['b', '2, 3'], @@ -2119,7 +2142,7 @@ describe('Headers', function () { ]); }); - it('should allow iterating through all headers with entries()', function() { + it('should allow iterating through all headers with entries()', () => { const headers = new Headers([ ['b', '2'], ['c', '4'], @@ -2135,7 +2158,7 @@ describe('Headers', function () { ]); }); - it('should allow iterating through all headers with keys()', function() { + it('should allow iterating through all headers with keys()', () => { const headers = new Headers([ ['b', '2'], ['c', '4'], @@ -2147,7 +2170,7 @@ describe('Headers', function () { .and.to.iterate.over(['a', 'b', 'c']); }); - it('should allow iterating through all headers with values()', function() { + it('should allow iterating through all headers with values()', () => { const headers = new Headers([ ['b', '2'], ['c', '4'], @@ -2159,37 +2182,37 @@ describe('Headers', function () { .and.to.iterate.over(['1', '2, 3', '4']); }); - it('should reject illegal header', function() { + it('should reject illegal header', () => { const headers = new Headers(); expect(() => new Headers({ 'He y': 'ok' })).to.throw(TypeError); expect(() => new Headers({ 'Hé-y': 'ok' })).to.throw(TypeError); expect(() => new Headers({ 'He-y': 'ăk' })).to.throw(TypeError); - expect(() => headers.append('Hé-y', 'ok')) .to.throw(TypeError); - expect(() => headers.delete('Hé-y')) .to.throw(TypeError); - expect(() => headers.get('Hé-y')) .to.throw(TypeError); - expect(() => headers.has('Hé-y')) .to.throw(TypeError); - expect(() => headers.set('Hé-y', 'ok')) .to.throw(TypeError); - // should reject empty header - expect(() => headers.append('', 'ok')) .to.throw(TypeError); + expect(() => headers.append('Hé-y', 'ok')).to.throw(TypeError); + expect(() => headers.delete('Hé-y')).to.throw(TypeError); + expect(() => headers.get('Hé-y')).to.throw(TypeError); + expect(() => headers.has('Hé-y')).to.throw(TypeError); + expect(() => headers.set('Hé-y', 'ok')).to.throw(TypeError); + // Should reject empty header + expect(() => headers.append('', 'ok')).to.throw(TypeError); // 'o k' is valid value but invalid name new Headers({ 'He-y': 'o k' }); }); - it('should ignore unsupported attributes while reading headers', function() { + it('should ignore unsupported attributes while reading headers', () => { const FakeHeader = function () {}; - // prototypes are currently ignored + // Prototypes are currently ignored // This might change in the future: #181 FakeHeader.prototype.z = 'fake'; - const res = new FakeHeader; + const res = new FakeHeader(); res.a = 'string'; - res.b = ['1','2']; + res.b = ['1', '2']; res.c = ''; res.d = []; res.e = 1; res.f = [1, 2]; - res.g = { a:1 }; + res.g = { a: 1 }; res.h = undefined; res.i = null; res.j = NaN; @@ -2199,30 +2222,30 @@ describe('Headers', function () { const h1 = new Headers(res); h1.set('n', [1, 2]); - h1.append('n', ['3', 4]) + h1.append('n', ['3', 4]); const h1Raw = h1.raw(); - expect(h1Raw['a']).to.include('string'); - expect(h1Raw['b']).to.include('1,2'); - expect(h1Raw['c']).to.include(''); - expect(h1Raw['d']).to.include(''); - expect(h1Raw['e']).to.include('1'); - expect(h1Raw['f']).to.include('1,2'); - expect(h1Raw['g']).to.include('[object Object]'); - expect(h1Raw['h']).to.include('undefined'); - expect(h1Raw['i']).to.include('null'); - expect(h1Raw['j']).to.include('NaN'); - expect(h1Raw['k']).to.include('true'); - expect(h1Raw['l']).to.include('false'); - expect(h1Raw['m']).to.include('test'); - expect(h1Raw['n']).to.include('1,2'); - expect(h1Raw['n']).to.include('3,4'); - - expect(h1Raw['z']).to.be.undefined; - }); - - it('should wrap headers', function() { + expect(h1Raw.a).to.include('string'); + expect(h1Raw.b).to.include('1,2'); + expect(h1Raw.c).to.include(''); + expect(h1Raw.d).to.include(''); + expect(h1Raw.e).to.include('1'); + expect(h1Raw.f).to.include('1,2'); + expect(h1Raw.g).to.include('[object Object]'); + expect(h1Raw.h).to.include('undefined'); + expect(h1Raw.i).to.include('null'); + expect(h1Raw.j).to.include('NaN'); + expect(h1Raw.k).to.include('true'); + expect(h1Raw.l).to.include('false'); + expect(h1Raw.m).to.include('test'); + expect(h1Raw.n).to.include('1,2'); + expect(h1Raw.n).to.include('3,4'); + + expect(h1Raw.z).to.be.undefined; + }); + + it('should wrap headers', () => { const h1 = new Headers({ a: '1' }); @@ -2236,19 +2259,19 @@ describe('Headers', function () { h3.append('a', '2'); const h3Raw = h3.raw(); - expect(h1Raw['a']).to.include('1'); - expect(h1Raw['a']).to.not.include('2'); + expect(h1Raw.a).to.include('1'); + expect(h1Raw.a).to.not.include('2'); - expect(h2Raw['a']).to.include('1'); - expect(h2Raw['a']).to.not.include('2'); - expect(h2Raw['b']).to.include('1'); + expect(h2Raw.a).to.include('1'); + expect(h2Raw.a).to.not.include('2'); + expect(h2Raw.b).to.include('1'); - expect(h3Raw['a']).to.include('1'); - expect(h3Raw['a']).to.include('2'); - expect(h3Raw['b']).to.include('1'); + expect(h3Raw.a).to.include('1'); + expect(h3Raw.a).to.include('2'); + expect(h3Raw.b).to.include('1'); }); - it('should accept headers as an iterable of tuples', function() { + it('should accept headers as an iterable of tuples', () => { let headers; headers = new Headers([ @@ -2275,29 +2298,48 @@ describe('Headers', function () { expect(headers.get('b')).to.equal('2'); }); - it('should throw a TypeError if non-tuple exists in a headers initializer', function() { - expect(() => new Headers([ ['b', '2', 'huh?'] ])).to.throw(TypeError); - expect(() => new Headers([ 'b2' ])).to.throw(TypeError); + it('should throw a TypeError if non-tuple exists in a headers initializer', () => { + expect(() => new Headers([['b', '2', 'huh?']])).to.throw(TypeError); + expect(() => new Headers(['b2'])).to.throw(TypeError); expect(() => new Headers('b2')).to.throw(TypeError); expect(() => new Headers({ [Symbol.iterator]: 42 })).to.throw(TypeError); }); }); -describe('Response', function () { - it('should have attributes conforming to Web IDL', function () { +describe('Response', () => { + it('should have attributes conforming to Web IDL', () => { const res = new Response(); const enumerableProperties = []; for (const property in res) { enumerableProperties.push(property); } + for (const toCheck of [ - 'body', 'bodyUsed', 'arrayBuffer', 'blob', 'json', 'text', - 'url', 'status', 'ok', 'redirected', 'statusText', 'headers', 'clone' + 'body', + 'bodyUsed', + 'arrayBuffer', + 'blob', + 'json', + 'text', + 'url', + 'status', + 'ok', + 'redirected', + 'statusText', + 'headers', + 'clone' ]) { expect(enumerableProperties).to.contain(toCheck); } + for (const toCheck of [ - 'body', 'bodyUsed', 'url', 'status', 'ok', 'redirected', 'statusText', + 'body', + 'bodyUsed', + 'url', + 'status', + 'ok', + 'redirected', + 'statusText', 'headers' ]) { expect(() => { @@ -2306,7 +2348,7 @@ describe('Response', function () { } }); - it('should support empty options', function() { + it('should support empty options', () => { let body = resumer().queue('a=1').end(); body = body.pipe(new stream.PassThrough()); const res = new Response(body); @@ -2315,7 +2357,7 @@ describe('Response', function () { }); }); - it('should support parsing headers', function() { + it('should support parsing headers', () => { const res = new Response(null, { headers: { a: '1' @@ -2324,42 +2366,42 @@ describe('Response', function () { expect(res.headers.get('a')).to.equal('1'); }); - it('should support text() method', function() { + it('should support text() method', () => { const res = new Response('a=1'); return res.text().then(result => { expect(result).to.equal('a=1'); }); }); - it('should support json() method', function() { + it('should support json() method', () => { const res = new Response('{"a":1}'); return res.json().then(result => { expect(result.a).to.equal(1); }); }); - it('should support buffer() method', function() { + it('should support buffer() method', () => { const res = new Response('a=1'); return res.buffer().then(result => { expect(result.toString()).to.equal('a=1'); }); }); - it('should support blob() method', function() { + it('should support blob() method', () => { const res = new Response('a=1', { method: 'POST', headers: { 'Content-Type': 'text/plain' } }); - return res.blob().then(function(result) { + return res.blob().then(result => { expect(result).to.be.an.instanceOf(Blob); expect(result.size).to.equal(3); expect(result.type).to.equal('text/plain'); }); }); - it('should support clone() method', function() { + it('should support clone() method', () => { let body = resumer().queue('a=1').end(); body = body.pipe(new stream.PassThrough()); const res = new Response(body, { @@ -2376,14 +2418,14 @@ describe('Response', function () { expect(cl.status).to.equal(346); expect(cl.statusText).to.equal('production'); expect(cl.ok).to.be.false; - // clone body shouldn't be the same body + // Clone body shouldn't be the same body expect(cl.body).to.not.equal(body); return cl.text().then(result => { expect(result).to.equal('a=1'); }); }); - it('should support stream as body', function() { + it('should support stream as body', () => { let body = resumer().queue('a=1').end(); body = body.pipe(new stream.PassThrough()); const res = new Response(body); @@ -2392,81 +2434,93 @@ describe('Response', function () { }); }); - it('should support string as body', function() { + it('should support string as body', () => { const res = new Response('a=1'); return res.text().then(result => { expect(result).to.equal('a=1'); }); }); - it('should support buffer as body', function() { + it('should support buffer as body', () => { const res = new Response(Buffer.from('a=1')); return res.text().then(result => { expect(result).to.equal('a=1'); }); }); - it('should support ArrayBuffer as body', function() { + it('should support ArrayBuffer as body', () => { const res = new Response(stringToArrayBuffer('a=1')); return res.text().then(result => { expect(result).to.equal('a=1'); }); }); - it('should support blob as body', function() { + it('should support blob as body', () => { const res = new Response(new Blob(['a=1'])); return res.text().then(result => { expect(result).to.equal('a=1'); }); }); - it('should support Uint8Array as body', function() { + it('should support Uint8Array as body', () => { const res = new Response(new Uint8Array(stringToArrayBuffer('a=1'))); return res.text().then(result => { expect(result).to.equal('a=1'); }); }); - it('should support DataView as body', function() { + it('should support DataView as body', () => { const res = new Response(new DataView(stringToArrayBuffer('a=1'))); return res.text().then(result => { expect(result).to.equal('a=1'); }); }); - it('should default to null as body', function() { + it('should default to null as body', () => { const res = new Response(); expect(res.body).to.equal(null); return res.text().then(result => expect(result).to.equal('')); }); - it('should default to 200 as status code', function() { + it('should default to 200 as status code', () => { const res = new Response(null); expect(res.status).to.equal(200); }); - it('should default to empty string as url', function() { + it('should default to empty string as url', () => { const res = new Response(); expect(res.url).to.equal(''); }); }); -describe('Request', function () { - it('should have attributes conforming to Web IDL', function () { +describe('Request', () => { + it('should have attributes conforming to Web IDL', () => { const req = new Request('https://github.com/'); const enumerableProperties = []; for (const property in req) { enumerableProperties.push(property); } + for (const toCheck of [ - 'body', 'bodyUsed', 'arrayBuffer', 'blob', 'json', 'text', - 'method', 'url', 'headers', 'redirect', 'clone', 'signal', + 'body', + 'bodyUsed', + 'arrayBuffer', + 'blob', + 'json', + 'text', + 'method', + 'url', + 'headers', + 'redirect', + 'clone', + 'signal' ]) { expect(enumerableProperties).to.contain(toCheck); } + for (const toCheck of [ - 'body', 'bodyUsed', 'method', 'url', 'headers', 'redirect', 'signal', + 'body', 'bodyUsed', 'method', 'url', 'headers', 'redirect', 'signal' ]) { expect(() => { req[toCheck] = 'abc'; @@ -2474,7 +2528,7 @@ describe('Request', function () { } }); - it('should support wrapping Request instance', function() { + it('should support wrapping Request instance', () => { const url = `${base}hello`; const form = new FormData(); @@ -2485,7 +2539,7 @@ describe('Request', function () { method: 'POST', follow: 1, body: form, - signal, + signal }); const r2 = new Request(r1, { follow: 2 @@ -2494,7 +2548,7 @@ describe('Request', function () { expect(r2.url).to.equal(url); expect(r2.method).to.equal('POST'); expect(r2.signal).to.equal(signal); - // note that we didn't clone the body + // Note that we didn't clone the body expect(r2.body).to.equal(form); expect(r1.follow).to.equal(1); expect(r2.follow).to.equal(2); @@ -2502,10 +2556,10 @@ describe('Request', function () { expect(r2.counter).to.equal(0); }); - it('should override signal on derived Request instances', function() { + it('should override signal on derived Request instances', () => { const parentAbortController = new AbortController(); const derivedAbortController = new AbortController(); - const parentRequest = new Request(`test`, { + const parentRequest = new Request('test', { signal: parentAbortController.signal }); const derivedRequest = new Request(parentRequest, { @@ -2515,9 +2569,9 @@ describe('Request', function () { expect(derivedRequest.signal).to.equal(derivedAbortController.signal); }); - it('should allow removing signal on derived Request instances', function() { + it('should allow removing signal on derived Request instances', () => { const parentAbortController = new AbortController(); - const parentRequest = new Request(`test`, { + const parentRequest = new Request('test', { signal: parentAbortController.signal }); const derivedRequest = new Request(parentRequest, { @@ -2527,7 +2581,7 @@ describe('Request', function () { expect(derivedRequest.signal).to.equal(null); }); - it('should throw error with GET/HEAD requests with body', function() { + it('should throw error with GET/HEAD requests with body', () => { expect(() => new Request('.', { body: '' })) .to.throw(TypeError); expect(() => new Request('.', { body: 'a' })) @@ -2542,13 +2596,13 @@ describe('Request', function () { .to.throw(TypeError); }); - it('should default to null as body', function() { + it('should default to null as body', () => { const req = new Request('.'); expect(req.body).to.equal(null); return req.text().then(result => expect(result).to.equal('')); }); - it('should support parsing headers', function() { + it('should support parsing headers', () => { const url = base; const req = new Request(url, { headers: { @@ -2559,21 +2613,21 @@ describe('Request', function () { expect(req.headers.get('a')).to.equal('1'); }); - it('should support arrayBuffer() method', function() { + it('should support arrayBuffer() method', () => { const url = base; - var req = new Request(url, { + const req = new Request(url, { method: 'POST', body: 'a=1' }); expect(req.url).to.equal(url); - return req.arrayBuffer().then(function(result) { + return req.arrayBuffer().then(result => { expect(result).to.be.an.instanceOf(ArrayBuffer); const str = String.fromCharCode.apply(null, new Uint8Array(result)); expect(str).to.equal('a=1'); }); }); - it('should support text() method', function() { + it('should support text() method', () => { const url = base; const req = new Request(url, { method: 'POST', @@ -2585,7 +2639,7 @@ describe('Request', function () { }); }); - it('should support json() method', function() { + it('should support json() method', () => { const url = base; const req = new Request(url, { method: 'POST', @@ -2597,7 +2651,7 @@ describe('Request', function () { }); }); - it('should support buffer() method', function() { + it('should support buffer() method', () => { const url = base; const req = new Request(url, { method: 'POST', @@ -2609,27 +2663,27 @@ describe('Request', function () { }); }); - it('should support blob() method', function() { + it('should support blob() method', () => { const url = base; - var req = new Request(url, { + const req = new Request(url, { method: 'POST', body: Buffer.from('a=1') }); expect(req.url).to.equal(url); - return req.blob().then(function(result) { + return req.blob().then(result => { expect(result).to.be.an.instanceOf(Blob); expect(result.size).to.equal(3); expect(result.type).to.equal(''); }); }); - it('should support arbitrary url', function() { + it('should support arbitrary url', () => { const url = 'anything'; const req = new Request(url); expect(req.url).to.equal('anything'); }); - it('should support clone() method', function() { + it('should support clone() method', () => { const url = base; let body = resumer().queue('a=1').end(); body = body.pipe(new stream.PassThrough()); @@ -2645,7 +2699,7 @@ describe('Request', function () { follow: 3, compress: false, agent, - signal, + signal }); const cl = req.clone(); expect(cl.url).to.equal(url); @@ -2658,7 +2712,7 @@ describe('Request', function () { expect(cl.counter).to.equal(0); expect(cl.agent).to.equal(agent); expect(cl.signal).to.equal(signal); - // clone body shouldn't be the same body + // Clone body shouldn't be the same body expect(cl.body).to.not.equal(body); return Promise.all([cl.text(), req.text()]).then(results => { expect(results[0]).to.equal('a=1'); @@ -2666,7 +2720,7 @@ describe('Request', function () { }); }); - it('should support ArrayBuffer as body', function() { + it('should support ArrayBuffer as body', () => { const req = new Request('', { method: 'POST', body: stringToArrayBuffer('a=1') @@ -2676,7 +2730,7 @@ describe('Request', function () { }); }); - it('should support Uint8Array as body', function() { + it('should support Uint8Array as body', () => { const req = new Request('', { method: 'POST', body: new Uint8Array(stringToArrayBuffer('a=1')) @@ -2686,7 +2740,7 @@ describe('Request', function () { }); }); - it('should support DataView as body', function() { + it('should support DataView as body', () => { const req = new Request('', { method: 'POST', body: new DataView(stringToArrayBuffer('a=1')) @@ -2712,22 +2766,24 @@ function streamToPromise(stream, dataHandler) { describe('external encoding', () => { const hasEncoding = typeof convert === 'function'; - describe('with optional `encoding`', function() { - before(function() { - if(!hasEncoding) this.skip(); + describe('with optional `encoding`', () => { + before(function () { + if (!hasEncoding) { + this.skip(); + } }); - it('should only use UTF-8 decoding with text()', function() { + it('should only use UTF-8 decoding with text()', () => { const url = `${base}encoding/euc-jp`; return fetch(url).then(res => { expect(res.status).to.equal(200); return res.text().then(result => { - expect(result).to.equal('\ufffd\ufffd\ufffd\u0738\ufffd'); + expect(result).to.equal('\uFFFD\uFFFD\uFFFD\u0738\ufffd'); }); }); }); - it('should support encoding decode, xml dtd detect', function() { + it('should support encoding decode, xml dtd detect', () => { const url = `${base}encoding/euc-jp`; return fetch(url).then(res => { expect(res.status).to.equal(200); @@ -2737,7 +2793,7 @@ describe('external encoding', () => { }); }); - it('should support encoding decode, content-type detect', function() { + it('should support encoding decode, content-type detect', () => { const url = `${base}encoding/shift-jis`; return fetch(url).then(res => { expect(res.status).to.equal(200); @@ -2747,7 +2803,7 @@ describe('external encoding', () => { }); }); - it('should support encoding decode, html5 detect', function() { + it('should support encoding decode, html5 detect', () => { const url = `${base}encoding/gbk`; return fetch(url).then(res => { expect(res.status).to.equal(200); @@ -2757,7 +2813,7 @@ describe('external encoding', () => { }); }); - it('should support encoding decode, html4 detect', function() { + it('should support encoding decode, html4 detect', () => { const url = `${base}encoding/gb2312`; return fetch(url).then(res => { expect(res.status).to.equal(200); @@ -2767,7 +2823,7 @@ describe('external encoding', () => { }); }); - it('should default to utf8 encoding', function() { + it('should default to utf8 encoding', () => { const url = `${base}encoding/utf8`; return fetch(url).then(res => { expect(res.status).to.equal(200); @@ -2778,7 +2834,7 @@ describe('external encoding', () => { }); }); - it('should support uncommon content-type order, charset in front', function() { + it('should support uncommon content-type order, charset in front', () => { const url = `${base}encoding/order1`; return fetch(url).then(res => { expect(res.status).to.equal(200); @@ -2788,7 +2844,7 @@ describe('external encoding', () => { }); }); - it('should support uncommon content-type order, end with qs', function() { + it('should support uncommon content-type order, end with qs', () => { const url = `${base}encoding/order2`; return fetch(url).then(res => { expect(res.status).to.equal(200); @@ -2798,7 +2854,7 @@ describe('external encoding', () => { }); }); - it('should support chunked encoding, html4 detect', function() { + it('should support chunked encoding, html4 detect', () => { const url = `${base}encoding/chunked`; return fetch(url).then(res => { expect(res.status).to.equal(200); @@ -2809,7 +2865,7 @@ describe('external encoding', () => { }); }); - it('should only do encoding detection up to 1024 bytes', function() { + it('should only do encoding detection up to 1024 bytes', () => { const url = `${base}encoding/invalid`; return fetch(url).then(res => { expect(res.status).to.equal(200); @@ -2821,16 +2877,18 @@ describe('external encoding', () => { }); }); - describe('without optional `encoding`', function() { - before(function() { - if (hasEncoding) this.skip() + describe('without optional `encoding`', () => { + before(function () { + if (hasEncoding) { + this.skip(); + } }); it('should throw a FetchError if res.textConverted() is called without `encoding` in require cache', () => { const url = `${base}hello`; - return fetch(url).then((res) => { + return fetch(url).then(res => { return expect(res.textConverted()).to.eventually.be.rejected - .and.have.property('message').which.includes('encoding') + .and.have.property('message').which.includes('encoding'); }); }); }); From 8607f961618b05c47ac3c6f1aca5a25c09a4de8f Mon Sep 17 00:00:00 2001 From: Richie Bendall Date: Sun, 8 Sep 2019 06:49:08 +1200 Subject: [PATCH 007/157] fix: Fix tests --- package.json | 3 ++- src/blob.js | 10 +++++----- src/headers.js | 2 +- 3 files changed, 8 insertions(+), 7 deletions(-) diff --git a/package.json b/package.json index 874f23dfa..1e1a2a830 100644 --- a/package.json +++ b/package.json @@ -75,7 +75,8 @@ ], "valid-jsdoc": 0, "no-multi-assign": 0, - "complexity": 0 + "complexity": 0, + "unicorn/prefer-spread": 0 } } } diff --git a/src/blob.js b/src/blob.js index 643bc18e5..13235c802 100644 --- a/src/blob.js +++ b/src/blob.js @@ -69,11 +69,11 @@ export default class Blob { } stream() { - const readable = new Readable(); - readable._read = () => {}; - readable.push(this[BUFFER]); - readable.push(null); - return readable; + const readable = new Readable(); + readable._read = () => {}; + readable.push(this[BUFFER]); + readable.push(null); + return readable; } toString() { diff --git a/src/headers.js b/src/headers.js index a90f4a473..cb6453ce6 100644 --- a/src/headers.js +++ b/src/headers.js @@ -84,7 +84,7 @@ export default class Headers { throw new TypeError('Each header pair must be iterable'); } - pairs.push([...pair]); + pairs.push(Array.from(pair)); } for (const pair of pairs) { From 155ef268ec59e0e18b626a13cc3151e2111cc4e4 Mon Sep 17 00:00:00 2001 From: Richie Bendall Date: Sun, 8 Sep 2019 07:10:48 +1200 Subject: [PATCH 008/157] chore!: Drop support for nodejs 4 and 6 --- .travis.yml | 22 +++++++++------------- package.json | 42 +++++++++++++++++++++--------------------- 2 files changed, 30 insertions(+), 34 deletions(-) diff --git a/.travis.yml b/.travis.yml index 3bb109e15..a41705b68 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,20 +1,16 @@ language: node_js + node_js: - - "4" - - "6" - "8" - - "10" - - "node" -env: - - FORMDATA_VERSION=1.0.0 - - FORMDATA_VERSION=2.1.0 -before_script: - - 'if [ "$FORMDATA_VERSION" ]; then npm install form-data@^$FORMDATA_VERSION; fi' + - "lts/*" # 10 + - "node" # 12 + +cache: + - npm + directories: + - node_modules + script: - - npm uninstall encoding - npm run coverage - npm install encoding - npm run coverage -cache: - directories: - - node_modules diff --git a/package.json b/package.json index 1e1a2a830..d7cc06e50 100644 --- a/package.json +++ b/package.json @@ -14,7 +14,7 @@ "types/*.d.ts" ], "engines": { - "node": "4.x || >=6.0.0" + "node": ">=8.0.0" }, "scripts": { "build": "cross-env BABEL_ENV=rollup rollup -c", @@ -40,43 +40,43 @@ }, "homepage": "https://github.com/bitinn/node-fetch", "devDependencies": { + "@babel/core": "^7.6.0", "@ungap/url-search-params": "^0.1.2", - "abort-controller": "^1.1.0", + "abort-controller": "^3.0.0", "abortcontroller-polyfill": "^1.3.0", - "babel-core": "^6.26.3", - "babel-plugin-istanbul": "^4.1.6", + "babel-plugin-istanbul": "^5.2.0", "babel-preset-env": "^1.6.1", "babel-register": "^6.16.3", - "chai": "^3.5.0", + "chai": "^4.2.0", "chai-as-promised": "^7.1.1", - "chai-iterator": "^1.1.1", - "chai-string": "~1.3.0", + "chai-iterator": "^3.0.2", + "chai-string": "^1.5.0", "codecov": "^3.3.0", "cross-env": "^5.2.0", "form-data": "^2.3.3", - "is-builtin-module": "^1.0.0", - "mocha": "^5.0.0", - "nyc": "11.9.0", + "is-builtin-module": "^3.0.0", + "mocha": "^6.2.0", + "nyc": "^14.1.1", "parted": "^0.1.1", "promise": "^8.0.3", "resumer": "0.0.0", - "rollup": "^0.63.4", - "rollup-plugin-babel": "^3.0.7", + "rollup": "^1.20.3", + "rollup-plugin-babel": "^4.3.3", "string-to-arraybuffer": "^1.0.2", - "whatwg-url": "^5.0.0", + "whatwg-url": "^7.0.0", "xo": "^0.24.0" }, "dependencies": {}, "xo": { "rules": { - "object-curly-spacing": [ - "error", - "always" - ], - "valid-jsdoc": 0, - "no-multi-assign": 0, - "complexity": 0, - "unicorn/prefer-spread": 0 + "object-curly-spacing": [ + "error", + "always" + ], + "valid-jsdoc": 0, + "no-multi-assign": 0, + "complexity": 0, + "unicorn/prefer-spread": 0 } } } From 939b3bc5693dd235074214ce15cd11d17c29ec1c Mon Sep 17 00:00:00 2001 From: Richie Bendall Date: Sun, 8 Sep 2019 07:33:20 +1200 Subject: [PATCH 009/157] chore: Fix Travis CI yml --- .travis.yml | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/.travis.yml b/.travis.yml index a41705b68..a2f43ff5c 100644 --- a/.travis.yml +++ b/.travis.yml @@ -5,10 +5,7 @@ node_js: - "lts/*" # 10 - "node" # 12 -cache: - - npm - directories: - - node_modules +cache: npm script: - npm run coverage From dfc7d247af51af81e3c8922fac214e054fccf928 Mon Sep 17 00:00:00 2001 From: Antoni Kepinski Date: Sat, 7 Sep 2019 22:51:11 +0200 Subject: [PATCH 010/157] Use old Babel (needs migration) --- package.json | 26 +++++++++++++++++++------- 1 file changed, 19 insertions(+), 7 deletions(-) diff --git a/package.json b/package.json index d7cc06e50..21a2e9266 100644 --- a/package.json +++ b/package.json @@ -40,10 +40,10 @@ }, "homepage": "https://github.com/bitinn/node-fetch", "devDependencies": { - "@babel/core": "^7.6.0", "@ungap/url-search-params": "^0.1.2", "abort-controller": "^3.0.0", "abortcontroller-polyfill": "^1.3.0", + "babel-core": "^6.26.3", "babel-plugin-istanbul": "^5.2.0", "babel-preset-env": "^1.6.1", "babel-register": "^6.16.3", @@ -68,15 +68,27 @@ }, "dependencies": {}, "xo": { + "envs": [ + "node", + "browser", + "mocha" + ], "rules": { - "object-curly-spacing": [ - "error", - "always" - ], "valid-jsdoc": 0, "no-multi-assign": 0, "complexity": 0, - "unicorn/prefer-spread": 0 - } + "unicorn/prefer-spread": 0, + "prefer-object-spread": 0, + "promise/prefer-await-to-then": 0, + "no-mixed-operators": 0, + "eqeqeq": 0, + "no-eq-null": 0, + "no-negated-condition": 0, + "prefer-rest-params": 0 + }, + "ignores": [ + "lib", + "test/test.js" + ] } } From 684f781d582a58c65a8064fd00e03d8d7c0b15c6 Mon Sep 17 00:00:00 2001 From: Antoni Kepinski Date: Sat, 7 Sep 2019 22:52:00 +0200 Subject: [PATCH 011/157] chore: lint everything --- browser.js | 2 +- build/babel-plugin.js | 4 ++-- rollup.config.js | 6 +++--- src/blob.js | 17 ++++++++++------- src/body.js | 32 ++++++++++++++++++-------------- src/headers.js | 22 +++++++++++----------- src/index.js | 40 ++++++++++++++++++++++------------------ src/request.js | 32 ++++++++++++++++---------------- src/response.js | 20 ++++++++++---------- test/server.js | 6 +++--- test/test.js | 22 ++++++++++++---------- 11 files changed, 108 insertions(+), 95 deletions(-) diff --git a/browser.js b/browser.js index 47f686731..99bedd908 100644 --- a/browser.js +++ b/browser.js @@ -20,7 +20,7 @@ const getGlobal = function () { throw new Error('unable to locate global object'); }; -var global = getGlobal(); +const global = getGlobal(); module.exports = exports = global.fetch; diff --git a/build/babel-plugin.js b/build/babel-plugin.js index f13727b6b..0cd948fc7 100644 --- a/build/babel-plugin.js +++ b/build/babel-plugin.js @@ -2,7 +2,7 @@ const walked = Symbol('walked'); -module.exports = ({ types: t }) => ({ +module.exports = ({types: t}) => ({ visitor: { Program: { exit(program) { @@ -16,7 +16,7 @@ module.exports = ({ types: t }) => ({ if (expr.isAssignmentExpression() && expr.get('left').matchesPattern('exports.*')) { const prop = expr.get('left').get('property'); - if (prop.isIdentifier({ name: 'default' })) { + if (prop.isIdentifier({name: 'default'})) { program.unshiftContainer('body', [ t.expressionStatement( t.assignmentExpression('=', diff --git a/rollup.config.js b/rollup.config.js index 9beb2014a..8110831ba 100644 --- a/rollup.config.js +++ b/rollup.config.js @@ -7,9 +7,9 @@ process.env.BABEL_ENV = 'rollup'; export default { input: 'src/index.js', output: [ - { file: 'lib/index.js', format: 'cjs', exports: 'named' }, - { file: 'lib/index.es.js', format: 'es', exports: 'named', intro: 'process.emitWarning("The .es.js file is deprecated. Use .mjs instead.");' }, - { file: 'lib/index.mjs', format: 'es', exports: 'named' } + {file: 'lib/index.js', format: 'cjs', exports: 'named'}, + {file: 'lib/index.es.js', format: 'es', exports: 'named', intro: 'process.emitWarning("The .es.js file is deprecated. Use .mjs instead.");'}, + {file: 'lib/index.mjs', format: 'es', exports: 'named'} ], plugins: [ babel({ diff --git a/src/blob.js b/src/blob.js index 13235c802..ee296c82d 100644 --- a/src/blob.js +++ b/src/blob.js @@ -4,7 +4,7 @@ import Stream from 'stream'; // Fix for "Readable" isn't a named export issue -const { Readable } = Stream; +const {Readable} = Stream; export const BUFFER = Symbol('buffer'); const TYPE = Symbol('type'); @@ -17,6 +17,7 @@ export default class Blob { const options = arguments[1]; const buffers = []; + /* eslint-disable-next-line no-unused-vars */ let size = 0; if (blobParts) { @@ -81,11 +82,13 @@ export default class Blob { } slice() { - const { size } = this; + const {size} = this; const start = arguments[0]; const end = arguments[1]; - let relativeStart; let relativeEnd; + let relativeStart; + let relativeEnd; + if (start === undefined) { relativeStart = 0; } else if (start < 0) { @@ -109,16 +112,16 @@ export default class Blob { relativeStart, relativeStart + span ); - const blob = new Blob([], { type: arguments[2] }); + const blob = new Blob([], {type: arguments[2]}); blob[BUFFER] = slicedBuffer; return blob; } } Object.defineProperties(Blob.prototype, { - size: { enumerable: true }, - type: { enumerable: true }, - slice: { enumerable: true } + size: {enumerable: true}, + type: {enumerable: true}, + slice: {enumerable: true} }); Object.defineProperty(Blob.prototype, Symbol.toStringTag, { diff --git a/src/body.js b/src/body.js index 17e014452..f85b7c161 100644 --- a/src/body.js +++ b/src/body.js @@ -7,18 +7,19 @@ import Stream from 'stream'; -import Blob, { BUFFER } from './blob'; +import Blob, {BUFFER} from './blob'; import FetchError from './fetch-error'; let convert; try { + /* eslint-disable-next-line import/no-unresolved */ convert = require('encoding').convert; } catch (error) {} const INTERNALS = Symbol('Body internals'); // Fix an issue where "PassThrough" isn't a named export for node <10 -const { PassThrough } = Stream; +const {PassThrough} = Stream; /** * Body mixin @@ -157,12 +158,12 @@ Body.prototype = { // In browsers, all properties are enumerable. Object.defineProperties(Body.prototype, { - body: { enumerable: true }, - bodyUsed: { enumerable: true }, - arrayBuffer: { enumerable: true }, - blob: { enumerable: true }, - json: { enumerable: true }, - text: { enumerable: true } + body: {enumerable: true}, + bodyUsed: {enumerable: true}, + arrayBuffer: {enumerable: true}, + blob: {enumerable: true}, + json: {enumerable: true}, + text: {enumerable: true} }); Body.mixIn = function (proto) { @@ -193,7 +194,7 @@ function consumeBody() { return Body.Promise.reject(this[INTERNALS].error); } - let { body } = this; + let {body} = this; // Body is null if (body === null) { @@ -291,7 +292,8 @@ function convertBody(buffer, headers) { const ct = headers.get('content-type'); let charset = 'utf-8'; - let res; let str; + let res; + let str; // Header if (ct) { @@ -299,6 +301,7 @@ function convertBody(buffer, headers) { } // No charset in content type, peek at response body for at most 1024 bytes + /* eslint-disable-next-line prefer-const */ str = buffer.slice(0, 1024).toString(); // Html5 @@ -387,8 +390,9 @@ function isBlob(obj) { * @return Mixed */ export function clone(instance) { - let p1; let p2; - let { body } = instance; + let p1; + let p2; + let {body} = instance; // Don't allow cloning a used body if (instance.bodyUsed) { @@ -481,7 +485,7 @@ export function extractContentType(body) { * @return Number? Number of bytes, or null if not possible */ export function getTotalBytes(instance) { - const { body } = instance; + const {body} = instance; if (body === null) { // Body is null @@ -518,7 +522,7 @@ export function getTotalBytes(instance) { * @return Void */ export function writeToStream(dest, instance) { - const { body } = instance; + const {body} = instance; if (body === null) { // Body is null diff --git a/src/headers.js b/src/headers.js index cb6453ce6..5a4f5c953 100644 --- a/src/headers.js +++ b/src/headers.js @@ -5,7 +5,7 @@ * Headers class offers convenient helpers */ -const invalidTokenRegex = /[^\^_`a-zA-Z\-0-9!#$%&'*+.|~]/; +const invalidTokenRegex = /[^_`a-zA-Z\-0-9!#$%&'*+.|~]/; const invalidHeaderCharRegex = /[^\t\u0020-\u007e\u0080-\u00ff]/; function validateName(name) { @@ -252,15 +252,15 @@ Object.defineProperty(Headers.prototype, Symbol.toStringTag, { }); Object.defineProperties(Headers.prototype, { - get: { enumerable: true }, - forEach: { enumerable: true }, - set: { enumerable: true }, - append: { enumerable: true }, - has: { enumerable: true }, - delete: { enumerable: true }, - keys: { enumerable: true }, - values: { enumerable: true }, - entries: { enumerable: true } + get: {enumerable: true}, + forEach: {enumerable: true}, + set: {enumerable: true}, + append: {enumerable: true}, + has: {enumerable: true}, + delete: {enumerable: true}, + keys: {enumerable: true}, + values: {enumerable: true}, + entries: {enumerable: true} }); function getHeaders(headers, kind = 'key+value') { @@ -333,7 +333,7 @@ Object.defineProperty(HeadersIteratorPrototype, Symbol.toStringTag, { * @return Object */ export function exportNodeCompatibleHeaders(headers) { - const obj = Object.assign({ __proto__: null }, headers[MAP]); + const obj = Object.assign({__proto__: null}, headers[MAP]); // Http.request() only supports string as Host header. This hack makes // specifying custom Host header possible. diff --git a/src/index.js b/src/index.js index c17e48f4b..dab7387df 100644 --- a/src/index.js +++ b/src/index.js @@ -13,16 +13,16 @@ import https from 'https'; import zlib from 'zlib'; import Stream from 'stream'; -import Body, { writeToStream, getTotalBytes } from './body'; +import Body, {writeToStream, getTotalBytes} from './body'; import Response from './response'; -import Headers, { createHeadersLenient } from './headers'; -import Request, { getNodeRequestOptions } from './request'; +import Headers, {createHeadersLenient} from './headers'; +import Request, {getNodeRequestOptions} from './request'; import FetchError from './fetch-error'; import AbortError from './abort-error'; // Fix an issue where "PassThrough", "resolve" aren't a named export for node <10 -const { PassThrough } = Stream; -const resolve_url = Url.resolve; +const {PassThrough} = Stream; +const resolveUrl = Url.resolve; /** * Fetch function @@ -46,7 +46,7 @@ export default function fetch(url, opts) { const options = getNodeRequestOptions(request); const send = (options.protocol === 'https:' ? https : http).request; - const { signal } = request; + const {signal} = request; let response = null; const abort = () => { @@ -91,7 +91,7 @@ export default function fetch(url, opts) { } if (request.timeout) { - req.once('socket', socket => { + req.once('socket', () => { reqTimeout = setTimeout(() => { reject(new FetchError(`network timeout at: ${request.url}`, 'request-timeout')); finalize(); @@ -115,7 +115,7 @@ export default function fetch(url, opts) { const location = headers.get('Location'); // HTTP fetch step 5.3 - const locationURL = location === null ? null : resolve_url(request.url, location); + const locationURL = location === null ? null : resolveUrl(request.url, location); // HTTP fetch step 5.5 switch (request.redirect) { @@ -136,7 +136,7 @@ export default function fetch(url, opts) { } break; - case 'follow': + case 'follow': { // HTTP-redirect fetch step 2 if (locationURL === null) { break; @@ -181,6 +181,10 @@ export default function fetch(url, opts) { resolve(fetch(new Request(locationURL, requestOpts))); finalize(); return; + } + + default: + // Do nothing } } @@ -192,7 +196,7 @@ export default function fetch(url, opts) { }); let body = res.pipe(new PassThrough()); - const response_options = { + const responseOptions = { url: request.url, status: res.statusCode, statusText: res.statusMessage, @@ -214,7 +218,7 @@ export default function fetch(url, opts) { // 4. no content response (204) // 5. content not modified response (304) if (!request.compress || request.method === 'HEAD' || codings === null || res.statusCode === 204 || res.statusCode === 304) { - response = new Response(body, response_options); + response = new Response(body, responseOptions); resolve(response); return; } @@ -230,15 +234,15 @@ export default function fetch(url, opts) { }; // For gzip - if (codings == 'gzip' || codings == 'x-gzip') { + if (codings === 'gzip' || codings === 'x-gzip') { body = body.pipe(zlib.createGunzip(zlibOptions)); - response = new Response(body, response_options); + response = new Response(body, responseOptions); resolve(response); return; } // For deflate - if (codings == 'deflate' || codings == 'x-deflate') { + if (codings === 'deflate' || codings === 'x-deflate') { // Handle the infamous raw deflate response from old servers // a hack for old IIS and Apache servers const raw = res.pipe(new PassThrough()); @@ -250,22 +254,22 @@ export default function fetch(url, opts) { body = body.pipe(zlib.createInflateRaw()); } - response = new Response(body, response_options); + response = new Response(body, responseOptions); resolve(response); }); return; } // For br - if (codings == 'br' && typeof zlib.createBrotliDecompress === 'function') { + if (codings === 'br' && typeof zlib.createBrotliDecompress === 'function') { body = body.pipe(zlib.createBrotliDecompress()); - response = new Response(body, response_options); + response = new Response(body, responseOptions); resolve(response); return; } // Otherwise, use response as-is - response = new Response(body, response_options); + response = new Response(body, responseOptions); resolve(response); }); diff --git a/src/request.js b/src/request.js index 2af7a3c0d..41c94782e 100644 --- a/src/request.js +++ b/src/request.js @@ -9,14 +9,14 @@ import Url from 'url'; import Stream from 'stream'; -import Headers, { exportNodeCompatibleHeaders } from './headers'; -import Body, { clone, extractContentType, getTotalBytes } from './body'; +import Headers, {exportNodeCompatibleHeaders} from './headers'; +import Body, {clone, extractContentType, getTotalBytes} from './body'; const INTERNALS = Symbol('Request internals'); // Fix an issue where "format", "parse" aren't a named export for node <10 -const parse_url = Url.parse; -const format_url = Url.format; +const parseUrl = Url.parse; +const formatUrl = Url.format; const streamDestructionSupported = 'destroy' in Stream.Readable.prototype; @@ -59,15 +59,15 @@ export default class Request { // In order to support Node.js' Url objects; though WHATWG's URL objects // will fall into this branch also (since their `toString()` will return // `href` property anyway) - parsedURL = parse_url(input.href); + parsedURL = parseUrl(input.href); } else { // Coerce input to a string before attempting to parse - parsedURL = parse_url(`${input}`); + parsedURL = parseUrl(`${input}`); } input = {}; } else { - parsedURL = parse_url(input.url); + parsedURL = parseUrl(input.url); } let method = init.method || input.method || 'GET'; @@ -133,7 +133,7 @@ export default class Request { } get url() { - return format_url(this[INTERNALS].parsedURL); + return formatUrl(this[INTERNALS].parsedURL); } get headers() { @@ -168,12 +168,12 @@ Object.defineProperty(Request.prototype, Symbol.toStringTag, { }); Object.defineProperties(Request.prototype, { - method: { enumerable: true }, - url: { enumerable: true }, - headers: { enumerable: true }, - redirect: { enumerable: true }, - clone: { enumerable: true }, - signal: { enumerable: true } + method: {enumerable: true}, + url: {enumerable: true}, + headers: {enumerable: true}, + redirect: {enumerable: true}, + clone: {enumerable: true}, + signal: {enumerable: true} }); /** @@ -183,7 +183,7 @@ Object.defineProperties(Request.prototype, { * @return Object The options object to be passed to http.request */ export function getNodeRequestOptions(request) { - const { parsedURL } = request[INTERNALS]; + const {parsedURL} = request[INTERNALS]; const headers = new Headers(request[INTERNALS].headers); // Fetch step 1.3 @@ -235,7 +235,7 @@ export function getNodeRequestOptions(request) { headers.set('Accept-Encoding', 'gzip,deflate'); } - let { agent } = request; + let {agent} = request; if (typeof agent === 'function') { agent = agent(parsedURL); } diff --git a/src/response.js b/src/response.js index 51a00c8e4..85246df99 100644 --- a/src/response.js +++ b/src/response.js @@ -8,12 +8,12 @@ import http from 'http'; import Headers from './headers'; -import Body, { clone, extractContentType } from './body'; +import Body, {clone, extractContentType} from './body'; const INTERNALS = Symbol('Response internals'); // Fix an issue where "STATUS_CODES" aren't a named export for node <10 -const { STATUS_CODES } = http; +const {STATUS_CODES} = http; /** * Response class @@ -29,7 +29,7 @@ export default class Response { const status = opts.status || 200; const headers = new Headers(opts.headers); - if (body != null && !headers.has('Content-Type')) { + if (body !== null && !headers.has('Content-Type')) { const contentType = extractContentType(body); if (contentType) { headers.append('Content-Type', contentType); @@ -92,13 +92,13 @@ export default class Response { Body.mixIn(Response.prototype); Object.defineProperties(Response.prototype, { - url: { enumerable: true }, - status: { enumerable: true }, - ok: { enumerable: true }, - redirected: { enumerable: true }, - statusText: { enumerable: true }, - headers: { enumerable: true }, - clone: { enumerable: true } + url: {enumerable: true}, + status: {enumerable: true}, + ok: {enumerable: true}, + redirected: {enumerable: true}, + statusText: {enumerable: true}, + headers: {enumerable: true}, + clone: {enumerable: true} }); Object.defineProperty(Response.prototype, Symbol.toStringTag, { diff --git a/test/server.js b/test/server.js index d28ec966c..243596bff 100644 --- a/test/server.js +++ b/test/server.js @@ -1,11 +1,11 @@ import * as http from 'http'; -import { parse } from 'url'; +import {parse} from 'url'; import * as zlib from 'zlib'; -import * as stream from 'stream'; -import { multipart as Multipart } from 'parted'; +import {multipart as Multipart} from 'parted'; let convert; try { + /* eslint-disable-next-line import/no-unresolved */ convert = require('encoding').convert; } catch (error) {} diff --git a/test/test.js b/test/test.js index 289139b9c..a8c880d60 100644 --- a/test/test.js +++ b/test/test.js @@ -1,4 +1,3 @@ - // Test tools import zlib from 'zlib'; import chai from 'chai'; @@ -21,12 +20,12 @@ import fetch, { Request, Response } from '../src'; -import FetchErrorOrig from '../src/fetch-error.js'; -import HeadersOrig, { createHeadersLenient } from '../src/headers.js'; -import RequestOrig from '../src/request.js'; -import ResponseOrig from '../src/response.js'; -import Body, { getTotalBytes, extractContentType } from '../src/body.js'; -import Blob from '../src/blob.js'; +import FetchErrorOrig from '../src/fetch-error'; +import HeadersOrig, { createHeadersLenient } from '../src/headers'; +import RequestOrig from '../src/request'; +import ResponseOrig from '../src/response'; +import Body, { getTotalBytes, extractContentType } from '../src/body'; +import Blob from '../src/blob'; import TestServer from './server'; const { spawn } = require('child_process'); @@ -1638,7 +1637,7 @@ describe('node-fetch', () => { const url = `${base}plain`; return fetch(url).then(res => { expect(res.headers.get('content-type')).to.equal('text/plain'); - return res.text().then(result => { + return res.text().then(() => { expect(res.bodyUsed).to.be.true; return expect(res.text()).to.eventually.be.rejectedWith(Error); }); @@ -1748,7 +1747,7 @@ describe('node-fetch', () => { it('should not allow cloning a response after its been used', () => { const url = `${base}hello`; return fetch(url).then(res => - res.text().then(result => { + res.text().then(() => { expect(() => { res.clone(); }).to.throw(Error); @@ -1862,7 +1861,8 @@ describe('node-fetch', () => { it('should support blob round-trip', () => { const url = `${base}hello`; - let length; let type; + let length; + let type; return fetch(url).then(res => res.blob()).then(blob => { const url = `${base}inspect`; @@ -1909,6 +1909,7 @@ describe('node-fetch', () => { expect(body).to.have.property('buffer'); }); + /* eslint-disable-next-line func-names */ it('should create custom FetchError', function funcName() { const systemError = new Error('system'); systemError.code = 'ESOMEERROR'; @@ -2081,6 +2082,7 @@ describe('Headers', () => { const headers = new Headers(); expect(Object.getOwnPropertyNames(headers)).to.be.empty; const enumerableProperties = []; + for (const property in headers) { enumerableProperties.push(property); } From d68d0ef2dc4bd0afc5ddbd2110b5509b7d17c5f8 Mon Sep 17 00:00:00 2001 From: Richie Bendall Date: Sun, 8 Sep 2019 09:32:34 +1200 Subject: [PATCH 012/157] chore: Migrate to microbundle --- .babelrc | 52 ----------------------------------- .gitignore | 3 +++ browser.js | 32 ---------------------- build/babel-plugin.js | 61 ------------------------------------------ build/rollup-plugin.js | 19 ------------- package.json | 44 +++++++++++++++++++----------- rollup.config.js | 28 ------------------- 7 files changed, 32 insertions(+), 207 deletions(-) delete mode 100644 .babelrc delete mode 100644 browser.js delete mode 100644 build/babel-plugin.js delete mode 100644 build/rollup-plugin.js delete mode 100644 rollup.config.js diff --git a/.babelrc b/.babelrc deleted file mode 100644 index 6a95c25e7..000000000 --- a/.babelrc +++ /dev/null @@ -1,52 +0,0 @@ -{ - env: { - test: { - presets: [ - [ 'env', { - loose: true, - targets: { node: 4 }, - exclude: [ - // skip some almost-compliant features on Node.js v4.x - 'transform-es2015-block-scoping', - 'transform-es2015-classes', - 'transform-es2015-for-of', - ] - } ] - ], - plugins: [ - './build/babel-plugin' - ] - }, - coverage: { - presets: [ - [ 'env', { - loose: true, - targets: { node: 4 }, - exclude: [ - 'transform-es2015-block-scoping', - 'transform-es2015-classes', - 'transform-es2015-for-of' - ] - } ] - ], - plugins: [ - [ 'istanbul', { exclude: [ 'src/blob.js', 'build', 'test' ] } ], - './build/babel-plugin' - ] - }, - rollup: { - presets: [ - [ 'env', { - loose: true, - targets: { node: 4 }, - exclude: [ - 'transform-es2015-block-scoping', - 'transform-es2015-classes', - 'transform-es2015-for-of' - ], - modules: false - } ] - ] - } - } -} diff --git a/.gitignore b/.gitignore index 839eff401..565dda366 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,6 @@ +# Generated files +dist/ + # Logs logs *.log diff --git a/browser.js b/browser.js deleted file mode 100644 index 47f686731..000000000 --- a/browser.js +++ /dev/null @@ -1,32 +0,0 @@ -'use strict'; - -// Ref: https://github.com/tc39/proposal-global -const getGlobal = function () { - // The only reliable means to get the global object is - // `Function('return this')()` - // However, this causes CSP violations in Chrome apps. - if (typeof self !== 'undefined') { - return self; - } - - if (typeof window !== 'undefined') { - return window; - } - - if (typeof global !== 'undefined') { - return global; - } - - throw new Error('unable to locate global object'); -}; - -var global = getGlobal(); - -module.exports = exports = global.fetch; - -// Needed for TypeScript and Webpack. -exports.default = global.fetch.bind(global); - -exports.Headers = global.Headers; -exports.Request = global.Request; -exports.Response = global.Response; diff --git a/build/babel-plugin.js b/build/babel-plugin.js deleted file mode 100644 index f13727b6b..000000000 --- a/build/babel-plugin.js +++ /dev/null @@ -1,61 +0,0 @@ -// This Babel plugin makes it possible to do CommonJS-style function exports - -const walked = Symbol('walked'); - -module.exports = ({ types: t }) => ({ - visitor: { - Program: { - exit(program) { - if (program[walked]) { - return; - } - - for (const path of program.get('body')) { - if (path.isExpressionStatement()) { - const expr = path.get('expression'); - if (expr.isAssignmentExpression() && - expr.get('left').matchesPattern('exports.*')) { - const prop = expr.get('left').get('property'); - if (prop.isIdentifier({ name: 'default' })) { - program.unshiftContainer('body', [ - t.expressionStatement( - t.assignmentExpression('=', - t.identifier('exports'), - t.assignmentExpression('=', - t.memberExpression( - t.identifier('module'), t.identifier('exports') - ), - expr.node.right - ) - ) - ), - t.expressionStatement( - t.callExpression( - t.memberExpression( - t.identifier('Object'), t.identifier('defineProperty')), - [ - t.identifier('exports'), - t.stringLiteral('__esModule'), - t.objectExpression([ - t.objectProperty(t.identifier('value'), t.booleanLiteral(true)) - ]) - ] - ) - ), - t.expressionStatement( - t.assignmentExpression('=', - expr.node.left, t.identifier('exports') - ) - ) - ]); - path.remove(); - } - } - } - } - - program[walked] = true; - } - } - } -}); diff --git a/build/rollup-plugin.js b/build/rollup-plugin.js deleted file mode 100644 index b9cff7aa5..000000000 --- a/build/rollup-plugin.js +++ /dev/null @@ -1,19 +0,0 @@ -export default function tweakDefault() { - return { - transformBundle(source) { - const lines = source.split('\n'); - for (let i = 0; i < lines.length; i++) { - const line = lines[i]; - const matches = /^(exports(?:\['default']|\.default)) = (.*);$/.exec(line); - if (matches) { - lines[i] = 'module.exports = exports = ' + matches[2] + ';\n' + - 'Object.defineProperty(exports, "__esModule", { value: true });\n' + - matches[1] + ' = exports;'; - break; - } - } - - return lines.join('\n'); - } - }; -} diff --git a/package.json b/package.json index d7cc06e50..6e8274be1 100644 --- a/package.json +++ b/package.json @@ -2,24 +2,21 @@ "name": "node-fetch", "version": "2.6.0", "description": "A light-weight module that brings window.fetch to node.js", - "main": "lib/index", - "browser": "./browser.js", - "module": "lib/index.mjs", - "types": "./types/index.d.ts", + "main": "dist/index.js", + "module": "dist/index.mjs", + "types": "types/index.d.ts", "files": [ - "lib/index.js", - "lib/index.mjs", - "lib/index.es.js", - "browser.js", + "src/*", + "dist/*", "types/*.d.ts" ], "engines": { "node": ">=8.0.0" }, "scripts": { - "build": "cross-env BABEL_ENV=rollup rollup -c", + "build": "microbundle --external http,https,stream,zlib --name fetch --target node --format es,cjs", "prepare": "npm run build", - "test": "cross-env BABEL_ENV=test mocha --require babel-register --throw-deprecation test/test.js", + "test": "cross-env BABEL_ENV=test mocha --require @babel/register --throw-deprecation test/test.js", "report": "cross-env BABEL_ENV=coverage nyc --reporter lcov --reporter text mocha -R spec test/test.js", "coverage": "cross-env BABEL_ENV=coverage nyc --reporter json --reporter text mocha -R spec test/test.js && codecov -f coverage/coverage-final.json", "lint": "xo src" @@ -41,12 +38,11 @@ "homepage": "https://github.com/bitinn/node-fetch", "devDependencies": { "@babel/core": "^7.6.0", + "@babel/preset-env": "^7.6.0", + "@babel/register": "^7.6.0", "@ungap/url-search-params": "^0.1.2", "abort-controller": "^3.0.0", "abortcontroller-polyfill": "^1.3.0", - "babel-plugin-istanbul": "^5.2.0", - "babel-preset-env": "^1.6.1", - "babel-register": "^6.16.3", "chai": "^4.2.0", "chai-as-promised": "^7.1.1", "chai-iterator": "^3.0.2", @@ -55,18 +51,24 @@ "cross-env": "^5.2.0", "form-data": "^2.3.3", "is-builtin-module": "^3.0.0", + "microbundle": "^0.11.0", "mocha": "^6.2.0", "nyc": "^14.1.1", "parted": "^0.1.1", "promise": "^8.0.3", "resumer": "0.0.0", - "rollup": "^1.20.3", - "rollup-plugin-babel": "^4.3.3", "string-to-arraybuffer": "^1.0.2", "whatwg-url": "^7.0.0", "xo": "^0.24.0" }, "dependencies": {}, + "optionalDependencies": { + "encoding": "^0.1.12" + }, + "resolutions": { + "microbundle/rollup-plugin-typescript2/rollup-pluginutils/micromatch/braces": "^2.3.1", + "microbundle/rollup-plugin-postcss/cssnano/postcss-svgo/svgo/js-yaml": "^3.13.1" + }, "xo": { "rules": { "object-curly-spacing": [ @@ -78,5 +80,17 @@ "complexity": 0, "unicorn/prefer-spread": 0 } + }, + "babel": { + "presets": [ + [ + "@babel/preset-env", + { + "targets": { + "node": true + } + } + ] + ] } } diff --git a/rollup.config.js b/rollup.config.js deleted file mode 100644 index 9beb2014a..000000000 --- a/rollup.config.js +++ /dev/null @@ -1,28 +0,0 @@ -import isBuiltin from 'is-builtin-module'; -import babel from 'rollup-plugin-babel'; -import tweakDefault from './build/rollup-plugin'; - -process.env.BABEL_ENV = 'rollup'; - -export default { - input: 'src/index.js', - output: [ - { file: 'lib/index.js', format: 'cjs', exports: 'named' }, - { file: 'lib/index.es.js', format: 'es', exports: 'named', intro: 'process.emitWarning("The .es.js file is deprecated. Use .mjs instead.");' }, - { file: 'lib/index.mjs', format: 'es', exports: 'named' } - ], - plugins: [ - babel({ - runtimeHelpers: true - }), - tweakDefault() - ], - external(id) { - if (isBuiltin(id)) { - return true; - } - - id = id.split('/').slice(0, id[0] === '@' ? 2 : 1).join('/'); - return Boolean(require('./package.json').dependencies[id]); - } -}; From 48a356c84a23cf5407c7f504e9af8536d602fc4e Mon Sep 17 00:00:00 2001 From: Antoni Kepinski Date: Sat, 7 Sep 2019 23:37:28 +0200 Subject: [PATCH 013/157] Default response.statusText should be blank (#578) --- src/response.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/response.js b/src/response.js index 85246df99..724f673c5 100644 --- a/src/response.js +++ b/src/response.js @@ -39,7 +39,7 @@ export default class Response { this[INTERNALS] = { url: opts.url, status, - statusText: opts.statusText || STATUS_CODES[status], + statusText: opts.statusText || '', headers, counter: opts.counter }; From 886277fc4349c98e3d44d75d8151120341f1e9e5 Mon Sep 17 00:00:00 2001 From: Richie Bendall Date: Sun, 8 Sep 2019 12:57:18 +1200 Subject: [PATCH 014/157] fix: Use correct AbortionError message Signed-off-by: Richie Bendall --- src/index.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/index.js b/src/index.js index dab7387df..c3676a474 100644 --- a/src/index.js +++ b/src/index.js @@ -50,7 +50,7 @@ export default function fetch(url, opts) { let response = null; const abort = () => { - const error = new AbortError('The user aborted a request.'); + const error = new AbortError('The operation was aborted.'); reject(error); if (request.body && request.body instanceof Stream.Readable) { request.body.destroy(error); @@ -184,7 +184,7 @@ export default function fetch(url, opts) { } default: - // Do nothing + // Do nothing } } From 446d9444debec90cb6507e7a869446a3c603b60d Mon Sep 17 00:00:00 2001 From: Richie Bendall Date: Sun, 8 Sep 2019 13:01:01 +1200 Subject: [PATCH 015/157] chore: Use modern @babel/register Signed-off-by: Richie Bendall --- .nycrc | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/.nycrc b/.nycrc index d8d9c1432..65327e73b 100644 --- a/.nycrc +++ b/.nycrc @@ -1,7 +1,5 @@ { - "require": [ - "babel-register" - ], + "require": ["@babel/register"], "sourceMap": false, "instrument": false } From ea3a74890aba92389152b4b31365dddafb507e2f Mon Sep 17 00:00:00 2001 From: Richie Bendall Date: Sun, 8 Sep 2019 13:44:50 +1200 Subject: [PATCH 016/157] chore: Remove redundant packages Signed-off-by: Richie Bendall --- package.json | 8 ++------ test/test.js | 15 ++++++--------- 2 files changed, 8 insertions(+), 15 deletions(-) diff --git a/package.json b/package.json index 0254f4cac..80f49b5ac 100644 --- a/package.json +++ b/package.json @@ -40,7 +40,6 @@ "@babel/core": "^7.6.0", "@babel/preset-env": "^7.6.0", "@babel/register": "^7.6.0", - "@ungap/url-search-params": "^0.1.2", "abort-controller": "^3.0.0", "abortcontroller-polyfill": "^1.3.0", "chai": "^4.2.0", @@ -49,8 +48,6 @@ "chai-string": "^1.5.0", "codecov": "^3.3.0", "cross-env": "^5.2.0", - "form-data": "^2.3.3", - "is-builtin-module": "^3.0.0", "microbundle": "^0.11.0", "mocha": "^6.2.0", "nyc": "^14.1.1", @@ -58,7 +55,6 @@ "promise": "^8.0.3", "resumer": "0.0.0", "string-to-arraybuffer": "^1.0.2", - "whatwg-url": "^7.0.0", "xo": "^0.24.0" }, "dependencies": {}, @@ -66,8 +62,8 @@ "encoding": "^0.1.12" }, "resolutions": { - "microbundle/rollup-plugin-typescript2/rollup-pluginutils/micromatch/braces": "^2.3.1", - "microbundle/rollup-plugin-postcss/cssnano/postcss-svgo/svgo/js-yaml": "^3.13.1" + "microbundle/rollup-plugin-typescript2/rollup-pluginutils/micromatch/braces": "^2.3.1", + "microbundle/rollup-plugin-postcss/cssnano/postcss-svgo/svgo/js-yaml": "^3.13.1" }, "xo": { "envs": [ diff --git a/test/test.js b/test/test.js index a8c880d60..37ad07f00 100644 --- a/test/test.js +++ b/test/test.js @@ -6,10 +6,7 @@ import chaiIterator from 'chai-iterator'; import chaiString from 'chai-string'; import then from 'promise'; import resumer from 'resumer'; -import FormData from 'form-data'; import stringToArrayBuffer from 'string-to-arraybuffer'; -import URLSearchParams_Polyfill from '@ungap/url-search-params'; -import { URL } from 'whatwg-url'; import { AbortController } from 'abortcontroller-polyfill/dist/abortcontroller'; import AbortController2 from 'abort-controller'; @@ -739,7 +736,7 @@ describe('node-fetch', () => { .then(res => { expect(res.status).to.equal(200); }) - .catch(() => {}) + .catch(() => { }) .then(() => { // Wait a few ms to see if a uncaught error occurs setTimeout(() => { @@ -1033,7 +1030,7 @@ describe('node-fetch', () => { (supportStreamDestroy ? it : it.skip)('should cancel request body of type Stream with AbortError when aborted', () => { const controller = new AbortController(); const body = new stream.Readable({ objectMode: true }); - body._read = () => {}; + body._read = () => { }; const promise = fetch( `${base}slow`, { signal: controller.signal, body, method: 'POST' } @@ -1063,7 +1060,7 @@ describe('node-fetch', () => { (supportStreamDestroy ? it.skip : it)('should immediately reject when attempting to cancel streamed Requests in node < 8', () => { const controller = new AbortController(); const body = new stream.Readable({ objectMode: true }); - body._read = () => {}; + body._read = () => { }; const promise = fetch( `${base}slow`, { signal: controller.signal, body, method: 'POST' } @@ -1472,7 +1469,7 @@ describe('node-fetch', () => { }); itUSP('should still recognize URLSearchParams when extended', () => { - class CustomSearchParams extends URLSearchParams {} + class CustomSearchParams extends URLSearchParams { } const params = new CustomSearchParams(); params.append('a', '1'); @@ -1494,7 +1491,7 @@ describe('node-fetch', () => { /* For 100% code coverage, checks for duck-typing-only detection * where both constructor.name and brand tests fail */ it('should still recognize URLSearchParams when extended from polyfill', () => { - class CustomPolyfilledSearchParams extends URLSearchParams_Polyfill {} + class CustomPolyfilledSearchParams extends URLSearchParams { } const params = new CustomPolyfilledSearchParams(); params.append('a', '1'); @@ -2202,7 +2199,7 @@ describe('Headers', () => { }); it('should ignore unsupported attributes while reading headers', () => { - const FakeHeader = function () {}; + const FakeHeader = function () { }; // Prototypes are currently ignored // This might change in the future: #181 FakeHeader.prototype.z = 'fake'; From 1cecbc4f5b97ae3e28079b6159e43d9259302da8 Mon Sep 17 00:00:00 2001 From: Richie Bendall Date: Sun, 8 Sep 2019 14:00:45 +1200 Subject: [PATCH 017/157] chore: Readd form-data Signed-off-by: Richie Bendall --- package.json | 1 + test/test.js | 1 + 2 files changed, 2 insertions(+) diff --git a/package.json b/package.json index 80f49b5ac..5b400649b 100644 --- a/package.json +++ b/package.json @@ -48,6 +48,7 @@ "chai-string": "^1.5.0", "codecov": "^3.3.0", "cross-env": "^5.2.0", + "form-data": "^2.5.1", "microbundle": "^0.11.0", "mocha": "^6.2.0", "nyc": "^14.1.1", diff --git a/test/test.js b/test/test.js index 37ad07f00..9b83cb77c 100644 --- a/test/test.js +++ b/test/test.js @@ -6,6 +6,7 @@ import chaiIterator from 'chai-iterator'; import chaiString from 'chai-string'; import then from 'promise'; import resumer from 'resumer'; +import FormData from 'form-data'; import stringToArrayBuffer from 'string-to-arraybuffer'; import { AbortController } from 'abortcontroller-polyfill/dist/abortcontroller'; import AbortController2 from 'abort-controller'; From f3b00aaf8ed847fd3f30b1b7182cdafa411d02f0 Mon Sep 17 00:00:00 2001 From: Richie Bendall Date: Sun, 8 Sep 2019 18:42:51 +1200 Subject: [PATCH 018/157] fix: Fix tests and force utf8-encoded urls Signed-off-by: Richie Bendall --- package.json | 2 ++ src/index.js | 12 ++++++------ src/request.js | 11 +++++++---- test/test.js | 2 ++ 4 files changed, 17 insertions(+), 10 deletions(-) diff --git a/package.json b/package.json index 5b400649b..f47aba571 100644 --- a/package.json +++ b/package.json @@ -56,6 +56,8 @@ "promise": "^8.0.3", "resumer": "0.0.0", "string-to-arraybuffer": "^1.0.2", + "utf8": "^3.0.0", + "whatwg-url": "^7.0.0", "xo": "^0.24.0" }, "dependencies": {}, diff --git a/src/index.js b/src/index.js index c3676a474..6540fa0ee 100644 --- a/src/index.js +++ b/src/index.js @@ -13,15 +13,15 @@ import https from 'https'; import zlib from 'zlib'; import Stream from 'stream'; -import Body, {writeToStream, getTotalBytes} from './body'; +import Body, { writeToStream, getTotalBytes } from './body'; import Response from './response'; -import Headers, {createHeadersLenient} from './headers'; -import Request, {getNodeRequestOptions} from './request'; +import Headers, { createHeadersLenient } from './headers'; +import Request, { getNodeRequestOptions } from './request'; import FetchError from './fetch-error'; import AbortError from './abort-error'; // Fix an issue where "PassThrough", "resolve" aren't a named export for node <10 -const {PassThrough} = Stream; +const { PassThrough } = Stream; const resolveUrl = Url.resolve; /** @@ -46,7 +46,7 @@ export default function fetch(url, opts) { const options = getNodeRequestOptions(request); const send = (options.protocol === 'https:' ? https : http).request; - const {signal} = request; + const { signal } = request; let response = null; const abort = () => { @@ -283,7 +283,7 @@ export default function fetch(url, opts) { * @param Number code Status code * @return Boolean */ -fetch.isRedirect = code => code === 301 || code === 302 || code === 303 || code === 307 || code === 308; +fetch.isRedirect = code => [301, 302, 303, 307, 308].includes(code); // Expose Promise fetch.Promise = global.Promise; diff --git a/src/request.js b/src/request.js index 41c94782e..1aaaaf8a4 100644 --- a/src/request.js +++ b/src/request.js @@ -9,6 +9,7 @@ import Url from 'url'; import Stream from 'stream'; +import utf8 from 'utf8'; import Headers, {exportNodeCompatibleHeaders} from './headers'; import Body, {clone, extractContentType, getTotalBytes} from './body'; @@ -53,21 +54,21 @@ export default class Request { constructor(input, init = {}) { let parsedURL; - // Normalize input + // Normalize input and force URL to be encoded as UTF-8 (https://github.com/bitinn/node-fetch/issues/245) if (!isRequest(input)) { 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); + parsedURL = parseUrl(utf8.encode(input.href)); } else { // Coerce input to a string before attempting to parse - parsedURL = parseUrl(`${input}`); + parsedURL = parseUrl(utf8.encode(`${input}`)); } input = {}; } else { - parsedURL = parseUrl(input.url); + parsedURL = parseUrl(utf8.encode(input.url)); } let method = init.method || input.method || 'GET'; @@ -191,6 +192,8 @@ export function getNodeRequestOptions(request) { headers.set('Accept', '*/*'); } + // Console.log(parsedURL.protocol, parsedURL.hostname) + // Basic fetch if (!parsedURL.protocol || !parsedURL.hostname) { throw new TypeError('Only absolute URLs are supported'); diff --git a/test/test.js b/test/test.js index 9b83cb77c..180dc9f4f 100644 --- a/test/test.js +++ b/test/test.js @@ -8,6 +8,8 @@ import then from 'promise'; import resumer from 'resumer'; import FormData from 'form-data'; import stringToArrayBuffer from 'string-to-arraybuffer'; + +import { URL } from 'whatwg-url'; import { AbortController } from 'abortcontroller-polyfill/dist/abortcontroller'; import AbortController2 from 'abort-controller'; From 9a44083c4f61402470f50fb0f9815a74d4871a45 Mon Sep 17 00:00:00 2001 From: Antoni Kepinski Date: Sun, 8 Sep 2019 12:40:04 +0200 Subject: [PATCH 019/157] lint index.js --- src/index.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/index.js b/src/index.js index 6540fa0ee..8cbc5b069 100644 --- a/src/index.js +++ b/src/index.js @@ -13,15 +13,15 @@ import https from 'https'; import zlib from 'zlib'; import Stream from 'stream'; -import Body, { writeToStream, getTotalBytes } from './body'; +import Body, {writeToStream, getTotalBytes} from './body'; import Response from './response'; -import Headers, { createHeadersLenient } from './headers'; -import Request, { getNodeRequestOptions } from './request'; +import Headers, {createHeadersLenient} from './headers'; +import Request, {getNodeRequestOptions} from './request'; import FetchError from './fetch-error'; import AbortError from './abort-error'; // Fix an issue where "PassThrough", "resolve" aren't a named export for node <10 -const { PassThrough } = Stream; +const {PassThrough} = Stream; const resolveUrl = Url.resolve; /** @@ -46,7 +46,7 @@ export default function fetch(url, opts) { const options = getNodeRequestOptions(request); const send = (options.protocol === 'https:' ? https : http).request; - const { signal } = request; + const {signal} = request; let response = null; const abort = () => { From 06212bfd9549ffdda3c3f684112fefa078a42e40 Mon Sep 17 00:00:00 2001 From: Antoni Kepinski Date: Sun, 8 Sep 2019 12:48:37 +0200 Subject: [PATCH 020/157] Update devDependencies & ignore `test` directory in linter options --- package.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index f47aba571..6f68e1ef8 100644 --- a/package.json +++ b/package.json @@ -46,8 +46,8 @@ "chai-as-promised": "^7.1.1", "chai-iterator": "^3.0.2", "chai-string": "^1.5.0", - "codecov": "^3.3.0", - "cross-env": "^5.2.0", + "codecov": "^3.5.0", + "cross-env": "^5.2.1", "form-data": "^2.5.1", "microbundle": "^0.11.0", "mocha": "^6.2.0", @@ -89,7 +89,7 @@ }, "ignores": [ "dist", - "test/test.js" + "test" ] }, "babel": { From cf8ac41f15bbbef13c663e8af78ae49b8719b379 Mon Sep 17 00:00:00 2001 From: Antoni Kepinski Date: Sun, 8 Sep 2019 12:48:52 +0200 Subject: [PATCH 021/157] Remove unnecessary eslint-ignore comment --- src/body.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/body.js b/src/body.js index f85b7c161..47503f1e1 100644 --- a/src/body.js +++ b/src/body.js @@ -12,7 +12,6 @@ import FetchError from './fetch-error'; let convert; try { - /* eslint-disable-next-line import/no-unresolved */ convert = require('encoding').convert; } catch (error) {} From 520ad77109922c6d9aa5990f738ccaeb17d4603c Mon Sep 17 00:00:00 2001 From: Antoni Kepinski Date: Sun, 8 Sep 2019 12:50:06 +0200 Subject: [PATCH 022/157] Update the `lint` script to run linter on every file --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 6f68e1ef8..19de3db94 100644 --- a/package.json +++ b/package.json @@ -19,7 +19,7 @@ "test": "cross-env BABEL_ENV=test mocha --require @babel/register --throw-deprecation test/test.js", "report": "cross-env BABEL_ENV=coverage nyc --reporter lcov --reporter text mocha -R spec test/test.js", "coverage": "cross-env BABEL_ENV=coverage nyc --reporter json --reporter text mocha -R spec test/test.js && codecov -f coverage/coverage-final.json", - "lint": "xo src" + "lint": "xo" }, "repository": { "type": "git", From 899c3d22e4b2c42f9a1021213a91d68024126ef3 Mon Sep 17 00:00:00 2001 From: Antoni Kepinski Date: Sun, 8 Sep 2019 18:51:09 +0200 Subject: [PATCH 023/157] Remove unused const & unnecessary import --- src/response.js | 6 ------ 1 file changed, 6 deletions(-) diff --git a/src/response.js b/src/response.js index 724f673c5..8ec0b9cf8 100644 --- a/src/response.js +++ b/src/response.js @@ -1,20 +1,14 @@ - /** * Response.js * * Response class provides content decoding */ -import http from 'http'; - import Headers from './headers'; import Body, {clone, extractContentType} from './body'; const INTERNALS = Symbol('Response internals'); -// Fix an issue where "STATUS_CODES" aren't a named export for node <10 -const {STATUS_CODES} = http; - /** * Response class * From e8beddb2069c7ecfa958819108689c86f9d6fb09 Mon Sep 17 00:00:00 2001 From: Antoni Kepinski Date: Sun, 8 Sep 2019 19:54:23 +0200 Subject: [PATCH 024/157] TypeScript: Fix Body.blob() wrong type (DefinitelyTyped/DefinitelyTyped#33721) --- types/index.d.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/types/index.d.ts b/types/index.d.ts index b3d739aa6..f7b2132cc 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -137,7 +137,7 @@ export class Blob { export class Body { constructor(body?: any, opts?: { size?: number; timeout?: number }); arrayBuffer(): Promise; - blob(): Promise; + blob(): Promise; body: NodeJS.ReadableStream; bodyUsed: boolean; buffer(): Promise; From 866e3bc3cea2d5a53508ae5a1d2007860dbd2c06 Mon Sep 17 00:00:00 2001 From: Richie Bendall Date: Tue, 10 Sep 2019 23:03:44 +1200 Subject: [PATCH 025/157] chore: Lint as part of the build process --- .travis.yml | 10 ++++++++-- package.json | 3 ++- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/.travis.yml b/.travis.yml index a2f43ff5c..f96441bad 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,8 +2,14 @@ language: node_js node_js: - "8" - - "lts/*" # 10 - - "node" # 12 + - "lts/*" # 10 (Latest LTS) + - "node" # 12 (Latest Stable) + +matrix: + include: + - # Linting stage + node_js: "lts/*" # Latest LTS + script: npm run lint cache: npm diff --git a/package.json b/package.json index 19de3db94..76001cfdb 100644 --- a/package.json +++ b/package.json @@ -85,7 +85,8 @@ "eqeqeq": 0, "no-eq-null": 0, "no-negated-condition": 0, - "prefer-rest-params": 0 + "prefer-rest-params": 0, + "node/no-deprecated-api": 1 }, "ignores": [ "dist", From 51c6f1e74a380a33cf5cc74e280256102dedc4ac Mon Sep 17 00:00:00 2001 From: Richie Bendall Date: Wed, 11 Sep 2019 15:48:33 +1200 Subject: [PATCH 026/157] fix: Convert Content-Encoding to lowercase (#672) --- src/headers.js | 7 ++++++- test/server.js | 9 +++++++++ test/test.js | 11 +++++++++++ 3 files changed, 26 insertions(+), 1 deletion(-) diff --git a/src/headers.js b/src/headers.js index 5a4f5c953..7df601e13 100644 --- a/src/headers.js +++ b/src/headers.js @@ -120,7 +120,12 @@ export default class Headers { return null; } - return this[MAP][key].join(', '); + let val = this[MAP][key].join(', '); + if (name.toLowerCase() === 'content-encoding') { + val = val.toLowerCase(); + } + + return val; } /** diff --git a/test/server.js b/test/server.js index 243596bff..d276a241d 100644 --- a/test/server.js +++ b/test/server.js @@ -87,6 +87,15 @@ export default class TestServer { }); } + if (p === '/gzip-capital') { + res.statusCode = 200; + res.setHeader('Content-Type', 'text/plain'); + res.setHeader('Content-Encoding', 'GZip'); + zlib.gzip('hello world', (err, buffer) => { + res.end(buffer); + }); + } + if (p === '/deflate') { res.statusCode = 200; res.setHeader('Content-Type', 'text/plain'); diff --git a/test/test.js b/test/test.js index 180dc9f4f..a7fe33df3 100644 --- a/test/test.js +++ b/test/test.js @@ -657,6 +657,17 @@ describe('node-fetch', () => { }); }); + it('should make capitalised Content-Encoding lowercase', () => { + const url = `${base}gzip-capital`; + return fetch(url).then(res => { + expect(res.headers.get('content-encoding')).to.equal("gzip"); + return res.text().then(result => { + expect(result).to.be.a('string'); + expect(result).to.equal('hello world'); + }); + }) + }) + it('should decompress deflate response', () => { const url = `${base}deflate`; return fetch(url).then(res => { From 44887cd247dfb021639d9f2adb2778f667c0ddb2 Mon Sep 17 00:00:00 2001 From: Richie Bendall Date: Sun, 15 Sep 2019 02:43:40 +1200 Subject: [PATCH 027/157] fix: Better object checks (#673) --- src/body.js | 44 ++---------------------------------- src/request.js | 22 ++++++------------ src/utils/is.js | 60 +++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 69 insertions(+), 57 deletions(-) create mode 100644 src/utils/is.js diff --git a/src/body.js b/src/body.js index 47503f1e1..3dde7f776 100644 --- a/src/body.js +++ b/src/body.js @@ -9,11 +9,12 @@ import Stream from 'stream'; import Blob, {BUFFER} from './blob'; import FetchError from './fetch-error'; +import {isBlob, isURLSearchParams} from './utils/is'; let convert; try { convert = require('encoding').convert; -} catch (error) {} +} catch (error) { } const INTERNALS = Symbol('Body internals'); @@ -341,47 +342,6 @@ function convertBody(buffer, headers) { ).toString(); } -/** - * Detect a URLSearchParams object - * ref: https://github.com/bitinn/node-fetch/issues/296#issuecomment-307598143 - * - * @param Object obj Object to detect by type or brand - * @return String - */ -function isURLSearchParams(obj) { - // Duck-typing as a necessary condition. - if (typeof obj !== 'object' || - typeof obj.append !== 'function' || - typeof obj.delete !== 'function' || - typeof obj.get !== 'function' || - typeof obj.getAll !== 'function' || - typeof obj.has !== 'function' || - typeof obj.set !== 'function') { - return false; - } - - // Brand-checking and more duck-typing as optional condition. - return obj.constructor.name === 'URLSearchParams' || - Object.prototype.toString.call(obj) === '[object URLSearchParams]' || - typeof obj.sort === 'function'; -} - -/** - * Check if `obj` is a W3C `Blob` object (which `File` inherits from) - * @param {*} obj - * @return {boolean} - */ -function isBlob(obj) { - return typeof obj === 'object' && - typeof obj.arrayBuffer === 'function' && - typeof obj.type === 'string' && - typeof obj.stream === 'function' && - typeof obj.constructor === 'function' && - typeof obj.constructor.name === 'string' && - /^(Blob|File)$/.test(obj.constructor.name) && - /^(Blob|File)$/.test(obj[Symbol.toStringTag]); -} - /** * Clone body given Res/Req instance * diff --git a/src/request.js b/src/request.js index 1aaaaf8a4..63eaa4211 100644 --- a/src/request.js +++ b/src/request.js @@ -12,6 +12,7 @@ import Stream from 'stream'; import utf8 from 'utf8'; import Headers, {exportNodeCompatibleHeaders} from './headers'; import Body, {clone, extractContentType, getTotalBytes} from './body'; +import {isAbortSignal} from './utils/is'; const INTERNALS = Symbol('Request internals'); @@ -22,27 +23,18 @@ const formatUrl = Url.format; const streamDestructionSupported = 'destroy' in Stream.Readable.prototype; /** - * Check if a value is an instance of Request. + * Check if `obj` is an instance of Request. * - * @param Mixed input - * @return Boolean + * @param {*} obj + * @return {boolean} */ -function isRequest(input) { +function isRequest(obj) { return ( - typeof input === 'object' && - typeof input[INTERNALS] === 'object' + typeof obj === 'object' && + typeof obj[INTERNALS] === 'object' ); } -function isAbortSignal(signal) { - const proto = ( - signal && - typeof signal === 'object' && - Object.getPrototypeOf(signal) - ); - return Boolean(proto && proto.constructor.name === 'AbortSignal'); -} - /** * Request class * diff --git a/src/utils/is.js b/src/utils/is.js new file mode 100644 index 000000000..2c585ee7e --- /dev/null +++ b/src/utils/is.js @@ -0,0 +1,60 @@ +/** + * Is.js + * + * Object type checks. + */ + +const NAME = Symbol.toStringTag; + +/** + * Check if `obj` is a URLSearchParams object + * ref: https://github.com/bitinn/node-fetch/issues/296#issuecomment-307598143 + * + * @param {*} obj + * @return {boolean} + */ +export function isURLSearchParams(obj) { + return ( + typeof obj === 'object' && + typeof obj.append === 'function' && + typeof obj.delete === 'function' && + typeof obj.get === 'function' && + typeof obj.getAll === 'function' && + typeof obj.has === 'function' && + typeof obj.set === 'function' && + typeof obj.sort === 'function' && + obj[NAME] === 'URLSearchParams' + ); +} + +/** + * Check if `obj` is a W3C `Blob` object (which `File` inherits from) + * + * @param {*} obj + * @return {boolean} + */ +export function isBlob(obj) { + return ( + typeof obj === 'object' && + typeof obj.arrayBuffer === 'function' && + typeof obj.type === 'string' && + typeof obj.stream === 'function' && + typeof obj.constructor === 'function' && + /^(Blob|File)$/.test(obj[NAME]) + ); +} + +/** + * Check if `obj` is an instance of AbortSignal. + * + * @param {*} obj + * @return {boolean} + */ +export function isAbortSignal(obj) { + return ( + obj && + typeof obj === 'object' && + obj[NAME] === 'AbortSignal' + ); +} + From b293d24d7c71be9f723ada8eacac0025dbeadf73 Mon Sep 17 00:00:00 2001 From: Antoni Kepinski Date: Tue, 17 Sep 2019 18:29:20 -0400 Subject: [PATCH 028/157] Fix stream piping (#670) --- package.json | 6 ++++-- src/index.js | 27 ++++++++++++++++++++------- 2 files changed, 24 insertions(+), 9 deletions(-) diff --git a/package.json b/package.json index 76001cfdb..f8d37c8af 100644 --- a/package.json +++ b/package.json @@ -14,7 +14,7 @@ "node": ">=8.0.0" }, "scripts": { - "build": "microbundle --external http,https,stream,zlib --name fetch --target node --format es,cjs", + "build": "microbundle --external http,https,stream,zlib,pump --name fetch --target node --format es,cjs", "prepare": "npm run build", "test": "cross-env BABEL_ENV=test mocha --require @babel/register --throw-deprecation test/test.js", "report": "cross-env BABEL_ENV=coverage nyc --reporter lcov --reporter text mocha -R spec test/test.js", @@ -60,7 +60,9 @@ "whatwg-url": "^7.0.0", "xo": "^0.24.0" }, - "dependencies": {}, + "dependencies": { + "pump": "^3.0.0" + }, "optionalDependencies": { "encoding": "^0.1.12" }, diff --git a/src/index.js b/src/index.js index 8cbc5b069..434a77042 100644 --- a/src/index.js +++ b/src/index.js @@ -1,4 +1,3 @@ - /** * Index.js * @@ -12,6 +11,7 @@ import http from 'http'; import https from 'https'; import zlib from 'zlib'; import Stream from 'stream'; +import pump from 'pump'; import Body, {writeToStream, getTotalBytes} from './body'; import Response from './response'; @@ -194,7 +194,10 @@ export default function fetch(url, opts) { signal.removeEventListener('abort', abortAndFinalize); } }); - let body = res.pipe(new PassThrough()); + + let body = pump(res, new PassThrough(), error => { + reject(error); + }); const responseOptions = { url: request.url, @@ -235,7 +238,9 @@ export default function fetch(url, opts) { // For gzip if (codings === 'gzip' || codings === 'x-gzip') { - body = body.pipe(zlib.createGunzip(zlibOptions)); + body = pump(body, zlib.createGunzip(zlibOptions), error => { + reject(error); + }); response = new Response(body, responseOptions); resolve(response); return; @@ -245,13 +250,19 @@ export default function fetch(url, opts) { if (codings === 'deflate' || codings === 'x-deflate') { // Handle the infamous raw deflate response from old servers // a hack for old IIS and Apache servers - const raw = res.pipe(new PassThrough()); + const raw = pump(res, new PassThrough(), error => { + reject(error); + }); raw.once('data', chunk => { // See http://stackoverflow.com/questions/37519828 if ((chunk[0] & 0x0F) === 0x08) { - body = body.pipe(zlib.createInflate()); + body = pump(body, zlib.createInflate(), error => { + reject(error); + }); } else { - body = body.pipe(zlib.createInflateRaw()); + body = pump(body, zlib.createInflateRaw(), error => { + reject(error); + }); } response = new Response(body, responseOptions); @@ -262,7 +273,9 @@ export default function fetch(url, opts) { // For br if (codings === 'br' && typeof zlib.createBrotliDecompress === 'function') { - body = body.pipe(zlib.createBrotliDecompress()); + body = pump(body, zlib.createBrotliDecompress(), error => { + reject(error); + }); response = new Response(body, responseOptions); resolve(response); return; From 672896a9196fb8bbc16d453e1d3c637d627a7b53 Mon Sep 17 00:00:00 2001 From: Richie Bendall Date: Sat, 21 Sep 2019 19:16:15 +1200 Subject: [PATCH 029/157] chore: Remove useless check Signed-off-by: Richie Bendall --- src/utils/is.js | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/src/utils/is.js b/src/utils/is.js index 2c585ee7e..5de070aa7 100644 --- a/src/utils/is.js +++ b/src/utils/is.js @@ -14,8 +14,8 @@ const NAME = Symbol.toStringTag; * @return {boolean} */ export function isURLSearchParams(obj) { - return ( - typeof obj === 'object' && + return ( + typeof obj === 'object' && typeof obj.append === 'function' && typeof obj.delete === 'function' && typeof obj.get === 'function' && @@ -24,7 +24,7 @@ export function isURLSearchParams(obj) { typeof obj.set === 'function' && typeof obj.sort === 'function' && obj[NAME] === 'URLSearchParams' - ); + ); } /** @@ -34,14 +34,14 @@ export function isURLSearchParams(obj) { * @return {boolean} */ export function isBlob(obj) { - return ( - typeof obj === 'object' && + return ( + typeof obj === 'object' && typeof obj.arrayBuffer === 'function' && typeof obj.type === 'string' && typeof obj.stream === 'function' && typeof obj.constructor === 'function' && /^(Blob|File)$/.test(obj[NAME]) - ); + ); } /** @@ -51,10 +51,9 @@ export function isBlob(obj) { * @return {boolean} */ export function isAbortSignal(obj) { - return ( - obj && + return ( typeof obj === 'object' && obj[NAME] === 'AbortSignal' - ); + ); } From 356210e34c1b525e5ba7ff7235b22c3ff5d7ed32 Mon Sep 17 00:00:00 2001 From: Richie Bendall Date: Sat, 21 Sep 2019 19:25:17 +1200 Subject: [PATCH 030/157] style: Fix lint Signed-off-by: Richie Bendall --- src/utils/is.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/utils/is.js b/src/utils/is.js index 5de070aa7..38bbe7a1d 100644 --- a/src/utils/is.js +++ b/src/utils/is.js @@ -56,4 +56,3 @@ export function isAbortSignal(obj) { obj[NAME] === 'AbortSignal' ); } - From 7330d39e91cf3bd2551a2629dbf76d5f4f2bb8a6 Mon Sep 17 00:00:00 2001 From: Richie Bendall Date: Sat, 21 Sep 2019 19:30:28 +1200 Subject: [PATCH 031/157] style: Fix lint Signed-off-by: Richie Bendall --- src/utils/is.js | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/utils/is.js b/src/utils/is.js index 38bbe7a1d..5e5df309e 100644 --- a/src/utils/is.js +++ b/src/utils/is.js @@ -14,8 +14,8 @@ const NAME = Symbol.toStringTag; * @return {boolean} */ export function isURLSearchParams(obj) { - return ( - typeof obj === 'object' && + return ( + typeof obj === 'object' && typeof obj.append === 'function' && typeof obj.delete === 'function' && typeof obj.get === 'function' && @@ -24,7 +24,7 @@ export function isURLSearchParams(obj) { typeof obj.set === 'function' && typeof obj.sort === 'function' && obj[NAME] === 'URLSearchParams' - ); + ); } /** @@ -34,14 +34,14 @@ export function isURLSearchParams(obj) { * @return {boolean} */ export function isBlob(obj) { - return ( - typeof obj === 'object' && + return ( + typeof obj === 'object' && typeof obj.arrayBuffer === 'function' && typeof obj.type === 'string' && typeof obj.stream === 'function' && typeof obj.constructor === 'function' && /^(Blob|File)$/.test(obj[NAME]) - ); + ); } /** @@ -51,8 +51,8 @@ export function isBlob(obj) { * @return {boolean} */ export function isAbortSignal(obj) { - return ( - typeof obj === 'object' && + return ( + typeof obj === 'object' && obj[NAME] === 'AbortSignal' - ); + ); } From 5c407c50172cd643c9a40749076510864c41972f Mon Sep 17 00:00:00 2001 From: Richie Bendall Date: Sat, 21 Sep 2019 20:12:08 +1200 Subject: [PATCH 032/157] refactor: Modernise code Signed-off-by: Richie Bendall --- package.json | 2 -- src/abort-error.js | 19 +++++++++---------- src/blob.js | 23 ++++++++++------------- src/body.js | 4 ++-- src/fetch-error.js | 27 +++++++++++++-------------- src/headers.js | 2 +- src/request.js | 5 +++-- 7 files changed, 38 insertions(+), 44 deletions(-) diff --git a/package.json b/package.json index f8d37c8af..f9ca2ccc3 100644 --- a/package.json +++ b/package.json @@ -81,13 +81,11 @@ "no-multi-assign": 0, "complexity": 0, "unicorn/prefer-spread": 0, - "prefer-object-spread": 0, "promise/prefer-await-to-then": 0, "no-mixed-operators": 0, "eqeqeq": 0, "no-eq-null": 0, "no-negated-condition": 0, - "prefer-rest-params": 0, "node/no-deprecated-api": 1 }, "ignores": [ diff --git a/src/abort-error.js b/src/abort-error.js index 8e39df204..ce438a7e9 100644 --- a/src/abort-error.js +++ b/src/abort-error.js @@ -10,16 +10,15 @@ * @param String message Error message for human * @return AbortError */ -export default function AbortError(message) { - Error.call(this, message); +export default class AbortError extends Error { + constructor(message) { + super(message); - this.type = 'aborted'; - this.message = message; + this.type = 'aborted'; + this.message = message; + this.name = 'AbortError'; - // Hide custom error implementation details from end-users - Error.captureStackTrace(this, this.constructor); + // Hide custom error implementation details from end-users + Error.captureStackTrace(this, this.constructor); + } } - -AbortError.prototype = Object.create(Error.prototype); -AbortError.prototype.constructor = AbortError; -AbortError.prototype.name = 'AbortError'; diff --git a/src/blob.js b/src/blob.js index ee296c82d..2963b36cd 100644 --- a/src/blob.js +++ b/src/blob.js @@ -10,21 +10,18 @@ export const BUFFER = Symbol('buffer'); const TYPE = Symbol('type'); export default class Blob { - constructor() { + constructor(...args) { this[TYPE] = ''; - const blobParts = arguments[0]; - const options = arguments[1]; + const blobParts = args[0]; + const options = args[1]; const buffers = []; /* eslint-disable-next-line no-unused-vars */ let size = 0; if (blobParts) { - const a = blobParts; - const length = Number(a.length); - for (let i = 0; i < length; i++) { - const element = a[i]; + blobParts.forEach(element => { let buffer; if (element instanceof Buffer) { buffer = element; @@ -40,7 +37,7 @@ export default class Blob { size += buffer.length; buffers.push(buffer); - } + }); } this[BUFFER] = Buffer.concat(buffers); @@ -71,7 +68,7 @@ export default class Blob { stream() { const readable = new Readable(); - readable._read = () => {}; + readable._read = () => { }; readable.push(this[BUFFER]); readable.push(null); return readable; @@ -81,11 +78,11 @@ export default class Blob { return '[object Blob]'; } - slice() { + slice(...args) { const {size} = this; - const start = arguments[0]; - const end = arguments[1]; + const start = args[0]; + const end = args[1]; let relativeStart; let relativeEnd; @@ -112,7 +109,7 @@ export default class Blob { relativeStart, relativeStart + span ); - const blob = new Blob([], {type: arguments[2]}); + const blob = new Blob([], {type: args[2]}); blob[BUFFER] = slicedBuffer; return blob; } diff --git a/src/body.js b/src/body.js index 3dde7f776..66fc63fe9 100644 --- a/src/body.js +++ b/src/body.js @@ -91,7 +91,7 @@ Body.prototype = { * @return Promise */ arrayBuffer() { - return consumeBody.call(this).then(buf => buf.buffer.slice(buf.byteOffset, buf.byteOffset + buf.byteLength)); + return consumeBody.call(this).then(({buffer, byteOffset, byteLength}) => buffer.slice(byteOffset, byteOffset + byteLength)); }, /** @@ -166,7 +166,7 @@ Object.defineProperties(Body.prototype, { text: {enumerable: true} }); -Body.mixIn = function (proto) { +Body.mixIn = proto => { for (const name of Object.getOwnPropertyNames(Body.prototype)) { // istanbul ignore else: future proof if (!(name in proto)) { diff --git a/src/fetch-error.js b/src/fetch-error.js index 8642f9d51..00f0580d1 100644 --- a/src/fetch-error.js +++ b/src/fetch-error.js @@ -13,21 +13,20 @@ * @param String systemError For Node.js system error * @return FetchError */ -export default function FetchError(message, type, systemError) { - Error.call(this, message); +export default class FetchError extends Error { + constructor(message, type, systemError) { + super(message); - this.message = message; - this.type = type; + this.message = message; + this.type = type; + this.name = 'FetchError'; - // When err.type is `system`, err.code contains system error code - if (systemError) { - this.code = this.errno = systemError.code; - } + // When err.type is `system`, err.code contains system error code + if (systemError) { + this.code = this.errno = systemError.code; + } - // Hide custom error implementation details from end-users - Error.captureStackTrace(this, this.constructor); + // Hide custom error implementation details from end-users + Error.captureStackTrace(this, this.constructor); + } } - -FetchError.prototype = Object.create(Error.prototype); -FetchError.prototype.constructor = FetchError; -FetchError.prototype.name = 'FetchError'; diff --git a/src/headers.js b/src/headers.js index 7df601e13..16191703c 100644 --- a/src/headers.js +++ b/src/headers.js @@ -338,7 +338,7 @@ Object.defineProperty(HeadersIteratorPrototype, Symbol.toStringTag, { * @return Object */ export function exportNodeCompatibleHeaders(headers) { - const obj = Object.assign({__proto__: null}, headers[MAP]); + const obj = {__proto__: null, ...headers[MAP]}; // Http.request() only supports string as Host header. This hack makes // specifying custom Host header possible. diff --git a/src/request.js b/src/request.js index 63eaa4211..d42273324 100644 --- a/src/request.js +++ b/src/request.js @@ -242,9 +242,10 @@ export function getNodeRequestOptions(request) { // HTTP-network fetch step 4.2 // chunked encoding is handled by Node.js - return Object.assign({}, parsedURL, { + return { + ...parsedURL, method: request.method, headers: exportNodeCompatibleHeaders(headers), agent - }); + }; } From a6fcfd9703c0bf1c45e9665dcb77c8329404b83d Mon Sep 17 00:00:00 2001 From: Richie Bendall Date: Sat, 21 Sep 2019 20:17:19 +1200 Subject: [PATCH 033/157] chore: Ensure all files are properly included Signed-off-by: Richie Bendall --- package.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index f9ca2ccc3..e309cbcd8 100644 --- a/package.json +++ b/package.json @@ -6,9 +6,9 @@ "module": "dist/index.mjs", "types": "types/index.d.ts", "files": [ - "src/*", - "dist/*", - "types/*.d.ts" + "src/**/*", + "dist/**/*", + "types/**/*.d.ts" ], "engines": { "node": ">=8.0.0" From 6a1f5c6e8cd604480971fb2e9c79e970a1d8851e Mon Sep 17 00:00:00 2001 From: Richie Bendall Date: Sat, 21 Sep 2019 20:25:55 +1200 Subject: [PATCH 034/157] chore: Update deps and utf8 should be in dependencies Signed-off-by: Richie Bendall --- package.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index e309cbcd8..080e3f37a 100644 --- a/package.json +++ b/package.json @@ -14,7 +14,7 @@ "node": ">=8.0.0" }, "scripts": { - "build": "microbundle --external http,https,stream,zlib,pump --name fetch --target node --format es,cjs", + "build": "microbundle --external http,https,stream,zlib,pump,utf8 --name fetch --target node --format es,cjs", "prepare": "npm run build", "test": "cross-env BABEL_ENV=test mocha --require @babel/register --throw-deprecation test/test.js", "report": "cross-env BABEL_ENV=coverage nyc --reporter lcov --reporter text mocha -R spec test/test.js", @@ -46,8 +46,8 @@ "chai-as-promised": "^7.1.1", "chai-iterator": "^3.0.2", "chai-string": "^1.5.0", - "codecov": "^3.5.0", - "cross-env": "^5.2.1", + "codecov": "^3.6.1", + "cross-env": "^6.0.0", "form-data": "^2.5.1", "microbundle": "^0.11.0", "mocha": "^6.2.0", @@ -56,12 +56,12 @@ "promise": "^8.0.3", "resumer": "0.0.0", "string-to-arraybuffer": "^1.0.2", - "utf8": "^3.0.0", "whatwg-url": "^7.0.0", "xo": "^0.24.0" }, "dependencies": { - "pump": "^3.0.0" + "pump": "^3.0.0", + "utf8": "^3.0.0" }, "optionalDependencies": { "encoding": "^0.1.12" From 697b75c01cf21ba54b6e2bb8e6594b41309fdb01 Mon Sep 17 00:00:00 2001 From: Richie Bendall Date: Sat, 21 Sep 2019 20:47:10 +1200 Subject: [PATCH 035/157] test: Drop Node v4 from tests Signed-off-by: Richie Bendall --- test/test.js | 17 +---------------- 1 file changed, 1 insertion(+), 16 deletions(-) diff --git a/test/test.js b/test/test.js index a7fe33df3..12d5f52a2 100644 --- a/test/test.js +++ b/test/test.js @@ -1205,13 +1205,6 @@ describe('node-fetch', () => { }); it('should allow POST request with ArrayBuffer body from a VM context', function () { - // TODO: Node.js v4 doesn't support ArrayBuffer from other contexts, so we skip this test, drop this check once Node.js v4 support is not needed - try { - Buffer.from(new VMArrayBuffer()); - } catch (error) { - this.skip(); - } - const url = `${base}inspect`; const opts = { method: 'POST', @@ -1257,13 +1250,6 @@ describe('node-fetch', () => { }); it('should allow POST request with ArrayBufferView (Uint8Array) body from a VM context', function () { - // TODO: Node.js v4 doesn't support ArrayBufferView from other contexts, so we skip this test, drop this check once Node.js v4 support is not needed - try { - Buffer.from(new VMArrayBuffer()); - } catch (error) { - this.skip(); - } - const url = `${base}inspect`; const opts = { method: 'POST', @@ -1278,8 +1264,7 @@ describe('node-fetch', () => { }); }); - // TODO: Node.js v4 doesn't support necessary Buffer API, so we skip this test, drop this check once Node.js v4 support is not needed - (Buffer.from.length === 3 ? it : it.skip)('should allow POST request with ArrayBufferView (Uint8Array, offset, length) body', () => { + it('should allow POST request with ArrayBufferView (Uint8Array, offset, length) body', () => { const url = `${base}inspect`; const opts = { method: 'POST', From 0adb99e621139e051b9df4a6fba24d938a3c4545 Mon Sep 17 00:00:00 2001 From: Richie Bendall Date: Mon, 23 Sep 2019 17:41:12 +1200 Subject: [PATCH 036/157] test: Modernise code Signed-off-by: Richie Bendall --- .nycrc | 5 ----- package.json | 7 +++++++ test/test.js | 23 +++++++++++------------ 3 files changed, 18 insertions(+), 17 deletions(-) delete mode 100644 .nycrc diff --git a/.nycrc b/.nycrc deleted file mode 100644 index 65327e73b..000000000 --- a/.nycrc +++ /dev/null @@ -1,5 +0,0 @@ -{ - "require": ["@babel/register"], - "sourceMap": false, - "instrument": false -} diff --git a/package.json b/package.json index 080e3f37a..fc22e8558 100644 --- a/package.json +++ b/package.json @@ -104,5 +104,12 @@ } ] ] + }, + "nyc": { + "require": [ + "@babel/register" + ], + "sourceMap": false, + "instrument": false } } diff --git a/test/test.js b/test/test.js index 12d5f52a2..00bdf053b 100644 --- a/test/test.js +++ b/test/test.js @@ -28,17 +28,16 @@ import Body, { getTotalBytes, extractContentType } from '../src/body'; import Blob from '../src/blob'; import TestServer from './server'; -const { spawn } = require('child_process'); -const http = require('http'); -const fs = require('fs'); -const path = require('path'); -const stream = require('stream'); -const { parse: parseURL, URLSearchParams } = require('url'); -const { lookup } = require('dns'); -const vm = require('vm'); +import { spawn } from 'child_process'; +import * as http from 'http'; +import * as fs from 'fs'; +import * as path from 'path'; +import * as stream from 'stream'; +import { parse as parseURL, URLSearchParams } from 'url'; +import { lookup } from 'dns'; +import vm from 'vm'; const { - ArrayBuffer: VMArrayBuffer, Uint8Array: VMUint8Array } = vm.runInNewContext('this'); @@ -124,7 +123,7 @@ describe('node-fetch', () => { return expect(fetch(url)).to.eventually.be.rejectedWith(TypeError, 'Only HTTP(S) protocols are supported'); }); - it('should reject with error on network failure', () => { + (process.platform !== "win32" ? it : it.skip)('should reject with error on network failure', () => { const url = 'http://localhost:50000/'; return expect(fetch(url)).to.eventually.be.rejected .and.be.an.instanceOf(FetchError) @@ -1354,7 +1353,7 @@ describe('node-fetch', () => { }); }); - it('should allow POST request with form-data using stream as body', () => { + (process.platform !== "win32" ? it : it.skip)('should allow POST request with form-data using stream as body', () => { const form = new FormData(); form.append('my_field', fs.createReadStream(path.join(__dirname, 'dummy.txt'))); @@ -1371,7 +1370,7 @@ describe('node-fetch', () => { expect(res.headers['content-type']).to.startWith('multipart/form-data;boundary='); expect(res.headers['content-length']).to.be.undefined; expect(res.body).to.contain('my_field='); - }); + }) }); it('should allow POST request with form-data as body and custom headers', () => { From 9134a0063e0486ac6953e0c6afb02385bac4ace0 Mon Sep 17 00:00:00 2001 From: Richie Bendall Date: Mon, 23 Sep 2019 23:08:04 +1200 Subject: [PATCH 037/157] chore: Move errors to seperate directory Signed-off-by: Richie Bendall --- src/body.js | 2 +- src/{ => errors}/abort-error.js | 2 ++ src/{ => errors}/fetch-error.js | 1 - src/index.js | 4 ++-- test/test.js | 2 +- 5 files changed, 6 insertions(+), 5 deletions(-) rename src/{ => errors}/abort-error.js (80%) rename src/{ => errors}/fetch-error.js (99%) diff --git a/src/body.js b/src/body.js index 66fc63fe9..2453e124e 100644 --- a/src/body.js +++ b/src/body.js @@ -8,7 +8,7 @@ import Stream from 'stream'; import Blob, {BUFFER} from './blob'; -import FetchError from './fetch-error'; +import FetchError from './errors/fetch-error'; import {isBlob, isURLSearchParams} from './utils/is'; let convert; diff --git a/src/abort-error.js b/src/errors/abort-error.js similarity index 80% rename from src/abort-error.js rename to src/errors/abort-error.js index ce438a7e9..f3d8f096b 100644 --- a/src/abort-error.js +++ b/src/errors/abort-error.js @@ -8,6 +8,8 @@ * 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 { diff --git a/src/fetch-error.js b/src/errors/fetch-error.js similarity index 99% rename from src/fetch-error.js rename to src/errors/fetch-error.js index 00f0580d1..7c36df65d 100644 --- a/src/fetch-error.js +++ b/src/errors/fetch-error.js @@ -1,4 +1,3 @@ - /** * Fetch-error.js * diff --git a/src/index.js b/src/index.js index 434a77042..9f1d46d12 100644 --- a/src/index.js +++ b/src/index.js @@ -17,8 +17,8 @@ import Body, {writeToStream, getTotalBytes} from './body'; import Response from './response'; import Headers, {createHeadersLenient} from './headers'; import Request, {getNodeRequestOptions} from './request'; -import FetchError from './fetch-error'; -import AbortError from './abort-error'; +import FetchError from './errors/fetch-error'; +import AbortError from './errors/abort-error'; // Fix an issue where "PassThrough", "resolve" aren't a named export for node <10 const {PassThrough} = Stream; diff --git a/test/test.js b/test/test.js index 00bdf053b..449ce7610 100644 --- a/test/test.js +++ b/test/test.js @@ -20,7 +20,7 @@ import fetch, { Request, Response } from '../src'; -import FetchErrorOrig from '../src/fetch-error'; +import FetchErrorOrig from '../src/errors/fetch-error'; import HeadersOrig, { createHeadersLenient } from '../src/headers'; import RequestOrig from '../src/request'; import ResponseOrig from '../src/response'; From e663ba7c244eecf87447a5277e2f520590e56aa9 Mon Sep 17 00:00:00 2001 From: Richie Bendall Date: Mon, 23 Sep 2019 23:49:21 +1200 Subject: [PATCH 038/157] refactor: Add fetch-blob (#678) --- package.json | 3 +- src/blob.js | 129 --------------------------------------------------- src/body.js | 15 ++---- test/test.js | 2 +- 4 files changed, 8 insertions(+), 141 deletions(-) delete mode 100644 src/blob.js diff --git a/package.json b/package.json index fc22e8558..55826ae24 100644 --- a/package.json +++ b/package.json @@ -14,7 +14,7 @@ "node": ">=8.0.0" }, "scripts": { - "build": "microbundle --external http,https,stream,zlib,pump,utf8 --name fetch --target node --format es,cjs", + "build": "microbundle --external http,https,stream,zlib,pump,utf8,fetch-blob --name fetch --target node --format es,cjs", "prepare": "npm run build", "test": "cross-env BABEL_ENV=test mocha --require @babel/register --throw-deprecation test/test.js", "report": "cross-env BABEL_ENV=coverage nyc --reporter lcov --reporter text mocha -R spec test/test.js", @@ -60,6 +60,7 @@ "xo": "^0.24.0" }, "dependencies": { + "fetch-blob": "^1.0.4", "pump": "^3.0.0", "utf8": "^3.0.0" }, diff --git a/src/blob.js b/src/blob.js deleted file mode 100644 index 2963b36cd..000000000 --- a/src/blob.js +++ /dev/null @@ -1,129 +0,0 @@ -// Based on https://github.com/tmpvar/jsdom/blob/aa85b2abf07766ff7bf5c1f6daafb3726f2f2db5/lib/jsdom/living/blob.js -// (MIT licensed) - -import Stream from 'stream'; - -// Fix for "Readable" isn't a named export issue -const {Readable} = Stream; - -export const BUFFER = Symbol('buffer'); -const TYPE = Symbol('type'); - -export default class Blob { - constructor(...args) { - this[TYPE] = ''; - - const blobParts = args[0]; - const options = args[1]; - - const buffers = []; - /* eslint-disable-next-line no-unused-vars */ - let size = 0; - - if (blobParts) { - blobParts.forEach(element => { - let buffer; - if (element instanceof Buffer) { - buffer = element; - } else if (ArrayBuffer.isView(element)) { - buffer = Buffer.from(element.buffer, element.byteOffset, element.byteLength); - } else if (element instanceof ArrayBuffer) { - buffer = Buffer.from(element); - } else if (element instanceof Blob) { - buffer = element[BUFFER]; - } else { - buffer = Buffer.from(typeof element === 'string' ? element : String(element)); - } - - size += buffer.length; - buffers.push(buffer); - }); - } - - this[BUFFER] = Buffer.concat(buffers); - - const type = options && options.type !== undefined && String(options.type).toLowerCase(); - if (type && !/[^\u0020-\u007E]/.test(type)) { - this[TYPE] = type; - } - } - - get size() { - return this[BUFFER].length; - } - - get type() { - return this[TYPE]; - } - - text() { - return Promise.resolve(this[BUFFER].toString()); - } - - arrayBuffer() { - const buf = this[BUFFER]; - const ab = buf.buffer.slice(buf.byteOffset, buf.byteOffset + buf.byteLength); - return Promise.resolve(ab); - } - - stream() { - const readable = new Readable(); - readable._read = () => { }; - readable.push(this[BUFFER]); - readable.push(null); - return readable; - } - - toString() { - return '[object Blob]'; - } - - slice(...args) { - const {size} = this; - - const start = args[0]; - const end = args[1]; - let relativeStart; - let relativeEnd; - - if (start === undefined) { - relativeStart = 0; - } else if (start < 0) { - relativeStart = Math.max(size + start, 0); - } else { - relativeStart = Math.min(start, size); - } - - if (end === undefined) { - relativeEnd = size; - } else if (end < 0) { - relativeEnd = Math.max(size + end, 0); - } else { - relativeEnd = Math.min(end, size); - } - - const span = Math.max(relativeEnd - relativeStart, 0); - - const buffer = this[BUFFER]; - const slicedBuffer = buffer.slice( - relativeStart, - relativeStart + span - ); - const blob = new Blob([], {type: args[2]}); - blob[BUFFER] = slicedBuffer; - return blob; - } -} - -Object.defineProperties(Blob.prototype, { - size: {enumerable: true}, - type: {enumerable: true}, - slice: {enumerable: true} -}); - -Object.defineProperty(Blob.prototype, Symbol.toStringTag, { - value: 'Blob', - writable: false, - enumerable: false, - configurable: true -}); diff --git a/src/body.js b/src/body.js index 2453e124e..b0297a51f 100644 --- a/src/body.js +++ b/src/body.js @@ -7,7 +7,7 @@ import Stream from 'stream'; -import Blob, {BUFFER} from './blob'; +import Blob from 'fetch-blob'; import FetchError from './errors/fetch-error'; import {isBlob, isURLSearchParams} from './utils/is'; @@ -101,15 +101,10 @@ Body.prototype = { */ blob() { const ct = this.headers && this.headers.get('content-type') || ''; - return consumeBody.call(this).then(buf => Object.assign( - // Prevent copying - new Blob([], { - type: ct.toLowerCase() - }), - { - [BUFFER]: buf - } - )); + return consumeBody.call(this).then(buf => new Blob([], { + type: ct.toLowerCase(), + buffer: buf + })); }, /** diff --git a/test/test.js b/test/test.js index 449ce7610..a6978fcfb 100644 --- a/test/test.js +++ b/test/test.js @@ -25,7 +25,7 @@ import HeadersOrig, { createHeadersLenient } from '../src/headers'; import RequestOrig from '../src/request'; import ResponseOrig from '../src/response'; import Body, { getTotalBytes, extractContentType } from '../src/body'; -import Blob from '../src/blob'; +import Blob from 'fetch-blob'; import TestServer from './server'; import { spawn } from 'child_process'; From e8c62bac4b5bd69baa1f4318d6225f63e5ad2b58 Mon Sep 17 00:00:00 2001 From: Richie Bendall Date: Tue, 24 Sep 2019 20:55:04 +1200 Subject: [PATCH 039/157] feat: Migrate data uri integration Signed-off-by: Richie Bendall --- src/index.js | 16 +++++++++++++++ test/test.js | 58 ++++++++++++++++++++++++++++++---------------------- 2 files changed, 50 insertions(+), 24 deletions(-) diff --git a/src/index.js b/src/index.js index 9f1d46d12..90dc1fc85 100644 --- a/src/index.js +++ b/src/index.js @@ -37,6 +37,22 @@ export default function fetch(url, opts) { 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)?,[a-z0-9!$&',()*+,;=\-._~:@/?%\s]*\s*$/i; + + // If valid data uri + if (dataUriRegex.test(url)) { + const data = Buffer.from(url.split(',')[1], 'base64'); + const res = new Response(data.body, {headers: {'Content-Type': data.mimeType || url.match(dataUriRegex)[1] || 'text/plain'}}); + return fetch.Promise.resolve(res); + } + + // If invalid data uri + if (url.toString().startsWith('data:')) { + const request = new Request(url, opts); + return fetch.Promise.reject(new FetchError(`[${request.method}] ${request.url} invalid URL`, 'system')); + } + Body.Promise = fetch.Promise; // Wrap http.request into fetch diff --git a/test/test.js b/test/test.js index a6978fcfb..cfa1e5452 100644 --- a/test/test.js +++ b/test/test.js @@ -51,12 +51,6 @@ chai.use(chaiIterator); chai.use(chaiString); const { expect } = chai; -const supportToString = ({ - [Symbol.toStringTag]: 'z' -}).toString() === '[object z]'; - -const supportStreamDestroy = 'destroy' in stream.Readable.prototype; - const local = new TestServer(); const base = `http://${local.hostname}:${local.port}/`; @@ -68,6 +62,8 @@ after(done => { local.stop(done); }); +const itIf = val => val ? it : it.skip + describe('node-fetch', () => { it('should return a promise', () => { const url = `${base}hello`; @@ -102,7 +98,7 @@ describe('node-fetch', () => { expect(Request).to.equal(RequestOrig); }); - (supportToString ? it : it.skip)('should support proper toString output for Headers, Response and Request objects', () => { + it('should support proper toString output for Headers, Response and Request objects', () => { expect(new Headers().toString()).to.equal('[object Headers]'); expect(new Response().toString()).to.equal('[object Response]'); expect(new Request(base).toString()).to.equal('[object Request]'); @@ -123,7 +119,7 @@ describe('node-fetch', () => { return expect(fetch(url)).to.eventually.be.rejectedWith(TypeError, 'Only HTTP(S) protocols are supported'); }); - (process.platform !== "win32" ? it : it.skip)('should reject with error on network failure', () => { + itIf(process.platform !== "win32")('should reject with error on network failure', () => { const url = 'http://localhost:50000/'; return expect(fetch(url)).to.eventually.be.rejected .and.be.an.instanceOf(FetchError) @@ -1040,7 +1036,7 @@ describe('node-fetch', () => { }); }); - (supportStreamDestroy ? it : it.skip)('should cancel request body of type Stream with AbortError when aborted', () => { + it('should cancel request body of type Stream with AbortError when aborted', () => { const controller = new AbortController(); const body = new stream.Readable({ objectMode: true }); body._read = () => { }; @@ -1070,20 +1066,6 @@ describe('node-fetch', () => { return result; }); - (supportStreamDestroy ? it.skip : it)('should immediately reject when attempting to cancel streamed Requests in node < 8', () => { - const controller = new AbortController(); - const body = new stream.Readable({ objectMode: true }); - body._read = () => { }; - const promise = fetch( - `${base}slow`, - { signal: controller.signal, body, method: 'POST' } - ); - - return expect(promise).to.eventually.be.rejected - .and.be.an.instanceof(Error) - .and.have.property('message').includes('not supported'); - }); - it('should throw a TypeError if a signal is not of type AbortSignal', () => { return Promise.all([ expect(fetch(`${base}inspect`, { signal: {} })) @@ -1353,7 +1335,7 @@ describe('node-fetch', () => { }); }); - (process.platform !== "win32" ? it : it.skip)('should allow POST request with form-data using stream as body', () => { + itIf(process.platform !== "win32")('should allow POST request with form-data using stream as body', () => { const form = new FormData(); form.append('my_field', fs.createReadStream(path.join(__dirname, 'dummy.txt'))); @@ -2889,4 +2871,32 @@ describe('external encoding', () => { }); }); }); + + describe('data uri', () => { + it('should accept data uri', () => { + return fetch('data:image/gif;base64,R0lGODlhAQABAIAAAAUEBAAAACwAAAAAAQABAAACAkQBADs=').then(r => { + expect(r.status).to.equal(200); + expect(r.headers.get('Content-Type')).to.equal("image/gif") + + return r.buffer().then(b => { + expect(b).to.be.an.instanceOf(Buffer) + }); + }); + }); + + it('should accept data uri of plain text', () => { + return fetch("data:,Hello%20World!").then(r => { + expect(r.status).to.equal(200); + expect(r.headers.get('Content-Type')).to.equal("text/plain") + return r.text().then(t => expect(t).to.equal("Hello World!")) + }); + }) + + it('should reject invalid data uri', () => { + return fetch('data:@@@@').catch(e => { + expect(e).to.exist; + expect(e.message).to.include('invalid URL') + }); + }); + }); }); From 48d45ec5a623dd859add2c1ea05507e77ec3b404 Mon Sep 17 00:00:00 2001 From: Antoni Kepinski Date: Tue, 24 Sep 2019 21:00:27 +0200 Subject: [PATCH 040/157] Allow setting custom highWaterMark via node-fetch options (#386) (#671) * Expose highWaterMark option to body clone function * Add highWaterMark to responseOptions * Add highWaterMark as node-fetch-only option * a way to silently pass highWaterMark to clone * Chai helper * Server helper * Tests * Remove debug comments * Document highWaterMark option --- README.md | 85 +++++++++++++++++++++++++++----------------- src/body.js | 9 ++--- src/index.js | 3 +- src/request.js | 1 + src/response.js | 9 +++-- test/chai-timeout.js | 17 +++++++++ test/server.js | 14 ++++++++ test/test.js | 67 ++++++++++++++++++++++++++++++++++ 8 files changed, 165 insertions(+), 40 deletions(-) create mode 100644 test/chai-timeout.js diff --git a/README.md b/README.md index cb1990120..1fc245be1 100644 --- a/README.md +++ b/README.md @@ -12,37 +12,54 @@ A light-weight module that brings `window.fetch` to Node.js -- [Motivation](#motivation) -- [Features](#features) -- [Difference from client-side fetch](#difference-from-client-side-fetch) -- [Installation](#installation) -- [Loading and configuring the module](#loading-and-configuring-the-module) -- [Common Usage](#common-usage) - - [Plain text or HTML](#plain-text-or-html) - - [JSON](#json) - - [Simple Post](#simple-post) - - [Post with JSON](#post-with-json) - - [Post with form parameters](#post-with-form-parameters) - - [Handling exceptions](#handling-exceptions) - - [Handling client and server errors](#handling-client-and-server-errors) -- [Advanced Usage](#advanced-usage) - - [Streams](#streams) - - [Buffer](#buffer) - - [Accessing Headers and other Meta data](#accessing-headers-and-other-meta-data) - - [Extract Set-Cookie Header](#extract-set-cookie-header) - - [Post data using a file stream](#post-data-using-a-file-stream) - - [Post with form-data (detect multipart)](#post-with-form-data-detect-multipart) - - [Request cancellation with AbortSignal](#request-cancellation-with-abortsignal) -- [API](#api) +- [node-fetch](#node-fetch) + - [Motivation](#motivation) + - [Features](#features) + - [Difference from client-side fetch](#difference-from-client-side-fetch) + - [Installation](#installation) + - [Loading and configuring the module](#loading-and-configuring-the-module) + - [Common Usage](#common-usage) + - [Plain text or HTML](#plain-text-or-html) + - [JSON](#json) + - [Simple Post](#simple-post) + - [Post with JSON](#post-with-json) + - [Post with form parameters](#post-with-form-parameters) + - [Handling exceptions](#handling-exceptions) + - [Handling client and server errors](#handling-client-and-server-errors) + - [Advanced Usage](#advanced-usage) + - [Streams](#streams) + - [Buffer](#buffer) + - [Accessing Headers and other Meta data](#accessing-headers-and-other-meta-data) + - [Extract Set-Cookie Header](#extract-set-cookie-header) + - [Post data using a file stream](#post-data-using-a-file-stream) + - [Post with form-data (detect multipart)](#post-with-form-data-detect-multipart) + - [Request cancellation with AbortSignal](#request-cancellation-with-abortsignal) + - [API](#api) - [fetch(url[, options])](#fetchurl-options) - [Options](#options) + - [Default Headers](#default-headers) + - [Custom Agent](#custom-agent) - [Class: Request](#class-request) + - [new Request(input[, options])](#new-requestinput-options) - [Class: Response](#class-response) + - [new Response([body[, options]])](#new-responsebody-options) + - [response.ok](#responseok) + - [response.redirected](#responseredirected) - [Class: Headers](#class-headers) + - [new Headers([init])](#new-headersinit) - [Interface: Body](#interface-body) + - [body.body](#bodybody) + - [body.bodyUsed](#bodybodyused) + - [body.arrayBuffer()](#bodyarraybuffer) + - [body.blob()](#bodyblob) + - [body.json()](#bodyjson) + - [body.text()](#bodytext) + - [body.buffer()](#bodybuffer) + - [body.textConverted()](#bodytextconverted) - [Class: FetchError](#class-fetcherror) -- [License](#license) -- [Acknowledgement](#acknowledgement) + - [Class: AbortError](#class-aborterror) + - [Acknowledgement](#acknowledgement) + - [License](#license) @@ -319,17 +336,18 @@ The default values are shown after each option key. { // 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 + 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 - timeout: 0, // req/res timeout in ms, it resets on redirect. 0 to disable (OS limit applies). Signal is recommended instead. - compress: true, // support gzip/deflate content encoding. false to disable - size: 0, // maximum response body size in bytes. 0 to disable - agent: null // http(s).Agent instance or function that returns an instance (see below) + follow: 20, // maximum redirect count. 0 to not follow redirect + timeout: 0, // req/res timeout in ms, it resets on redirect. 0 to disable (OS limit applies). Signal is recommended instead. + compress: true, // support gzip/deflate content encoding. false to disable + size: 0, // maximum response body size in bytes. 0 to disable + agent: null // http(s).Agent instance 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. } ``` @@ -402,6 +420,7 @@ The following node-fetch extension properties are provided: - `compress` - `counter` - `agent` +- `highWaterMark` See [options](#fetch-options) for exact meaning of these extensions. diff --git a/src/body.js b/src/body.js index b0297a51f..75482f56d 100644 --- a/src/body.js +++ b/src/body.js @@ -340,10 +340,11 @@ function convertBody(buffer, headers) { /** * Clone body given Res/Req instance * - * @param Mixed instance Response or Request instance + * @param Mixed instance Response or Request instance + * @param String highWaterMark highWaterMark for both PassThrough body streams * @return Mixed */ -export function clone(instance) { +export function clone(instance, highWaterMark) { let p1; let p2; let {body} = instance; @@ -357,8 +358,8 @@ export function clone(instance) { // note: we can't clone the form-data object without having it as a dependency if ((body instanceof Stream) && (typeof body.getBoundary !== 'function')) { // Tee instance body - p1 = new PassThrough(); - p2 = new PassThrough(); + p1 = new PassThrough({highWaterMark}); + p2 = new PassThrough({highWaterMark}); body.pipe(p1); body.pipe(p2); // Set instance body to teed body and return the other teed body diff --git a/src/index.js b/src/index.js index 90dc1fc85..dcda64a7b 100644 --- a/src/index.js +++ b/src/index.js @@ -222,7 +222,8 @@ export default function fetch(url, opts) { headers, size: request.size, timeout: request.timeout, - counter: request.counter + counter: request.counter, + highWaterMark: request.highWaterMark }; // HTTP-network fetch step 12.1.1.3 diff --git a/src/request.js b/src/request.js index d42273324..76aee8e48 100644 --- a/src/request.js +++ b/src/request.js @@ -119,6 +119,7 @@ export default class Request { input.compress : true; this.counter = init.counter || input.counter || 0; this.agent = init.agent || input.agent; + this.highWaterMark = init.highWaterMark || input.highWaterMark; } get method() { diff --git a/src/response.js b/src/response.js index 8ec0b9cf8..2dfc3b5a3 100644 --- a/src/response.js +++ b/src/response.js @@ -35,7 +35,8 @@ export default class Response { status, statusText: opts.statusText || '', headers, - counter: opts.counter + counter: opts.counter, + highWaterMark: opts.highWaterMark }; } @@ -66,13 +67,17 @@ export default class Response { return this[INTERNALS].headers; } + get highWaterMark() { + return this[INTERNALS].highWaterMark; + } + /** * Clone this response * * @return Response */ clone() { - return new Response(clone(this), { + return new Response(clone(this, this.highWaterMark), { url: this.url, status: this.status, statusText: this.statusText, diff --git a/test/chai-timeout.js b/test/chai-timeout.js new file mode 100644 index 000000000..cecb9efa9 --- /dev/null +++ b/test/chai-timeout.js @@ -0,0 +1,17 @@ +module.exports = (chai, utils) => { + utils.addProperty(chai.Assertion.prototype, 'timeout', function () { + return new Promise(resolve => { + const timer = setTimeout(() => resolve(true), 150); + this._obj.then(() => { + clearTimeout(timer); + resolve(false); + }); + }).then(timeouted => { + this.assert( + timeouted, + 'expected promise to timeout but it was resolved', + 'expected promise not to timeout but it timed out' + ); + }); + }); +}; diff --git a/test/server.js b/test/server.js index d276a241d..0b0e96530 100644 --- a/test/server.js +++ b/test/server.js @@ -33,9 +33,23 @@ export default class TestServer { this.server.close(cb); } + mockResponse(responseHandler) { + this.server.nextResponseHandler = responseHandler; + return `http://${this.hostname}:${this.port}/mocked`; + } + router(req, res) { const p = parse(req.url).pathname; + if (p === '/mocked') { + if (this.nextResponseHandler) { + this.nextResponseHandler(res); + this.nextResponseHandler = undefined; + } else { + throw new Error('No mocked response. Use \'TestServer.mockResponse()\'.'); + } + } + if (p === '/hello') { res.statusCode = 200; res.setHeader('Content-Type', 'text/plain'); diff --git a/test/test.js b/test/test.js index cfa1e5452..7c7f8ff4c 100644 --- a/test/test.js +++ b/test/test.js @@ -12,6 +12,7 @@ import stringToArrayBuffer from 'string-to-arraybuffer'; import { URL } from 'whatwg-url'; import { AbortController } from 'abortcontroller-polyfill/dist/abortcontroller'; import AbortController2 from 'abort-controller'; +import crypto from 'crypto'; // Test subjects import fetch, { @@ -46,9 +47,12 @@ try { convert = require('encoding').convert; } catch (error) { } +import chaiTimeout from './chai-timeout'; + chai.use(chaiPromised); chai.use(chaiIterator); chai.use(chaiString); +chai.use(chaiTimeout); const { expect } = chai; const local = new TestServer(); @@ -1732,6 +1736,69 @@ describe('node-fetch', () => { ); }); + it('should timeout on cloning response without consuming one of the streams when the second packet size is equal default highWaterMark', function () { + this.timeout(300); + const url = local.mockResponse(res => { + // Observed behavior of TCP packets splitting: + // - response body size <= 65438 → single packet sent + // - response body size > 65438 → multiple packets sent + // Max TCP packet size is 64kB (https://stackoverflow.com/a/2614188/5763764), + // but first packet probably transfers more than the response body. + const firstPacketMaxSize = 65438; + const secondPacketSize = 16 * 1024; // = defaultHighWaterMark + res.end(crypto.randomBytes(firstPacketMaxSize + secondPacketSize)); + }); + return expect( + fetch(url).then(res => res.clone().buffer()) + ).to.timeout; + }); + + it('should timeout on cloning response without consuming one of the streams when the second packet size is equal custom highWaterMark', function () { + this.timeout(300); + const url = local.mockResponse(res => { + const firstPacketMaxSize = 65438; + const secondPacketSize = 10; + res.end(crypto.randomBytes(firstPacketMaxSize + secondPacketSize)); + }); + return expect( + fetch(url, {highWaterMark: 10}).then(res => res.clone().buffer()) + ).to.timeout; + }); + + it('should not timeout on cloning response without consuming one of the streams when the second packet size is less than default highWaterMark', function () { + this.timeout(300); + const url = local.mockResponse(res => { + const firstPacketMaxSize = 65438; + const secondPacketSize = 16 * 1024; // = defaultHighWaterMark + res.end(crypto.randomBytes(firstPacketMaxSize + secondPacketSize - 1)); + }); + return expect( + fetch(url).then(res => res.clone().buffer()) + ).not.to.timeout; + }); + + it('should not timeout on cloning response without consuming one of the streams when the second packet size is less than custom highWaterMark', function () { + this.timeout(300); + const url = local.mockResponse(res => { + const firstPacketMaxSize = 65438; + const secondPacketSize = 10; + res.end(crypto.randomBytes(firstPacketMaxSize + secondPacketSize - 1)); + }); + return expect( + fetch(url, {highWaterMark: 10}).then(res => res.clone().buffer()) + ).not.to.timeout; + }); + + it('should not timeout on cloning response without consuming one of the streams when the response size is double the custom large highWaterMark - 1', function () { + this.timeout(300); + const url = local.mockResponse(res => { + res.end(crypto.randomBytes(2 * 512 * 1024 - 1)); + }); + return expect( + fetch(url, {highWaterMark: 512 * 1024}).then(res => res.clone().buffer()) + ).not.to.timeout; + }); + it('should allow get all responses of a header', () => { const url = `${base}cookie`; return fetch(url).then(res => { From 10cebbc7637da15846c050a415658485f8316e01 Mon Sep 17 00:00:00 2001 From: Antoni Kepinski Date: Tue, 24 Sep 2019 21:22:30 +0200 Subject: [PATCH 041/157] Add TypeScript types for the new highWaterMark option --- types/index.d.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/types/index.d.ts b/types/index.d.ts index f7b2132cc..ce655bba0 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -36,6 +36,7 @@ export class Request extends Body { protocol: string; size: number; timeout: number; + highWaterMark?: number; } export interface RequestInit { @@ -52,6 +53,7 @@ export interface RequestInit { follow?: number; // =20 maximum redirect count. 0 to not follow redirect size?: number; // =0 maximum response body size in bytes. 0 to disable timeout?: number; // =0 req/res timeout in ms, it resets on redirect. 0 to disable (OS limit applies) + highWaterMark?: number; // =16384 the maximum number of bytes to store in the internal buffer before ceasing to read from the underlying resource. // node-fetch does not support mode, cache or credentials options } From af4e44f2add8c35e7531236cbfda61fd73c91e12 Mon Sep 17 00:00:00 2001 From: aeb-sia <50743092+aeb-sia@users.noreply.github.com> Date: Fri, 27 Sep 2019 10:09:44 +0200 Subject: [PATCH 042/157] feat: Include system error in FetchError if one occurs (#654) --- src/errors/fetch-error.js | 3 ++- test/test.js | 19 ++++++++++++++++++- 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/src/errors/fetch-error.js b/src/errors/fetch-error.js index 7c36df65d..28a2ba577 100644 --- a/src/errors/fetch-error.js +++ b/src/errors/fetch-error.js @@ -20,9 +20,10 @@ export default class FetchError extends Error { this.type = type; this.name = 'FetchError'; - // When err.type is `system`, err.code contains system error code + // When err.type is `system`, err.erroredSysCall contains system error and err.code contains system error code if (systemError) { this.code = this.errno = systemError.code; + this.erroredSysCall = systemError; } // Hide custom error implementation details from end-users diff --git a/test/test.js b/test/test.js index 7c7f8ff4c..442cc81f1 100644 --- a/test/test.js +++ b/test/test.js @@ -130,7 +130,24 @@ describe('node-fetch', () => { .and.include({ type: 'system', code: 'ECONNREFUSED', errno: 'ECONNREFUSED' }); }); - it('should resolve into response', () => { + it('error should contain system error if one occurred', function() { + const err = new FetchError('a message', 'system', new Error('an error')); + return expect(err).to.have.property('erroredSysCall'); + }); + + it('error should not contain system error if none occurred', function() { + const err = new FetchError('a message', 'a type'); + return expect(err).to.not.have.property('erroredSysCall'); + }); + + it('system error is extracted from failed requests', function() { + const url = 'http://localhost:50000/'; + return expect(fetch(url)).to.eventually.be.rejected + .and.be.an.instanceOf(FetchError) + .and.have.property('erroredSysCall'); + }) + + it('should resolve into response', function() { const url = `${base}hello`; return fetch(url).then(res => { expect(res).to.be.an.instanceof(Response); From 8dde72402058ae79d9a842cdf6200d3b56a30e77 Mon Sep 17 00:00:00 2001 From: Richie Bendall Date: Fri, 27 Sep 2019 20:25:36 +1200 Subject: [PATCH 043/157] style: Add editorconfig Signed-off-by: Richie Bendall --- .editorconfig | 13 ++ README.md | 368 ++++++++++++++++++++++++++------------------------ package.json | 228 +++++++++++++++---------------- 3 files changed, 319 insertions(+), 290 deletions(-) create mode 100644 .editorconfig diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 000000000..991f40fb5 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,13 @@ +# editorconfig.org + +root = true + +[*] +end_of_line = lf +charset = utf-8 +trim_trailing_whitespace = true +insert_final_newline = true +indent_style = tab + +[*.md] +trim_trailing_whitespace = false diff --git a/README.md b/README.md index 1fc245be1..b5ff6c442 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,4 @@ -node-fetch -========== +# node-fetch [![npm version][npm-image]][npm-url] [![build status][travis-image]][travis-url] @@ -19,26 +18,26 @@ A light-weight module that brings `window.fetch` to Node.js - [Installation](#installation) - [Loading and configuring the module](#loading-and-configuring-the-module) - [Common Usage](#common-usage) - - [Plain text or HTML](#plain-text-or-html) - - [JSON](#json) - - [Simple Post](#simple-post) - - [Post with JSON](#post-with-json) - - [Post with form parameters](#post-with-form-parameters) - - [Handling exceptions](#handling-exceptions) - - [Handling client and server errors](#handling-client-and-server-errors) + - [Plain text or HTML](#plain-text-or-html) + - [JSON](#json) + - [Simple Post](#simple-post) + - [Post with JSON](#post-with-json) + - [Post with form parameters](#post-with-form-parameters) + - [Handling exceptions](#handling-exceptions) + - [Handling client and server errors](#handling-client-and-server-errors) - [Advanced Usage](#advanced-usage) - - [Streams](#streams) - - [Buffer](#buffer) - - [Accessing Headers and other Meta data](#accessing-headers-and-other-meta-data) - - [Extract Set-Cookie Header](#extract-set-cookie-header) - - [Post data using a file stream](#post-data-using-a-file-stream) - - [Post with form-data (detect multipart)](#post-with-form-data-detect-multipart) - - [Request cancellation with AbortSignal](#request-cancellation-with-abortsignal) + - [Streams](#streams) + - [Buffer](#buffer) + - [Accessing Headers and other Meta data](#accessing-headers-and-other-meta-data) + - [Extract Set-Cookie Header](#extract-set-cookie-header) + - [Post data using a file stream](#post-data-using-a-file-stream) + - [Post with form-data (detect multipart)](#post-with-form-data-detect-multipart) + - [Request cancellation with AbortSignal](#request-cancellation-with-abortsignal) - [API](#api) - [fetch(url[, options])](#fetchurl-options) - [Options](#options) - - [Default Headers](#default-headers) - - [Custom Agent](#custom-agent) + - [Default Headers](#default-headers) + - [Custom Agent](#custom-agent) - [Class: Request](#class-request) - [new Request(input[, options])](#new-requestinput-options) - [Class: Response](#class-response) @@ -89,18 +88,21 @@ See Matt Andrews' [isomorphic-fetch](https://github.com/matthew-andrews/isomorph Current stable release (`2.x`) ```sh -$ npm install node-fetch --save +npm install node-fetch --save ``` ## Loading and configuring the module + We suggest you load the module via `require`, pending the stabalizing of es modules in node: + ```js -const fetch = require('node-fetch'); +const fetch = require("node-fetch"); ``` If you are using a Promise library other than native, set it through fetch.Promise: + ```js -const Bluebird = require('bluebird'); +const Bluebird = require("bluebird"); fetch.Promise = Bluebird; ``` @@ -109,176 +111,185 @@ fetch.Promise = Bluebird; NOTE: The documentation below is up-to-date with `2.x` releases, [see `1.x` readme](https://github.com/bitinn/node-fetch/blob/1.x/README.md), [changelog](https://github.com/bitinn/node-fetch/blob/1.x/CHANGELOG.md) and [2.x upgrade guide](UPGRADE-GUIDE.md) for the differences. -#### Plain text or HTML +### Plain text or HTML + ```js -fetch('https://github.com/') - .then(res => res.text()) - .then(body => console.log(body)); +fetch("https://github.com/") + .then(res => res.text()) + .then(body => console.log(body)); ``` -#### JSON +### JSON ```js - -fetch('https://api.github.com/users/github') - .then(res => res.json()) - .then(json => console.log(json)); +fetch("https://api.github.com/users/github") + .then(res => res.json()) + .then(json => console.log(json)); ``` -#### Simple Post +### Simple Post + ```js -fetch('https://httpbin.org/post', { method: 'POST', body: 'a=1' }) - .then(res => res.json()) // expecting a json response - .then(json => console.log(json)); +fetch("https://httpbin.org/post", { method: "POST", body: "a=1" }) + .then(res => res.json()) // expecting a json response + .then(json => console.log(json)); ``` -#### Post with JSON +### Post with JSON ```js const body = { a: 1 }; -fetch('https://httpbin.org/post', { - method: 'post', - body: JSON.stringify(body), - headers: { 'Content-Type': 'application/json' }, - }) - .then(res => res.json()) - .then(json => console.log(json)); +fetch("https://httpbin.org/post", { + method: "post", + body: JSON.stringify(body), + headers: { "Content-Type": "application/json" } +}) + .then(res => res.json()) + .then(json => console.log(json)); ``` -#### Post with form parameters +### Post with form parameters + `URLSearchParams` is available in Node.js as of v7.5.0. See [official documentation](https://nodejs.org/api/url.html#url_class_urlsearchparams) for more usage methods. NOTE: The `Content-Type` header is only set automatically to `x-www-form-urlencoded` when an instance of `URLSearchParams` is given as such: ```js -const { URLSearchParams } = require('url'); +const { URLSearchParams } = require("url"); const params = new URLSearchParams(); -params.append('a', 1); +params.append("a", 1); -fetch('https://httpbin.org/post', { method: 'POST', body: params }) - .then(res => res.json()) - .then(json => console.log(json)); +fetch("https://httpbin.org/post", { method: "POST", body: params }) + .then(res => res.json()) + .then(json => console.log(json)); ``` -#### Handling exceptions -NOTE: 3xx-5xx responses are *NOT* exceptions, and should be handled in `then()`, see the next section. +### Handling exceptions + +NOTE: 3xx-5xx responses are _NOT_ exceptions, and should be handled in `then()`, see the next section. -Adding a catch to the fetch promise chain will catch *all* exceptions, such as errors originating from node core libraries, like network errors, and operational errors which are instances of FetchError. See the [error handling document](ERROR-HANDLING.md) for more details. +Adding a catch to the fetch promise chain will catch _all_ exceptions, such as errors originating from node core libraries, like network errors, and operational errors which are instances of FetchError. See the [error handling document](ERROR-HANDLING.md) for more details. ```js -fetch('https://domain.invalid/') - .catch(err => console.error(err)); +fetch("https://domain.invalid/").catch(err => console.error(err)); ``` -#### Handling client and server errors +### Handling client and server errors + It is common to create a helper function to check that the response contains no client (4xx) or server (5xx) error responses: ```js function checkStatus(res) { - if (res.ok) { // res.status >= 200 && res.status < 300 - return res; - } else { - throw MyCustomError(res.statusText); - } + if (res.ok) { + // res.status >= 200 && res.status < 300 + return res; + } else { + throw MyCustomError(res.statusText); + } } -fetch('https://httpbin.org/status/400') - .then(checkStatus) - .then(res => console.log('will not get here...')) +fetch("https://httpbin.org/status/400") + .then(checkStatus) + .then(res => console.log("will not get here...")); ``` ## Advanced Usage -#### Streams +### Streams + The "Node.js way" is to use streams when possible: ```js -fetch('https://assets-cdn.github.com/images/modules/logos_page/Octocat.png') - .then(res => { - const dest = fs.createWriteStream('./octocat.png'); - res.body.pipe(dest); - }); +fetch( + "https://assets-cdn.github.com/images/modules/logos_page/Octocat.png" +).then(res => { + const dest = fs.createWriteStream("./octocat.png"); + res.body.pipe(dest); +}); ``` -#### Buffer +### Buffer + If you prefer to cache binary data in full, use buffer(). (NOTE: buffer() is a `node-fetch` only API) ```js -const fileType = require('file-type'); - -fetch('https://assets-cdn.github.com/images/modules/logos_page/Octocat.png') - .then(res => res.buffer()) - .then(buffer => fileType(buffer)) - .then(type => { /* ... */ }); +const fileType = require("file-type"); + +fetch("https://assets-cdn.github.com/images/modules/logos_page/Octocat.png") + .then(res => res.buffer()) + .then(buffer => fileType(buffer)) + .then(type => { + /* ... */ + }); ``` -#### Accessing Headers and other Meta data +### Accessing Headers and other Meta data + ```js -fetch('https://github.com/') - .then(res => { - console.log(res.ok); - console.log(res.status); - console.log(res.statusText); - console.log(res.headers.raw()); - console.log(res.headers.get('content-type')); - }); +fetch("https://github.com/").then(res => { + console.log(res.ok); + console.log(res.status); + console.log(res.statusText); + console.log(res.headers.raw()); + console.log(res.headers.get("content-type")); +}); ``` -#### Extract Set-Cookie Header +### Extract Set-Cookie Header Unlike browsers, you can access raw `Set-Cookie` headers manually using `Headers.raw()`, this is a `node-fetch` only API. ```js fetch(url).then(res => { - // returns an array of values, instead of a string of comma-separated values - console.log(res.headers.raw()['set-cookie']); + // returns an array of values, instead of a string of comma-separated values + console.log(res.headers.raw()["set-cookie"]); }); ``` -#### Post data using a file stream +### Post data using a file stream ```js -const { createReadStream } = require('fs'); +const { createReadStream } = require("fs"); -const stream = createReadStream('input.txt'); +const stream = createReadStream("input.txt"); -fetch('https://httpbin.org/post', { method: 'POST', body: stream }) - .then(res => res.json()) - .then(json => console.log(json)); +fetch("https://httpbin.org/post", { method: "POST", body: stream }) + .then(res => res.json()) + .then(json => console.log(json)); ``` -#### Post with form-data (detect multipart) +### Post with form-data (detect multipart) ```js -const FormData = require('form-data'); +const FormData = require("form-data"); const form = new FormData(); -form.append('a', 1); +form.append("a", 1); -fetch('https://httpbin.org/post', { method: 'POST', body: form }) - .then(res => res.json()) - .then(json => console.log(json)); +fetch("https://httpbin.org/post", { method: "POST", body: form }) + .then(res => res.json()) + .then(json => console.log(json)); // OR, using custom headers // NOTE: getHeaders() is non-standard API const form = new FormData(); -form.append('a', 1); +form.append("a", 1); const options = { - method: 'POST', - body: form, - headers: form.getHeaders() -} + method: "POST", + body: form, + headers: form.getHeaders() +}; -fetch('https://httpbin.org/post', options) - .then(res => res.json()) - .then(json => console.log(json)); +fetch("https://httpbin.org/post", options) + .then(res => res.json()) + .then(json => console.log(json)); ``` -#### Request cancellation with AbortSignal +### Request cancellation with AbortSignal > NOTE: You may only cancel streamed requests on Node >= v8.0.0 @@ -287,34 +298,32 @@ You may cancel requests with `AbortController`. A suggested implementation is [` An example of timing out a request after 150ms could be achieved as follows: ```js -import AbortController from 'abort-controller'; +import AbortController from "abort-controller"; const controller = new AbortController(); -const timeout = setTimeout( - () => { controller.abort(); }, - 150, -); +const timeout = setTimeout(() => { + controller.abort(); +}, 150); fetch(url, { signal: controller.signal }) - .then(res => res.json()) - .then( - data => { - useData(data) - }, - err => { - if (err.name === 'AbortError') { - // request was aborted - } - }, - ) - .finally(() => { - clearTimeout(timeout); - }); + .then(res => res.json()) + .then( + data => { + useData(data); + }, + err => { + if (err.name === "AbortError") { + // request was aborted + } + } + ) + .finally(() => { + clearTimeout(timeout); + }); ``` See [test cases](https://github.com/bitinn/node-fetch/blob/master/test/test.js) for more examples. - ## API ### fetch(url[, options]) @@ -328,6 +337,7 @@ Perform an HTTP(S) fetch. `url` should be an absolute url, such as `https://example.com/`. A path-relative URL (`/file/under/root`) or protocol-relative URL (`//can-be-http-or-https.com/`) will result in a rejected promise. + ### Options The default values are shown after each option key. @@ -351,22 +361,22 @@ The default values are shown after each option key. } ``` -##### Default Headers +#### Default Headers If no values are set, the following request headers will be sent automatically: -Header | Value -------------------- | -------------------------------------------------------- -`Accept-Encoding` | `gzip,deflate` _(when `options.compress === true`)_ -`Accept` | `*/*` -`Connection` | `close` _(when no `options.agent` is present)_ -`Content-Length` | _(automatically calculated, if possible)_ -`Transfer-Encoding` | `chunked` _(when `req.body` is a stream)_ -`User-Agent` | `node-fetch/1.0 (+https://github.com/bitinn/node-fetch)` +| Header | Value | +| ------------------- | -------------------------------------------------------- | +| `Accept-Encoding` | `gzip,deflate` _(when `options.compress === true`)_ | +| `Accept` | `*/*` | +| `Connection` | `close` _(when no `options.agent` is present)_ | +| `Content-Length` | _(automatically calculated, if possible)_ | +| `Transfer-Encoding` | `chunked` _(when `req.body` is a stream)_ | +| `User-Agent` | `node-fetch/1.0 (+https://github.com/bitinn/node-fetch)` | Note: when `body` is a `Stream`, `Content-Length` is not set automatically. -##### Custom Agent +#### Custom Agent The `agent` option allows you to specify networking related options that's out of the scope of Fetch. Including and not limit to: @@ -380,24 +390,25 @@ In addition, `agent` option accepts a function that returns http(s).Agent instan ```js const httpAgent = new http.Agent({ - keepAlive: true + keepAlive: true }); const httpsAgent = new https.Agent({ - keepAlive: true + keepAlive: true }); const options = { - agent: function (_parsedURL) { - if (_parsedURL.protocol == 'http:') { - return httpAgent; - } else { - return httpsAgent; - } - } -} + agent: function(_parsedURL) { + if (_parsedURL.protocol == "http:") { + return httpAgent; + } else { + return httpsAgent; + } + } +}; ``` + ### Class: Request An HTTP(S) request containing information about URL, method, headers, and the body. This class implements the [Body](#iface-body) interface. @@ -426,7 +437,7 @@ See [options](#fetch-options) for exact meaning of these extensions. #### new Request(input[, options]) -*(spec-compliant)* +_(spec-compliant)_ - `input` A string representing a URL, or another `Request` (which will be cloned) - `options` [Options][#fetch-options] for the HTTP(S) request @@ -436,6 +447,7 @@ Constructs a new `Request` object. The constructor is identical to that in the [ In most cases, directly `fetch(url, options)` is simpler than creating a `Request` object. + ### Class: Response An HTTP(S) response. This class implements the [Body](#iface-body) interface. @@ -449,7 +461,7 @@ The following properties are not implemented in node-fetch at this moment: #### new Response([body[, options]]) -*(spec-compliant)* +_(spec-compliant)_ - `body` A string or [Readable stream][node-readable] - `options` A [`ResponseInit`][response-init] options dictionary @@ -460,24 +472,25 @@ Because Node.js does not implement service workers (for which this class was des #### response.ok -*(spec-compliant)* +_(spec-compliant)_ Convenience property representing if the request ended normally. Will evaluate to true if the response status was greater than or equal to 200 but smaller than 300. #### response.redirected -*(spec-compliant)* +_(spec-compliant)_ Convenience property representing if the request has been redirected at least once. Will evaluate to true if the internal redirect counter is greater than 0. + ### Class: Headers This class allows manipulating and iterating over a set of HTTP headers. All methods specified in the [Fetch Standard][whatwg-fetch] are implemented. #### new Headers([init]) -*(spec-compliant)* +_(spec-compliant)_ - `init` Optional argument to pre-fill the `Headers` object @@ -487,27 +500,25 @@ Construct a new `Headers` object. `init` can be either `null`, a `Headers` objec // Example adapted from https://fetch.spec.whatwg.org/#example-headers-class const meta = { - 'Content-Type': 'text/xml', - 'Breaking-Bad': '<3' + "Content-Type": "text/xml", + "Breaking-Bad": "<3" }; const headers = new Headers(meta); // The above is equivalent to -const meta = [ - [ 'Content-Type', 'text/xml' ], - [ 'Breaking-Bad', '<3' ] -]; +const meta = [["Content-Type", "text/xml"], ["Breaking-Bad", "<3"]]; const headers = new Headers(meta); // You can in fact use any iterable objects, like a Map or even another Headers const meta = new Map(); -meta.set('Content-Type', 'text/xml'); -meta.set('Breaking-Bad', '<3'); +meta.set("Content-Type", "text/xml"); +meta.set("Breaking-Bad", "<3"); const headers = new Headers(meta); const copyOfHeaders = new Headers(headers); ``` + ### Interface: Body `Body` is an abstract interface with methods that are applicable to both `Request` and `Response` classes. @@ -518,60 +529,65 @@ The following methods are not yet implemented in node-fetch at this moment: #### body.body -*(deviation from spec)* +_(deviation from spec)_ -* Node.js [`Readable` stream][node-readable] +- Node.js [`Readable` stream][node-readable] The data encapsulated in the `Body` object. Note that while the [Fetch Standard][whatwg-fetch] requires the property to always be a WHATWG `ReadableStream`, in node-fetch it is a Node.js [`Readable` stream][node-readable]. #### body.bodyUsed -*(spec-compliant)* +_(spec-compliant)_ -* `Boolean` +- `Boolean` A boolean property for if this body has been consumed. Per spec, a consumed body cannot be used again. #### body.arrayBuffer() + #### body.blob() + #### body.json() + #### body.text() -*(spec-compliant)* +_(spec-compliant)_ -* Returns: Promise +- Returns: `Promise` Consume the body and return a promise that will resolve to one of these formats. #### body.buffer() -*(node-fetch extension)* +_(node-fetch extension)_ -* Returns: Promise<Buffer> +- Returns: `Promise` Consume the body and return a promise that will resolve to a Buffer. #### body.textConverted() -*(node-fetch extension)* +_(node-fetch extension)_ -* Returns: Promise<String> +- Returns: `Promise` Identical to `body.text()`, except instead of always converting to UTF-8, encoding sniffing will be performed and text converted to UTF-8, if possible. (This API requires an optional dependency on npm package [encoding](https://www.npmjs.com/package/encoding), which you need to install manually. `webpack` users may see [a warning message](https://github.com/bitinn/node-fetch/issues/412#issuecomment-379007792) due to this optional dependency.) + ### Class: FetchError -*(node-fetch extension)* +_(node-fetch extension)_ An operational error in the fetching process. See [ERROR-HANDLING.md][] for more info. + ### Class: AbortError -*(node-fetch extension)* +_(node-fetch extension)_ An Error thrown when the request is aborted in response to an `AbortSignal`'s `abort` event. It has a `name` property of `AbortError`. See [ERROR-HANDLING.MD][] for more info. @@ -597,6 +613,6 @@ MIT [response-init]: https://fetch.spec.whatwg.org/#responseinit [node-readable]: https://nodejs.org/api/stream.html#stream_readable_streams [mdn-headers]: https://developer.mozilla.org/en-US/docs/Web/API/Headers -[LIMITS.md]: https://github.com/bitinn/node-fetch/blob/master/LIMITS.md -[ERROR-HANDLING.md]: https://github.com/bitinn/node-fetch/blob/master/ERROR-HANDLING.md -[UPGRADE-GUIDE.md]: https://github.com/bitinn/node-fetch/blob/master/UPGRADE-GUIDE.md +[limits.md]: https://github.com/bitinn/node-fetch/blob/master/LIMITS.md +[error-handling.md]: https://github.com/bitinn/node-fetch/blob/master/ERROR-HANDLING.md +[upgrade-guide.md]: https://github.com/bitinn/node-fetch/blob/master/UPGRADE-GUIDE.md diff --git a/package.json b/package.json index 55826ae24..c5a73ab2c 100644 --- a/package.json +++ b/package.json @@ -1,116 +1,116 @@ { - "name": "node-fetch", - "version": "2.6.0", - "description": "A light-weight module that brings window.fetch to node.js", - "main": "dist/index.js", - "module": "dist/index.mjs", - "types": "types/index.d.ts", - "files": [ - "src/**/*", - "dist/**/*", - "types/**/*.d.ts" - ], - "engines": { - "node": ">=8.0.0" - }, - "scripts": { - "build": "microbundle --external http,https,stream,zlib,pump,utf8,fetch-blob --name fetch --target node --format es,cjs", - "prepare": "npm run build", - "test": "cross-env BABEL_ENV=test mocha --require @babel/register --throw-deprecation test/test.js", - "report": "cross-env BABEL_ENV=coverage nyc --reporter lcov --reporter text mocha -R spec test/test.js", - "coverage": "cross-env BABEL_ENV=coverage nyc --reporter json --reporter text mocha -R spec test/test.js && codecov -f coverage/coverage-final.json", - "lint": "xo" - }, - "repository": { - "type": "git", - "url": "https://github.com/bitinn/node-fetch.git" - }, - "keywords": [ - "fetch", - "http", - "promise" - ], - "author": "David Frank", - "license": "MIT", - "bugs": { - "url": "https://github.com/bitinn/node-fetch/issues" - }, - "homepage": "https://github.com/bitinn/node-fetch", - "devDependencies": { - "@babel/core": "^7.6.0", - "@babel/preset-env": "^7.6.0", - "@babel/register": "^7.6.0", - "abort-controller": "^3.0.0", - "abortcontroller-polyfill": "^1.3.0", - "chai": "^4.2.0", - "chai-as-promised": "^7.1.1", - "chai-iterator": "^3.0.2", - "chai-string": "^1.5.0", - "codecov": "^3.6.1", - "cross-env": "^6.0.0", - "form-data": "^2.5.1", - "microbundle": "^0.11.0", - "mocha": "^6.2.0", - "nyc": "^14.1.1", - "parted": "^0.1.1", - "promise": "^8.0.3", - "resumer": "0.0.0", - "string-to-arraybuffer": "^1.0.2", - "whatwg-url": "^7.0.0", - "xo": "^0.24.0" - }, - "dependencies": { - "fetch-blob": "^1.0.4", - "pump": "^3.0.0", - "utf8": "^3.0.0" - }, - "optionalDependencies": { - "encoding": "^0.1.12" - }, - "resolutions": { - "microbundle/rollup-plugin-typescript2/rollup-pluginutils/micromatch/braces": "^2.3.1", - "microbundle/rollup-plugin-postcss/cssnano/postcss-svgo/svgo/js-yaml": "^3.13.1" - }, - "xo": { - "envs": [ - "node", - "browser", - "mocha" - ], - "rules": { - "valid-jsdoc": 0, - "no-multi-assign": 0, - "complexity": 0, - "unicorn/prefer-spread": 0, - "promise/prefer-await-to-then": 0, - "no-mixed-operators": 0, - "eqeqeq": 0, - "no-eq-null": 0, - "no-negated-condition": 0, - "node/no-deprecated-api": 1 - }, - "ignores": [ - "dist", - "test" - ] - }, - "babel": { - "presets": [ - [ - "@babel/preset-env", - { - "targets": { - "node": true - } - } - ] - ] - }, - "nyc": { - "require": [ - "@babel/register" - ], - "sourceMap": false, - "instrument": false - } + "name": "node-fetch", + "version": "2.6.0", + "description": "A light-weight module that brings window.fetch to node.js", + "main": "dist/index.js", + "module": "dist/index.mjs", + "types": "types/index.d.ts", + "files": [ + "src/**/*", + "dist/**/*", + "types/**/*.d.ts" + ], + "engines": { + "node": ">=8.0.0" + }, + "scripts": { + "build": "microbundle --external http,https,stream,zlib,pump,utf8,fetch-blob --name fetch --target node --format es,cjs", + "prepare": "npm run build", + "test": "cross-env BABEL_ENV=test mocha --require @babel/register --throw-deprecation test/test.js", + "report": "cross-env BABEL_ENV=coverage nyc --reporter lcov --reporter text mocha -R spec test/test.js", + "coverage": "cross-env BABEL_ENV=coverage nyc --reporter json --reporter text mocha -R spec test/test.js && codecov -f coverage/coverage-final.json", + "lint": "xo" + }, + "repository": { + "type": "git", + "url": "https://github.com/bitinn/node-fetch.git" + }, + "keywords": [ + "fetch", + "http", + "promise" + ], + "author": "David Frank", + "license": "MIT", + "bugs": { + "url": "https://github.com/bitinn/node-fetch/issues" + }, + "homepage": "https://github.com/bitinn/node-fetch", + "devDependencies": { + "@babel/core": "^7.6.0", + "@babel/preset-env": "^7.6.0", + "@babel/register": "^7.6.0", + "abort-controller": "^3.0.0", + "abortcontroller-polyfill": "^1.3.0", + "chai": "^4.2.0", + "chai-as-promised": "^7.1.1", + "chai-iterator": "^3.0.2", + "chai-string": "^1.5.0", + "codecov": "^3.6.1", + "cross-env": "^6.0.0", + "form-data": "^2.5.1", + "microbundle": "^0.11.0", + "mocha": "^6.2.0", + "nyc": "^14.1.1", + "parted": "^0.1.1", + "promise": "^8.0.3", + "resumer": "0.0.0", + "string-to-arraybuffer": "^1.0.2", + "whatwg-url": "^7.0.0", + "xo": "^0.24.0" + }, + "dependencies": { + "fetch-blob": "^1.0.4", + "pump": "^3.0.0", + "utf8": "^3.0.0" + }, + "optionalDependencies": { + "encoding": "^0.1.12" + }, + "resolutions": { + "microbundle/rollup-plugin-typescript2/rollup-pluginutils/micromatch/braces": "^2.3.1", + "microbundle/rollup-plugin-postcss/cssnano/postcss-svgo/svgo/js-yaml": "^3.13.1" + }, + "xo": { + "envs": [ + "node", + "browser", + "mocha" + ], + "rules": { + "valid-jsdoc": 0, + "no-multi-assign": 0, + "complexity": 0, + "unicorn/prefer-spread": 0, + "promise/prefer-await-to-then": 0, + "no-mixed-operators": 0, + "eqeqeq": 0, + "no-eq-null": 0, + "no-negated-condition": 0, + "node/no-deprecated-api": 1 + }, + "ignores": [ + "dist", + "test" + ] + }, + "babel": { + "presets": [ + [ + "@babel/preset-env", + { + "targets": { + "node": true + } + } + ] + ] + }, + "nyc": { + "require": [ + "@babel/register" + ], + "sourceMap": false, + "instrument": false + } } From bf65e085a3e526d98dc4e03f797739d6b9c8276e Mon Sep 17 00:00:00 2001 From: Richie Bendall Date: Fri, 27 Sep 2019 20:48:55 +1200 Subject: [PATCH 044/157] chore!: Drop NodeJS v8 Signed-off-by: Richie Bendall --- .travis.yml | 1 - package.json | 6 ++---- src/index.js | 19 +++++++------------ test/chai-timeout.js | 5 +++-- test/test.js | 1 - 5 files changed, 12 insertions(+), 20 deletions(-) diff --git a/.travis.yml b/.travis.yml index f96441bad..892ddf3da 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,7 +1,6 @@ language: node_js node_js: - - "8" - "lts/*" # 10 (Latest LTS) - "node" # 12 (Latest Stable) diff --git a/package.json b/package.json index c5a73ab2c..78e3836d8 100644 --- a/package.json +++ b/package.json @@ -11,10 +11,10 @@ "types/**/*.d.ts" ], "engines": { - "node": ">=8.0.0" + "node": ">=10.0.0" }, "scripts": { - "build": "microbundle --external http,https,stream,zlib,pump,utf8,fetch-blob --name fetch --target node --format es,cjs", + "build": "microbundle --external http,https,stream,zlib,utf8,fetch-blob --name fetch --target node --format es,cjs", "prepare": "npm run build", "test": "cross-env BABEL_ENV=test mocha --require @babel/register --throw-deprecation test/test.js", "report": "cross-env BABEL_ENV=coverage nyc --reporter lcov --reporter text mocha -R spec test/test.js", @@ -56,12 +56,10 @@ "promise": "^8.0.3", "resumer": "0.0.0", "string-to-arraybuffer": "^1.0.2", - "whatwg-url": "^7.0.0", "xo": "^0.24.0" }, "dependencies": { "fetch-blob": "^1.0.4", - "pump": "^3.0.0", "utf8": "^3.0.0" }, "optionalDependencies": { diff --git a/src/index.js b/src/index.js index dcda64a7b..09d6c3b3d 100644 --- a/src/index.js +++ b/src/index.js @@ -6,24 +6,19 @@ * All spec algorithm step numbers are based on https://fetch.spec.whatwg.org/commit-snapshots/ae716822cb3a61843226cd090eefc6589446c1d2/. */ -import Url from 'url'; +import { resolve as resolveUrl } from 'url'; import http from 'http'; import https from 'https'; import zlib from 'zlib'; -import Stream from 'stream'; -import pump from 'pump'; +import Stream, { PassThrough, pipeline as pump } from 'stream'; -import Body, {writeToStream, getTotalBytes} from './body'; +import Body, { writeToStream, getTotalBytes } from './body'; import Response from './response'; -import Headers, {createHeadersLenient} from './headers'; -import Request, {getNodeRequestOptions} from './request'; +import Headers, { createHeadersLenient } from './headers'; +import Request, { getNodeRequestOptions } from './request'; import FetchError from './errors/fetch-error'; import AbortError from './errors/abort-error'; -// Fix an issue where "PassThrough", "resolve" aren't a named export for node <10 -const {PassThrough} = Stream; -const resolveUrl = Url.resolve; - /** * Fetch function * @@ -43,7 +38,7 @@ export default function fetch(url, opts) { // If valid data uri if (dataUriRegex.test(url)) { const data = Buffer.from(url.split(',')[1], 'base64'); - const res = new Response(data.body, {headers: {'Content-Type': data.mimeType || url.match(dataUriRegex)[1] || 'text/plain'}}); + const res = new Response(data.body, { headers: { 'Content-Type': data.mimeType || url.match(dataUriRegex)[1] || 'text/plain' } }); return fetch.Promise.resolve(res); } @@ -62,7 +57,7 @@ export default function fetch(url, opts) { const options = getNodeRequestOptions(request); const send = (options.protocol === 'https:' ? https : http).request; - const {signal} = request; + const { signal } = request; let response = null; const abort = () => { diff --git a/test/chai-timeout.js b/test/chai-timeout.js index cecb9efa9..0fbce7b59 100644 --- a/test/chai-timeout.js +++ b/test/chai-timeout.js @@ -1,5 +1,5 @@ -module.exports = (chai, utils) => { - utils.addProperty(chai.Assertion.prototype, 'timeout', function () { +export default ({ Assertion }, utils) => { + utils.addProperty(Assertion.prototype, 'timeout', function () { return new Promise(resolve => { const timer = setTimeout(() => resolve(true), 150); this._obj.then(() => { @@ -15,3 +15,4 @@ module.exports = (chai, utils) => { }); }); }; + diff --git a/test/test.js b/test/test.js index 7c7f8ff4c..60069a55d 100644 --- a/test/test.js +++ b/test/test.js @@ -9,7 +9,6 @@ import resumer from 'resumer'; import FormData from 'form-data'; import stringToArrayBuffer from 'string-to-arraybuffer'; -import { URL } from 'whatwg-url'; import { AbortController } from 'abortcontroller-polyfill/dist/abortcontroller'; import AbortController2 from 'abort-controller'; import crypto from 'crypto'; From 9afeea50d96a747c4311abcd096d2f0c4a17fd72 Mon Sep 17 00:00:00 2001 From: Richie Bendall Date: Fri, 27 Sep 2019 21:02:21 +1200 Subject: [PATCH 045/157] chore: Remove legacy code for node < 8 Signed-off-by: Richie Bendall --- src/body.js | 5 +---- src/index.js | 14 +++++++------- src/request.js | 8 ++------ test/test.js | 2 +- 4 files changed, 11 insertions(+), 18 deletions(-) diff --git a/src/body.js b/src/body.js index 75482f56d..d0bbac3b5 100644 --- a/src/body.js +++ b/src/body.js @@ -5,7 +5,7 @@ * Body interface provides common methods for Request and Response */ -import Stream from 'stream'; +import Stream, {PassThrough} from 'stream'; import Blob from 'fetch-blob'; import FetchError from './errors/fetch-error'; @@ -18,9 +18,6 @@ try { const INTERNALS = Symbol('Body internals'); -// Fix an issue where "PassThrough" isn't a named export for node <10 -const {PassThrough} = Stream; - /** * Body mixin * diff --git a/src/index.js b/src/index.js index 09d6c3b3d..2060da1af 100644 --- a/src/index.js +++ b/src/index.js @@ -6,16 +6,16 @@ * All spec algorithm step numbers are based on https://fetch.spec.whatwg.org/commit-snapshots/ae716822cb3a61843226cd090eefc6589446c1d2/. */ -import { resolve as resolveUrl } from 'url'; +import {resolve as resolveUrl} from 'url'; import http from 'http'; import https from 'https'; import zlib from 'zlib'; -import Stream, { PassThrough, pipeline as pump } from 'stream'; +import Stream, {PassThrough, pipeline as pump} from 'stream'; -import Body, { writeToStream, getTotalBytes } from './body'; +import Body, {writeToStream, getTotalBytes} from './body'; import Response from './response'; -import Headers, { createHeadersLenient } from './headers'; -import Request, { getNodeRequestOptions } from './request'; +import Headers, {createHeadersLenient} from './headers'; +import Request, {getNodeRequestOptions} from './request'; import FetchError from './errors/fetch-error'; import AbortError from './errors/abort-error'; @@ -38,7 +38,7 @@ export default function fetch(url, opts) { // If valid data uri if (dataUriRegex.test(url)) { const data = Buffer.from(url.split(',')[1], 'base64'); - const res = new Response(data.body, { headers: { 'Content-Type': data.mimeType || url.match(dataUriRegex)[1] || 'text/plain' } }); + const res = new Response(data.body, {headers: {'Content-Type': data.mimeType || url.match(dataUriRegex)[1] || 'text/plain'}}); return fetch.Promise.resolve(res); } @@ -57,7 +57,7 @@ export default function fetch(url, opts) { const options = getNodeRequestOptions(request); const send = (options.protocol === 'https:' ? https : http).request; - const { signal } = request; + const {signal} = request; let response = null; const abort = () => { diff --git a/src/request.js b/src/request.js index 76aee8e48..16141d66d 100644 --- a/src/request.js +++ b/src/request.js @@ -7,7 +7,7 @@ * All spec algorithm step numbers are based on https://fetch.spec.whatwg.org/commit-snapshots/ae716822cb3a61843226cd090eefc6589446c1d2/. */ -import Url from 'url'; +import {parse as parseUrl, format as formatUrl} from 'url'; import Stream from 'stream'; import utf8 from 'utf8'; import Headers, {exportNodeCompatibleHeaders} from './headers'; @@ -16,10 +16,6 @@ import {isAbortSignal} from './utils/is'; const INTERNALS = Symbol('Request internals'); -// Fix an issue where "format", "parse" aren't a named export for node <10 -const parseUrl = Url.parse; -const formatUrl = Url.format; - const streamDestructionSupported = 'destroy' in Stream.Readable.prototype; /** @@ -201,7 +197,7 @@ export function getNodeRequestOptions(request) { request.body instanceof Stream.Readable && !streamDestructionSupported ) { - throw new Error('Cancellation of streamed requests with AbortSignal is not supported in node < 8'); + throw new Error('Cancellation of streamed requests with AbortSignal is not supported'); } // HTTP-network-or-cache fetch steps 2.4-2.7 diff --git a/test/test.js b/test/test.js index d4df2152c..cdf03eaca 100644 --- a/test/test.js +++ b/test/test.js @@ -139,7 +139,7 @@ describe('node-fetch', () => { return expect(err).to.not.have.property('erroredSysCall'); }); - it('system error is extracted from failed requests', function() { + itIf(process.platform !== "win32")('system error is extracted from failed requests', function() { const url = 'http://localhost:50000/'; return expect(fetch(url)).to.eventually.be.rejected .and.be.an.instanceOf(FetchError) From eb9f43cbeb513908b2efc6f5bc68bcea83fca689 Mon Sep 17 00:00:00 2001 From: Richie Bendall Date: Sat, 28 Sep 2019 16:58:11 +1200 Subject: [PATCH 046/157] chore: Use proper checks for ArrayBuffer and AbortError Signed-off-by: Richie Bendall --- src/body.js | 8 ++++---- src/utils/is.js | 48 ++++++++++++++++++++++++++++++++++-------------- 2 files changed, 38 insertions(+), 18 deletions(-) diff --git a/src/body.js b/src/body.js index d0bbac3b5..88fc18bf5 100644 --- a/src/body.js +++ b/src/body.js @@ -9,7 +9,7 @@ import Stream, {PassThrough} from 'stream'; import Blob from 'fetch-blob'; import FetchError from './errors/fetch-error'; -import {isBlob, isURLSearchParams} from './utils/is'; +import {isBlob, isURLSearchParams, isArrayBuffer, isAbortError} from './utils/is'; let convert; try { @@ -41,7 +41,7 @@ export default function Body(body, { // Body is blob } else if (Buffer.isBuffer(body)) { // Body is Buffer - } else if (Object.prototype.toString.call(body) === '[object ArrayBuffer]') { + } else if (isArrayBuffer(body)) { // Body is ArrayBuffer body = Buffer.from(body); } else if (ArrayBuffer.isView(body)) { @@ -65,7 +65,7 @@ export default function Body(body, { if (body instanceof Stream) { body.on('error', err => { - const error = err.name === 'AbortError' ? + const error = isAbortError(err) ? err : new FetchError(`Invalid response body while trying to fetch ${this.url}: ${err.message}`, 'system', err); this[INTERNALS].error = error; @@ -227,7 +227,7 @@ function consumeBody() { // Handle stream errors body.on('error', err => { - if (err.name === 'AbortError') { + if (isAbortError(err)) { // If the request was aborted, reject with this Error abort = true; reject(err); diff --git a/src/utils/is.js b/src/utils/is.js index 5e5df309e..ce745bd30 100644 --- a/src/utils/is.js +++ b/src/utils/is.js @@ -16,14 +16,14 @@ const NAME = Symbol.toStringTag; export function isURLSearchParams(obj) { return ( typeof obj === 'object' && - typeof obj.append === 'function' && - typeof obj.delete === 'function' && - typeof obj.get === 'function' && - typeof obj.getAll === 'function' && - typeof obj.has === 'function' && - typeof obj.set === 'function' && - typeof obj.sort === 'function' && - obj[NAME] === 'URLSearchParams' + typeof obj.append === 'function' && + typeof obj.delete === 'function' && + typeof obj.get === 'function' && + typeof obj.getAll === 'function' && + typeof obj.has === 'function' && + typeof obj.set === 'function' && + typeof obj.sort === 'function' && + obj[NAME] === 'URLSearchParams' ); } @@ -36,11 +36,11 @@ export function isURLSearchParams(obj) { export function isBlob(obj) { return ( typeof obj === 'object' && - typeof obj.arrayBuffer === 'function' && - typeof obj.type === 'string' && - typeof obj.stream === 'function' && - typeof obj.constructor === 'function' && - /^(Blob|File)$/.test(obj[NAME]) + typeof obj.arrayBuffer === 'function' && + typeof obj.type === 'string' && + typeof obj.stream === 'function' && + typeof obj.constructor === 'function' && + /^(Blob|File)$/.test(obj[NAME]) ); } @@ -53,6 +53,26 @@ export function isBlob(obj) { export function isAbortSignal(obj) { return ( typeof obj === 'object' && - obj[NAME] === 'AbortSignal' + obj[NAME] === 'AbortSignal' ); } + +/** + * Check if `obj` is an instance of ArrayBuffer. + * + * @param {*} obj + * @return {boolean} + */ +export function isArrayBuffer(obj) { + return obj[NAME] === 'ArrayBuffer'; +} + +/** + * Check if `obj` is an instance of AbortError. + * + * @param {*} obj + * @return {boolean} + */ +export function isAbortError(obj) { + return obj[NAME] === 'AbortError'; +} From 6bddf43d533bbca0ecfed82486e3c84449af3905 Mon Sep 17 00:00:00 2001 From: Richie Bendall Date: Sat, 28 Sep 2019 17:10:31 +1200 Subject: [PATCH 047/157] chore: Use explicitly set error name in checks Signed-off-by: Richie Bendall --- src/utils/is.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/utils/is.js b/src/utils/is.js index ce745bd30..3242d2d4b 100644 --- a/src/utils/is.js +++ b/src/utils/is.js @@ -74,5 +74,5 @@ export function isArrayBuffer(obj) { * @return {boolean} */ export function isAbortError(obj) { - return obj[NAME] === 'AbortError'; + return obj.name === 'AbortError'; } From 352de0bfeffcb0c6d8520373695d802d3c1c8ee5 Mon Sep 17 00:00:00 2001 From: Nazar Mokrynskyi Date: Sat, 28 Sep 2019 16:06:26 +0300 Subject: [PATCH 048/157] Propagate size and timeout to cloned response (#664) * Remove --save option as it isn't required anymore (#581) * Propagate size and timeout to cloned response Co-authored-by: Steve Moser Co-authored-by: Antoni Kepinski --- README.md | 2 +- src/response.js | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index b5ff6c442..3c7c1a8f7 100644 --- a/README.md +++ b/README.md @@ -88,7 +88,7 @@ See Matt Andrews' [isomorphic-fetch](https://github.com/matthew-andrews/isomorph Current stable release (`2.x`) ```sh -npm install node-fetch --save +$ npm install node-fetch ``` ## Loading and configuring the module diff --git a/src/response.js b/src/response.js index 2dfc3b5a3..d39cd218e 100644 --- a/src/response.js +++ b/src/response.js @@ -83,7 +83,9 @@ export default class Response { statusText: this.statusText, headers: this.headers, ok: this.ok, - redirected: this.redirected + redirected: this.redirected, + size: this.size, + timeout: this.timeout }); } } From afc83c6bf2e2c5fa9a3e987b8b18f41ebdaf8054 Mon Sep 17 00:00:00 2001 From: Antoni Kepinski Date: Sat, 28 Sep 2019 15:12:28 +0200 Subject: [PATCH 049/157] Update Response types --- types/index.d.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/types/index.d.ts b/types/index.d.ts index ce655bba0..9089f61f7 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -169,7 +169,9 @@ export class Response extends Body { status: number; statusText: string; type: ResponseType; - url: string; + url: string; + size: number; + timeout: number; } export type ResponseType = @@ -209,4 +211,4 @@ declare namespace fetch { function isRedirect(code: number): boolean; } -export default fetch; \ No newline at end of file +export default fetch; From 8064fe57e3002b21ccb4d3887dc715d5abf1a849 Mon Sep 17 00:00:00 2001 From: Antoni Kepinski Date: Sat, 28 Sep 2019 15:12:39 +0200 Subject: [PATCH 050/157] Update devDependencies --- package.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/package.json b/package.json index 78e3836d8..4d6b2b48a 100644 --- a/package.json +++ b/package.json @@ -37,9 +37,9 @@ }, "homepage": "https://github.com/bitinn/node-fetch", "devDependencies": { - "@babel/core": "^7.6.0", - "@babel/preset-env": "^7.6.0", - "@babel/register": "^7.6.0", + "@babel/core": "^7.6.2", + "@babel/preset-env": "^7.6.2", + "@babel/register": "^7.6.2", "abort-controller": "^3.0.0", "abortcontroller-polyfill": "^1.3.0", "chai": "^4.2.0", @@ -56,7 +56,7 @@ "promise": "^8.0.3", "resumer": "0.0.0", "string-to-arraybuffer": "^1.0.2", - "xo": "^0.24.0" + "xo": "^0.25.3" }, "dependencies": { "fetch-blob": "^1.0.4", From 46be1bd30b187da62e3ef3f2d7628d3c3b6b8e97 Mon Sep 17 00:00:00 2001 From: Richie Bendall Date: Tue, 1 Oct 2019 13:46:25 +1300 Subject: [PATCH 051/157] feat: Fallback to blob type (Closes: #607) Signed-off-by: Richie Bendall --- src/body.js | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/src/body.js b/src/body.js index 88fc18bf5..f098cf69a 100644 --- a/src/body.js +++ b/src/body.js @@ -5,11 +5,11 @@ * Body interface provides common methods for Request and Response */ -import Stream, {PassThrough} from 'stream'; +import Stream, { PassThrough } from 'stream'; import Blob from 'fetch-blob'; import FetchError from './errors/fetch-error'; -import {isBlob, isURLSearchParams, isArrayBuffer, isAbortError} from './utils/is'; +import { isBlob, isURLSearchParams, isArrayBuffer, isAbortError } from './utils/is'; let convert; try { @@ -88,7 +88,7 @@ Body.prototype = { * @return Promise */ arrayBuffer() { - return consumeBody.call(this).then(({buffer, byteOffset, byteLength}) => buffer.slice(byteOffset, byteOffset + byteLength)); + return consumeBody.call(this).then(({ buffer, byteOffset, byteLength }) => buffer.slice(byteOffset, byteOffset + byteLength)); }, /** @@ -97,7 +97,7 @@ Body.prototype = { * @return Promise */ blob() { - const ct = this.headers && this.headers.get('content-type') || ''; + const ct = this.headers && this.headers.get('content-type') || this[INTERNALS].body && this[INTERNALS].body.type || ''; return consumeBody.call(this).then(buf => new Blob([], { type: ct.toLowerCase(), buffer: buf @@ -150,12 +150,12 @@ Body.prototype = { // In browsers, all properties are enumerable. Object.defineProperties(Body.prototype, { - body: {enumerable: true}, - bodyUsed: {enumerable: true}, - arrayBuffer: {enumerable: true}, - blob: {enumerable: true}, - json: {enumerable: true}, - text: {enumerable: true} + body: { enumerable: true }, + bodyUsed: { enumerable: true }, + arrayBuffer: { enumerable: true }, + blob: { enumerable: true }, + json: { enumerable: true }, + text: { enumerable: true } }); Body.mixIn = proto => { @@ -186,7 +186,7 @@ function consumeBody() { return Body.Promise.reject(this[INTERNALS].error); } - let {body} = this; + let { body } = this; // Body is null if (body === null) { @@ -344,7 +344,7 @@ function convertBody(buffer, headers) { export function clone(instance, highWaterMark) { let p1; let p2; - let {body} = instance; + let { body } = instance; // Don't allow cloning a used body if (instance.bodyUsed) { @@ -355,8 +355,8 @@ export function clone(instance, highWaterMark) { // note: we can't clone the form-data object without having it as a dependency if ((body instanceof Stream) && (typeof body.getBoundary !== 'function')) { // Tee instance body - p1 = new PassThrough({highWaterMark}); - p2 = new PassThrough({highWaterMark}); + p1 = new PassThrough({ highWaterMark }); + p2 = new PassThrough({ highWaterMark }); body.pipe(p1); body.pipe(p2); // Set instance body to teed body and return the other teed body @@ -437,7 +437,7 @@ export function extractContentType(body) { * @return Number? Number of bytes, or null if not possible */ export function getTotalBytes(instance) { - const {body} = instance; + const { body } = instance; if (body === null) { // Body is null @@ -474,7 +474,7 @@ export function getTotalBytes(instance) { * @return Void */ export function writeToStream(dest, instance) { - const {body} = instance; + const { body } = instance; if (body === null) { // Body is null From 5e3525e28fee9eca82ba7a0ccdb44325f1aad468 Mon Sep 17 00:00:00 2001 From: Richie Bendall Date: Tue, 1 Oct 2019 13:47:53 +1300 Subject: [PATCH 052/157] style: Update formatting Signed-off-by: Richie Bendall --- src/body.js | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/src/body.js b/src/body.js index f098cf69a..09dd5ce2f 100644 --- a/src/body.js +++ b/src/body.js @@ -5,11 +5,11 @@ * Body interface provides common methods for Request and Response */ -import Stream, { PassThrough } from 'stream'; +import Stream, {PassThrough} from 'stream'; import Blob from 'fetch-blob'; import FetchError from './errors/fetch-error'; -import { isBlob, isURLSearchParams, isArrayBuffer, isAbortError } from './utils/is'; +import {isBlob, isURLSearchParams, isArrayBuffer, isAbortError} from './utils/is'; let convert; try { @@ -88,7 +88,7 @@ Body.prototype = { * @return Promise */ arrayBuffer() { - return consumeBody.call(this).then(({ buffer, byteOffset, byteLength }) => buffer.slice(byteOffset, byteOffset + byteLength)); + return consumeBody.call(this).then(({buffer, byteOffset, byteLength}) => buffer.slice(byteOffset, byteOffset + byteLength)); }, /** @@ -150,12 +150,12 @@ Body.prototype = { // In browsers, all properties are enumerable. Object.defineProperties(Body.prototype, { - body: { enumerable: true }, - bodyUsed: { enumerable: true }, - arrayBuffer: { enumerable: true }, - blob: { enumerable: true }, - json: { enumerable: true }, - text: { enumerable: true } + body: {enumerable: true}, + bodyUsed: {enumerable: true}, + arrayBuffer: {enumerable: true}, + blob: {enumerable: true}, + json: {enumerable: true}, + text: {enumerable: true} }); Body.mixIn = proto => { @@ -186,7 +186,7 @@ function consumeBody() { return Body.Promise.reject(this[INTERNALS].error); } - let { body } = this; + let {body} = this; // Body is null if (body === null) { @@ -344,7 +344,7 @@ function convertBody(buffer, headers) { export function clone(instance, highWaterMark) { let p1; let p2; - let { body } = instance; + let {body} = instance; // Don't allow cloning a used body if (instance.bodyUsed) { @@ -355,8 +355,8 @@ export function clone(instance, highWaterMark) { // note: we can't clone the form-data object without having it as a dependency if ((body instanceof Stream) && (typeof body.getBoundary !== 'function')) { // Tee instance body - p1 = new PassThrough({ highWaterMark }); - p2 = new PassThrough({ highWaterMark }); + p1 = new PassThrough({highWaterMark}); + p2 = new PassThrough({highWaterMark}); body.pipe(p1); body.pipe(p2); // Set instance body to teed body and return the other teed body @@ -437,7 +437,7 @@ export function extractContentType(body) { * @return Number? Number of bytes, or null if not possible */ export function getTotalBytes(instance) { - const { body } = instance; + const {body} = instance; if (body === null) { // Body is null @@ -474,7 +474,7 @@ export function getTotalBytes(instance) { * @return Void */ export function writeToStream(dest, instance) { - const { body } = instance; + const {body} = instance; if (body === null) { // Body is null From 58c43c0d992f1045d3bf6b8559d977e56f5c2dc9 Mon Sep 17 00:00:00 2001 From: Richie Bendall Date: Mon, 7 Oct 2019 20:09:33 +1300 Subject: [PATCH 053/157] style: Fix linting issues Signed-off-by: Richie Bendall --- package.json | 2 ++ src/body.js | 2 +- src/headers.js | 6 +++--- src/request.js | 12 ++++++------ 4 files changed, 12 insertions(+), 10 deletions(-) diff --git a/package.json b/package.json index 4d6b2b48a..5ae91b6f1 100644 --- a/package.json +++ b/package.json @@ -85,6 +85,8 @@ "eqeqeq": 0, "no-eq-null": 0, "no-negated-condition": 0, + "prefer-named-capture-group": 0, + "unicorn/catch-error-name": 0, "node/no-deprecated-api": 1 }, "ignores": [ diff --git a/src/body.js b/src/body.js index 09dd5ce2f..4f75966b3 100644 --- a/src/body.js +++ b/src/body.js @@ -14,7 +14,7 @@ import {isBlob, isURLSearchParams, isArrayBuffer, isAbortError} from './utils/is let convert; try { convert = require('encoding').convert; -} catch (error) { } +} catch (_) { } const INTERNALS = Symbol('Body internals'); diff --git a/src/headers.js b/src/headers.js index 16191703c..297aa4f93 100644 --- a/src/headers.js +++ b/src/headers.js @@ -6,7 +6,7 @@ */ const invalidTokenRegex = /[^_`a-zA-Z\-0-9!#$%&'*+.|~]/; -const invalidHeaderCharRegex = /[^\t\u0020-\u007e\u0080-\u00ff]/; +const invalidHeaderCharRegex = /[^\t\u0020-\u007E\u0080-\u00FF]/; function validateName(name) { name = `${name}`; @@ -273,9 +273,9 @@ function getHeaders(headers, kind = 'key+value') { return keys.map( kind === 'key' ? k => k.toLowerCase() : - kind === 'value' ? + (kind === 'value' ? k => headers[MAP][k].join(', ') : - k => [k.toLowerCase(), headers[MAP][k].join(', ')] + k => [k.toLowerCase(), headers[MAP][k].join(', ')]) ); } diff --git a/src/request.js b/src/request.js index 16141d66d..c6c14421e 100644 --- a/src/request.js +++ b/src/request.js @@ -69,9 +69,9 @@ export default class Request { const inputBody = init.body != null ? init.body : - isRequest(input) && input.body !== null ? + (isRequest(input) && input.body !== null ? clone(input) : - null; + null); Body.call(this, inputBody, { timeout: init.timeout || input.timeout || 0, @@ -108,11 +108,11 @@ export default class Request { // Node-fetch-only options this.follow = init.follow !== undefined ? - init.follow : input.follow !== undefined ? - input.follow : 20; + init.follow : (input.follow !== undefined ? + input.follow : 20); this.compress = init.compress !== undefined ? - init.compress : input.compress !== undefined ? - input.compress : true; + init.compress : (input.compress !== undefined ? + input.compress : true); this.counter = init.counter || input.counter || 0; this.agent = init.agent || input.agent; this.highWaterMark = init.highWaterMark || input.highWaterMark; From 7e5a483dbe481c6c4df4a8a74ba446bafad031eb Mon Sep 17 00:00:00 2001 From: Richie Bendall Date: Sun, 13 Oct 2019 01:36:17 +1300 Subject: [PATCH 054/157] docs: Add info on patching the global object --- README.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/README.md b/README.md index 3c7c1a8f7..94ae4429f 100644 --- a/README.md +++ b/README.md @@ -107,6 +107,14 @@ const Bluebird = require("bluebird"); fetch.Promise = Bluebird; ``` +If you want to patch the global object in node: + +```js +if (!global.fetch) { + global.fetch = fetch; +} +``` + ## Common Usage NOTE: The documentation below is up-to-date with `2.x` releases, [see `1.x` readme](https://github.com/bitinn/node-fetch/blob/1.x/README.md), [changelog](https://github.com/bitinn/node-fetch/blob/1.x/CHANGELOG.md) and [2.x upgrade guide](UPGRADE-GUIDE.md) for the differences. From 67b03462dd7a030120341c3211e8109f534eb312 Mon Sep 17 00:00:00 2001 From: Richie Bendall Date: Mon, 14 Oct 2019 15:57:05 +1300 Subject: [PATCH 055/157] docs: Added non-globalThis polyfill --- README.md | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 94ae4429f..533308b4e 100644 --- a/README.md +++ b/README.md @@ -110,11 +110,27 @@ fetch.Promise = Bluebird; If you want to patch the global object in node: ```js -if (!global.fetch) { - global.fetch = fetch; +if (!globalThis.fetch) { + globalThis.fetch = fetch; } ``` +For versions of node earlier than 12.x, use this `globalThis` [polyfill](https://mathiasbynens.be/notes/globalthis): + +```js +(function() { + if (typeof globalThis === 'object') return; + Object.defineProperty(Object.prototype, '__magic__', { + get: function() { + return this; + }, + configurable: true + }); + __magic__.globalThis = __magic__; + delete Object.prototype.__magic__; +}()); +``` + ## Common Usage NOTE: The documentation below is up-to-date with `2.x` releases, [see `1.x` readme](https://github.com/bitinn/node-fetch/blob/1.x/README.md), [changelog](https://github.com/bitinn/node-fetch/blob/1.x/CHANGELOG.md) and [2.x upgrade guide](UPGRADE-GUIDE.md) for the differences. From e3cd4d22c3f1871fb41b17035ac83b47ad57fdf6 Mon Sep 17 00:00:00 2001 From: Antoni Kepinski Date: Tue, 29 Oct 2019 23:27:28 +0100 Subject: [PATCH 056/157] Replace deprecated `url.resolve` with the new WHATWG URL --- src/index.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/index.js b/src/index.js index 2060da1af..e1ed83172 100644 --- a/src/index.js +++ b/src/index.js @@ -6,7 +6,6 @@ * All spec algorithm step numbers are based on https://fetch.spec.whatwg.org/commit-snapshots/ae716822cb3a61843226cd090eefc6589446c1d2/. */ -import {resolve as resolveUrl} from 'url'; import http from 'http'; import https from 'https'; import zlib from 'zlib'; @@ -126,7 +125,7 @@ export default function fetch(url, opts) { const location = headers.get('Location'); // HTTP fetch step 5.3 - const locationURL = location === null ? null : resolveUrl(request.url, location); + const locationURL = location === null ? null : new URL(location, request.url); // HTTP fetch step 5.5 switch (request.redirect) { From 24a75808970625abf78db74f7f028bd82e7a462a Mon Sep 17 00:00:00 2001 From: Antoni Kepinski Date: Wed, 30 Oct 2019 16:52:08 +0100 Subject: [PATCH 057/157] Update devDependencies --- package.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/package.json b/package.json index 5ae91b6f1..6e76990c7 100644 --- a/package.json +++ b/package.json @@ -37,8 +37,8 @@ }, "homepage": "https://github.com/bitinn/node-fetch", "devDependencies": { - "@babel/core": "^7.6.2", - "@babel/preset-env": "^7.6.2", + "@babel/core": "^7.6.4", + "@babel/preset-env": "^7.6.3", "@babel/register": "^7.6.2", "abort-controller": "^3.0.0", "abortcontroller-polyfill": "^1.3.0", @@ -47,10 +47,10 @@ "chai-iterator": "^3.0.2", "chai-string": "^1.5.0", "codecov": "^3.6.1", - "cross-env": "^6.0.0", + "cross-env": "^6.0.3", "form-data": "^2.5.1", "microbundle": "^0.11.0", - "mocha": "^6.2.0", + "mocha": "^6.2.2", "nyc": "^14.1.1", "parted": "^0.1.1", "promise": "^8.0.3", From e4e5316e51a7e56aee1ae44350b3f05e8e094ccd Mon Sep 17 00:00:00 2001 From: Antoni Kepinski Date: Wed, 30 Oct 2019 16:53:31 +0100 Subject: [PATCH 058/157] Format code in examples to use `xo` style --- README.md | 184 ++++++++++++++++++++++++++---------------------------- 1 file changed, 90 insertions(+), 94 deletions(-) diff --git a/README.md b/README.md index 533308b4e..65059391f 100644 --- a/README.md +++ b/README.md @@ -7,58 +7,56 @@ A light-weight module that brings `window.fetch` to Node.js -(We are looking for [v2 maintainers and collaborators](https://github.com/bitinn/node-fetch/issues/567)) - - [node-fetch](#node-fetch) - - [Motivation](#motivation) - - [Features](#features) - - [Difference from client-side fetch](#difference-from-client-side-fetch) - - [Installation](#installation) - - [Loading and configuring the module](#loading-and-configuring-the-module) - - [Common Usage](#common-usage) - - [Plain text or HTML](#plain-text-or-html) - - [JSON](#json) - - [Simple Post](#simple-post) - - [Post with JSON](#post-with-json) - - [Post with form parameters](#post-with-form-parameters) - - [Handling exceptions](#handling-exceptions) - - [Handling client and server errors](#handling-client-and-server-errors) - - [Advanced Usage](#advanced-usage) - - [Streams](#streams) - - [Buffer](#buffer) - - [Accessing Headers and other Meta data](#accessing-headers-and-other-meta-data) - - [Extract Set-Cookie Header](#extract-set-cookie-header) - - [Post data using a file stream](#post-data-using-a-file-stream) - - [Post with form-data (detect multipart)](#post-with-form-data-detect-multipart) - - [Request cancellation with AbortSignal](#request-cancellation-with-abortsignal) - - [API](#api) - - [fetch(url[, options])](#fetchurl-options) - - [Options](#options) - - [Default Headers](#default-headers) - - [Custom Agent](#custom-agent) - - [Class: Request](#class-request) - - [new Request(input[, options])](#new-requestinput-options) - - [Class: Response](#class-response) - - [new Response([body[, options]])](#new-responsebody-options) - - [response.ok](#responseok) - - [response.redirected](#responseredirected) - - [Class: Headers](#class-headers) - - [new Headers([init])](#new-headersinit) - - [Interface: Body](#interface-body) - - [body.body](#bodybody) - - [body.bodyUsed](#bodybodyused) - - [body.arrayBuffer()](#bodyarraybuffer) - - [body.blob()](#bodyblob) - - [body.json()](#bodyjson) - - [body.text()](#bodytext) - - [body.buffer()](#bodybuffer) - - [body.textConverted()](#bodytextconverted) - - [Class: FetchError](#class-fetcherror) - - [Class: AbortError](#class-aborterror) - - [Acknowledgement](#acknowledgement) - - [License](#license) + - [Motivation](#motivation) + - [Features](#features) + - [Difference from client-side fetch](#difference-from-client-side-fetch) + - [Installation](#installation) + - [Loading and configuring the module](#loading-and-configuring-the-module) + - [Common Usage](#common-usage) + - [Plain text or HTML](#plain-text-or-html) + - [JSON](#json) + - [Simple Post](#simple-post) + - [Post with JSON](#post-with-json) + - [Post with form parameters](#post-with-form-parameters) + - [Handling exceptions](#handling-exceptions) + - [Handling client and server errors](#handling-client-and-server-errors) + - [Advanced Usage](#advanced-usage) + - [Streams](#streams) + - [Buffer](#buffer) + - [Accessing Headers and other Meta data](#accessing-headers-and-other-meta-data) + - [Extract Set-Cookie Header](#extract-set-cookie-header) + - [Post data using a file stream](#post-data-using-a-file-stream) + - [Post with form-data (detect multipart)](#post-with-form-data-detect-multipart) + - [Request cancellation with AbortSignal](#request-cancellation-with-abortsignal) + - [API](#api) + - [fetch(url[, options])](#fetchurl-options) + - [Options](#options) + - [Default Headers](#default-headers) + - [Custom Agent](#custom-agent) + - [Class: Request](#class-request) + - [new Request(input[, options])](#new-requestinput-options) + - [Class: Response](#class-response) + - [new Response([body[, options]])](#new-responsebody-options) + - [response.ok](#responseok) + - [response.redirected](#responseredirected) + - [Class: Headers](#class-headers) + - [new Headers([init])](#new-headersinit) + - [Interface: Body](#interface-body) + - [body.body](#bodybody) + - [body.bodyUsed](#bodybodyused) + - [body.arrayBuffer()](#bodyarraybuffer) + - [body.blob()](#bodyblob) + - [body.json()](#bodyjson) + - [body.text()](#bodytext) + - [body.buffer()](#bodybuffer) + - [body.textConverted()](#bodytextconverted) + - [Class: FetchError](#class-fetcherror) + - [Class: AbortError](#class-aborterror) + - [Acknowledgement](#acknowledgement) + - [License](#license) @@ -85,7 +83,7 @@ See Matt Andrews' [isomorphic-fetch](https://github.com/matthew-andrews/isomorph ## Installation -Current stable release (`2.x`) +Current stable release (`3.x`) ```sh $ npm install node-fetch @@ -96,13 +94,13 @@ $ npm install node-fetch We suggest you load the module via `require`, pending the stabalizing of es modules in node: ```js -const fetch = require("node-fetch"); +const fetch = require('node-fetch'); ``` -If you are using a Promise library other than native, set it through fetch.Promise: +If you are using a Promise library other than native, set it through `fetch.Promise`: ```js -const Bluebird = require("bluebird"); +const Bluebird = require('bluebird'); fetch.Promise = Bluebird; ``` @@ -133,12 +131,12 @@ For versions of node earlier than 12.x, use this `globalThis` [polyfill](https:/ ## Common Usage -NOTE: The documentation below is up-to-date with `2.x` releases, [see `1.x` readme](https://github.com/bitinn/node-fetch/blob/1.x/README.md), [changelog](https://github.com/bitinn/node-fetch/blob/1.x/CHANGELOG.md) and [2.x upgrade guide](UPGRADE-GUIDE.md) for the differences. +NOTE: The documentation below is up-to-date with `3.x` releases, [see `2.x` readme](https://github.com/bitinn/node-fetch/blob/2.x/README.md), [changelog](https://github.com/bitinn/node-fetch/blob/2.x/CHANGELOG.md) and [3.x upgrade guide](UPGRADE-GUIDE.md) for the differences. ### Plain text or HTML ```js -fetch("https://github.com/") +fetch('https://github.com/') .then(res => res.text()) .then(body => console.log(body)); ``` @@ -146,7 +144,7 @@ fetch("https://github.com/") ### JSON ```js -fetch("https://api.github.com/users/github") +fetch('https://api.github.com/users/github') .then(res => res.json()) .then(json => console.log(json)); ``` @@ -154,7 +152,7 @@ fetch("https://api.github.com/users/github") ### Simple Post ```js -fetch("https://httpbin.org/post", { method: "POST", body: "a=1" }) +fetch('https://httpbin.org/post', {method: 'POST', body: 'a=1'}) .then(res => res.json()) // expecting a json response .then(json => console.log(json)); ``` @@ -162,12 +160,12 @@ fetch("https://httpbin.org/post", { method: "POST", body: "a=1" }) ### Post with JSON ```js -const body = { a: 1 }; +const body = {a: 1}; -fetch("https://httpbin.org/post", { - method: "post", +fetch('https://httpbin.org/post', { + method: 'post', body: JSON.stringify(body), - headers: { "Content-Type": "application/json" } + headers: {'Content-Type': 'application/json'} }) .then(res => res.json()) .then(json => console.log(json)); @@ -175,17 +173,15 @@ fetch("https://httpbin.org/post", { ### Post with form parameters -`URLSearchParams` is available in Node.js as of v7.5.0. See [official documentation](https://nodejs.org/api/url.html#url_class_urlsearchparams) for more usage methods. +`URLSearchParams` is available on the global object in Node.js as of v10.0.0. See [official documentation](https://nodejs.org/api/url.html#url_class_urlsearchparams) for more usage methods. NOTE: The `Content-Type` header is only set automatically to `x-www-form-urlencoded` when an instance of `URLSearchParams` is given as such: ```js -const { URLSearchParams } = require("url"); - const params = new URLSearchParams(); -params.append("a", 1); +params.append('a', 1); -fetch("https://httpbin.org/post", { method: "POST", body: params }) +fetch('https://httpbin.org/post', {method: 'POST', body: params}) .then(res => res.json()) .then(json => console.log(json)); ``` @@ -197,7 +193,7 @@ NOTE: 3xx-5xx responses are _NOT_ exceptions, and should be handled in `then()`, Adding a catch to the fetch promise chain will catch _all_ exceptions, such as errors originating from node core libraries, like network errors, and operational errors which are instances of FetchError. See the [error handling document](ERROR-HANDLING.md) for more details. ```js -fetch("https://domain.invalid/").catch(err => console.error(err)); +fetch('https://domain.invalid/').catch(err => console.error(err)); ``` ### Handling client and server errors @@ -214,9 +210,9 @@ function checkStatus(res) { } } -fetch("https://httpbin.org/status/400") +fetch('https://httpbin.org/status/400') .then(checkStatus) - .then(res => console.log("will not get here...")); + .then(res => console.log('will not get here...')); ``` ## Advanced Usage @@ -227,9 +223,9 @@ The "Node.js way" is to use streams when possible: ```js fetch( - "https://assets-cdn.github.com/images/modules/logos_page/Octocat.png" + 'https://assets-cdn.github.com/images/modules/logos_page/Octocat.png' ).then(res => { - const dest = fs.createWriteStream("./octocat.png"); + const dest = fs.createWriteStream('./octocat.png'); res.body.pipe(dest); }); ``` @@ -239,9 +235,9 @@ fetch( If you prefer to cache binary data in full, use buffer(). (NOTE: buffer() is a `node-fetch` only API) ```js -const fileType = require("file-type"); +const fileType = require('file-type'); -fetch("https://assets-cdn.github.com/images/modules/logos_page/Octocat.png") +fetch('https://assets-cdn.github.com/images/modules/logos_page/Octocat.png') .then(res => res.buffer()) .then(buffer => fileType(buffer)) .then(type => { @@ -252,12 +248,12 @@ fetch("https://assets-cdn.github.com/images/modules/logos_page/Octocat.png") ### Accessing Headers and other Meta data ```js -fetch("https://github.com/").then(res => { +fetch('https://github.com/').then(res => { 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(res.headers.get('content-type')); }); ``` @@ -268,18 +264,18 @@ Unlike browsers, you can access raw `Set-Cookie` headers manually using `Headers ```js fetch(url).then(res => { // returns an array of values, instead of a string of comma-separated values - console.log(res.headers.raw()["set-cookie"]); + console.log(res.headers.raw()['set-cookie']); }); ``` ### Post data using a file stream ```js -const { createReadStream } = require("fs"); +const {createReadStream} = require('fs'); -const stream = createReadStream("input.txt"); +const stream = createReadStream('input.txt'); -fetch("https://httpbin.org/post", { method: "POST", body: stream }) +fetch('https://httpbin.org/post', {method: 'POST', body: stream}) .then(res => res.json()) .then(json => console.log(json)); ``` @@ -287,12 +283,12 @@ fetch("https://httpbin.org/post", { method: "POST", body: stream }) ### Post with form-data (detect multipart) ```js -const FormData = require("form-data"); +const FormData = require('form-data'); const form = new FormData(); -form.append("a", 1); +form.append('a', 1); -fetch("https://httpbin.org/post", { method: "POST", body: form }) +fetch('https://httpbin.org/post', {method: 'POST', body: form}) .then(res => res.json()) .then(json => console.log(json)); @@ -300,15 +296,15 @@ fetch("https://httpbin.org/post", { method: "POST", body: form }) // NOTE: getHeaders() is non-standard API const form = new FormData(); -form.append("a", 1); +form.append('a', 1); const options = { - method: "POST", + method: 'POST', body: form, headers: form.getHeaders() }; -fetch("https://httpbin.org/post", options) +fetch('https://httpbin.org/post', options) .then(res => res.json()) .then(json => console.log(json)); ``` @@ -322,21 +318,21 @@ You may cancel requests with `AbortController`. A suggested implementation is [` An example of timing out a request after 150ms could be achieved as follows: ```js -import AbortController from "abort-controller"; +import AbortController from 'abort-controller'; const controller = new AbortController(); const timeout = setTimeout(() => { controller.abort(); }, 150); -fetch(url, { signal: controller.signal }) +fetch(url, {signal: controller.signal}) .then(res => res.json()) .then( data => { useData(data); }, err => { - if (err.name === "AbortError") { + if (err.name === 'AbortError') { // request was aborted } } @@ -422,7 +418,7 @@ const httpsAgent = new https.Agent({ const options = { agent: function(_parsedURL) { - if (_parsedURL.protocol == "http:") { + if (_parsedURL.protocol == 'http:') { return httpAgent; } else { return httpsAgent; @@ -524,19 +520,19 @@ Construct a new `Headers` object. `init` can be either `null`, a `Headers` objec // Example adapted from https://fetch.spec.whatwg.org/#example-headers-class const meta = { - "Content-Type": "text/xml", - "Breaking-Bad": "<3" + 'Content-Type': 'text/xml', + 'Breaking-Bad': '<3' }; const headers = new Headers(meta); // The above is equivalent to -const meta = [["Content-Type", "text/xml"], ["Breaking-Bad", "<3"]]; +const meta = [['Content-Type', 'text/xml'], ['Breaking-Bad', '<3']]; const headers = new Headers(meta); // You can in fact use any iterable objects, like a Map or even another Headers const meta = new Map(); -meta.set("Content-Type", "text/xml"); -meta.set("Breaking-Bad", "<3"); +meta.set('Content-Type', 'text/xml'); +meta.set('Breaking-Bad', '<3'); const headers = new Headers(meta); const copyOfHeaders = new Headers(headers); ``` From 6d22a65dd3e551ca2ce36fce7518e70571bb16db Mon Sep 17 00:00:00 2001 From: Antoni Kepinski Date: Wed, 30 Oct 2019 21:31:19 +0100 Subject: [PATCH 059/157] Verify examples with RunKit and edit them if necessary --- README.md | 51 +++++++++++++++++++++++++++++++++++++++------------ 1 file changed, 39 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index 65059391f..dbe666a40 100644 --- a/README.md +++ b/README.md @@ -100,6 +100,7 @@ const fetch = require('node-fetch'); 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; @@ -108,6 +109,8 @@ fetch.Promise = Bluebird; If you want to patch the global object in node: ```js +const fetch = require('node-fetch'); + if (!globalThis.fetch) { globalThis.fetch = fetch; } @@ -136,6 +139,8 @@ NOTE: The documentation below is up-to-date with `3.x` releases, [see `2.x` read ### Plain text or HTML ```js +const fetch = require('node-fetch'); + fetch('https://github.com/') .then(res => res.text()) .then(body => console.log(body)); @@ -144,6 +149,8 @@ fetch('https://github.com/') ### JSON ```js +const fetch = require('node-fetch'); + fetch('https://api.github.com/users/github') .then(res => res.json()) .then(json => console.log(json)); @@ -152,6 +159,8 @@ fetch('https://api.github.com/users/github') ### Simple Post ```js +const fetch = require('node-fetch'); + fetch('https://httpbin.org/post', {method: 'POST', body: 'a=1'}) .then(res => res.json()) // expecting a json response .then(json => console.log(json)); @@ -160,6 +169,8 @@ fetch('https://httpbin.org/post', {method: 'POST', body: 'a=1'}) ### Post with JSON ```js +const fetch = require('node-fetch'); + const body = {a: 1}; fetch('https://httpbin.org/post', { @@ -178,6 +189,8 @@ fetch('https://httpbin.org/post', { NOTE: The `Content-Type` header is only set automatically to `x-www-form-urlencoded` when an instance of `URLSearchParams` is given as such: ```js +const fetch = require('node-fetch'); + const params = new URLSearchParams(); params.append('a', 1); @@ -193,6 +206,8 @@ NOTE: 3xx-5xx responses are _NOT_ exceptions, and should be handled in `then()`, Adding a catch to the fetch promise chain will catch _all_ exceptions, such as errors originating from node core libraries, like network errors, and operational errors which are instances of FetchError. See the [error handling document](ERROR-HANDLING.md) for more details. ```js +const fetch = require('node-fetch'); + fetch('https://domain.invalid/').catch(err => console.error(err)); ``` @@ -201,6 +216,8 @@ fetch('https://domain.invalid/').catch(err => console.error(err)); It is common to create a helper function to check that the response contains no client (4xx) or server (5xx) error responses: ```js +const fetch = require('node-fetch'); + function checkStatus(res) { if (res.ok) { // res.status >= 200 && res.status < 300 @@ -222,8 +239,11 @@ fetch('https://httpbin.org/status/400') The "Node.js way" is to use streams when possible: ```js +const {createWriteStream} = require('fs'); +const fetch = require('node-fetch'); + fetch( - 'https://assets-cdn.github.com/images/modules/logos_page/Octocat.png' + 'https://octodex.github.com/images/Fintechtocat.png' ).then(res => { const dest = fs.createWriteStream('./octocat.png'); res.body.pipe(dest); @@ -235,19 +255,22 @@ fetch( If you prefer to cache binary data in full, use buffer(). (NOTE: buffer() is a `node-fetch` only API) ```js +const fetch = require('node-fetch'); const fileType = require('file-type'); -fetch('https://assets-cdn.github.com/images/modules/logos_page/Octocat.png') +fetch('https://octodex.github.com/images/Fintechtocat.png') .then(res => res.buffer()) .then(buffer => fileType(buffer)) .then(type => { - /* ... */ + console.log(type); }); ``` ### Accessing Headers and other Meta data ```js +const fetch = require('node-fetch'); + fetch('https://github.com/').then(res => { console.log(res.ok); console.log(res.status); @@ -262,7 +285,9 @@ fetch('https://github.com/').then(res => { Unlike browsers, you can access raw `Set-Cookie` headers manually using `Headers.raw()`, this is a `node-fetch` only API. ```js -fetch(url).then(res => { +const fetch = require('node-fetch'); + +fetch('https://example.com').then(res => { // returns an array of values, instead of a string of comma-separated values console.log(res.headers.raw()['set-cookie']); }); @@ -272,6 +297,7 @@ fetch(url).then(res => { ```js const {createReadStream} = require('fs'); +const fetch = require('node-fetch'); const stream = createReadStream('input.txt'); @@ -283,6 +309,7 @@ fetch('https://httpbin.org/post', {method: 'POST', body: stream}) ### Post with form-data (detect multipart) ```js +const fetch = require('node-fetch'); const FormData = require('form-data'); const form = new FormData(); @@ -295,9 +322,6 @@ fetch('https://httpbin.org/post', {method: 'POST', body: form}) // OR, using custom headers // NOTE: getHeaders() is non-standard API -const form = new FormData(); -form.append('a', 1); - const options = { method: 'POST', body: form, @@ -311,21 +335,20 @@ fetch('https://httpbin.org/post', options) ### Request cancellation with AbortSignal -> NOTE: You may only cancel streamed requests on Node >= v8.0.0 - You may cancel requests with `AbortController`. A suggested implementation is [`abort-controller`](https://www.npmjs.com/package/abort-controller). An example of timing out a request after 150ms could be achieved as follows: ```js -import AbortController from 'abort-controller'; +const fetch = require('node-fetch'); +const AbortController = require('abort-controller'); const controller = new AbortController(); const timeout = setTimeout(() => { controller.abort(); }, 150); -fetch(url, {signal: controller.signal}) +fetch('https://example.com', {signal: controller.signal}) .then(res => res.json()) .then( data => { @@ -333,7 +356,7 @@ fetch(url, {signal: controller.signal}) }, err => { if (err.name === 'AbortError') { - // request was aborted + console.log('request was aborted'); } } ) @@ -409,6 +432,9 @@ See [`http.Agent`](https://nodejs.org/api/http.html#http_new_agent_options) for In addition, `agent` option accepts a function that returns http(s).Agent instance given current [URL](https://nodejs.org/api/url.html), this is useful during a redirection chain across HTTP and HTTPS protocol. ```js +const http = require('http'); +const https = require('https'); + const httpAgent = new http.Agent({ keepAlive: true }); @@ -518,6 +544,7 @@ Construct a new `Headers` object. `init` can be either `null`, a `Headers` objec ```js // Example adapted from https://fetch.spec.whatwg.org/#example-headers-class +const Headers = require('node-fetch'); const meta = { 'Content-Type': 'text/xml', From 55f42384e4892d6fc8249a4b029537fd1e090840 Mon Sep 17 00:00:00 2001 From: Antoni Kepinski Date: Wed, 30 Oct 2019 21:37:02 +0100 Subject: [PATCH 060/157] Add information about TypeScript support --- README.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/README.md b/README.md index dbe666a40..87f174be7 100644 --- a/README.md +++ b/README.md @@ -55,6 +55,7 @@ A light-weight module that brings `window.fetch` to Node.js - [body.textConverted()](#bodytextconverted) - [Class: FetchError](#class-fetcherror) - [Class: AbortError](#class-aborterror) + - [TypeScript](#typescript) - [Acknowledgement](#acknowledgement) - [License](#license) @@ -638,6 +639,16 @@ An operational error in the fetching process. See [ERROR-HANDLING.md][] for more An Error thrown when the request is aborted in response to an `AbortSignal`'s `abort` event. It has a `name` property of `AbortError`. See [ERROR-HANDLING.MD][] for more info. +## TypeScript + +Since `3.x` types are bundled with `node-fetch`, so you don't need to install any additional packages. + +For older versions please use the type definitions from [DefinitelyTyped](https://github.com/DefinitelyTyped/DefinitelyTyped): + +```sh +$ npm install --save-dev @types/node-fetch +``` + ## Acknowledgement Thanks to [github/fetch](https://github.com/github/fetch) for providing a solid implementation reference. From 9ae768ce4e90a53331ee7e19ca55c62c125c8f1e Mon Sep 17 00:00:00 2001 From: Antoni Kepinski Date: Wed, 30 Oct 2019 21:45:49 +0100 Subject: [PATCH 061/157] Document the new `highWaterMark` option --- README.md | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/README.md b/README.md index 87f174be7..dff58b469 100644 --- a/README.md +++ b/README.md @@ -36,6 +36,7 @@ A light-weight module that brings `window.fetch` to Node.js - [Options](#options) - [Default Headers](#default-headers) - [Custom Agent](#custom-agent) + - [Custom highWaterMark](#custom-highwatermark) - [Class: Request](#class-request) - [new Request(input[, options])](#new-requestinput-options) - [Class: Response](#class-response) @@ -454,6 +455,20 @@ const options = { }; ``` + + +#### Custom highWaterMark + +Stream on Node.js have a smaller internal buffer size (16Kb, aka `highWaterMark`) from client-side browsers (>1Mb, not consistent across browsers). Because of that, when you are writing an isomorphic app and using `res.clone()`, it will hang with large response in Node. + +Since `3.x` you are able to modify the `highWaterMark` option: + +```js +const fetch = require('node-fetch'); + +fetch('https://example.com', {highWaterMark: 10}).then(res => res.clone().buffer()); +``` + ### Class: Request From ab8b8400e788a1eabfecec0b37991bd1a45f61b7 Mon Sep 17 00:00:00 2001 From: Antoni Kepinski Date: Wed, 30 Oct 2019 21:51:31 +0100 Subject: [PATCH 062/157] Add Discord badge & information about Open Collective --- README.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/README.md b/README.md index dff58b469..d60e564e0 100644 --- a/README.md +++ b/README.md @@ -4,9 +4,16 @@ [![build status][travis-image]][travis-url] [![coverage status][codecov-image]][codecov-url] [![install size][install-size-image]][install-size-url] +[![Discord][discord-image]][discord-url] A light-weight module that brings `window.fetch` to Node.js +--- + +If you and/or your company is using `node-fetch`, consider supporting us on our Open Collective: + +[![Backers][opencollective-image]][opencollective-url] + - [node-fetch](#node-fetch) @@ -682,6 +689,10 @@ MIT [codecov-url]: https://codecov.io/gh/bitinn/node-fetch [install-size-image]: https://flat.badgen.net/packagephobia/install/node-fetch [install-size-url]: https://packagephobia.now.sh/result?p=node-fetch +[discord-image]: https://img.shields.io/discord/619915844268326952?color=%237289DA&label=Discord&style=flat-square +[discord-url]: https://discord.gg/Zxbndcm +[opencollective-image]: https://opencollective.com/node-fetch/backers.svg +[opencollective-url]: https://opencollective.com/node-fetch [whatwg-fetch]: https://fetch.spec.whatwg.org/ [response-init]: https://fetch.spec.whatwg.org/#responseinit [node-readable]: https://nodejs.org/api/stream.html#stream_readable_streams From 3ea46b6824354c3626adebea40ea2bb457c6cbee Mon Sep 17 00:00:00 2001 From: Antoni Kepinski Date: Wed, 30 Oct 2019 21:55:01 +0100 Subject: [PATCH 063/157] Style change --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index d60e564e0..1353d478f 100644 --- a/README.md +++ b/README.md @@ -8,12 +8,12 @@ A light-weight module that brings `window.fetch` to Node.js ---- - -If you and/or your company is using `node-fetch`, consider supporting us on our Open Collective: +**Consider supporting us on our Open Collective:** [![Backers][opencollective-image]][opencollective-url] +--- + - [node-fetch](#node-fetch) From ae71303213a2d254dd570848f8fb7cae8acbbd31 Mon Sep 17 00:00:00 2001 From: Antoni Kepinski Date: Wed, 30 Oct 2019 22:09:26 +0100 Subject: [PATCH 064/157] Edit acknowledgement & add "Team" section --- README.md | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 1353d478f..b57dfddeb 100644 --- a/README.md +++ b/README.md @@ -65,6 +65,8 @@ A light-weight module that brings `window.fetch` to Node.js - [Class: AbortError](#class-aborterror) - [TypeScript](#typescript) - [Acknowledgement](#acknowledgement) + - [Team](#team) + - [Former](#former) - [License](#license) @@ -675,7 +677,16 @@ $ npm install --save-dev @types/node-fetch Thanks to [github/fetch](https://github.com/github/fetch) for providing a solid implementation reference. -`node-fetch` v1 was maintained by [@bitinn](https://github.com/bitinn); v2 was maintained by [@TimothyGu](https://github.com/timothygu), [@bitinn](https://github.com/bitinn) and [@jimmywarting](https://github.com/jimmywarting); v2 readme is written by [@jkantr](https://github.com/jkantr). +## Team + +[![David Frank](https://github.com/bitinn.png?size=100)](https://github.com/bitinn) | [![Jimmy Wärting](https://github.com/jimmywarting.png?size=100)](https://github.com/jimmywarting) | [![Antoni Kepinski](https://github.com/xxczaki.png?size=100)](https://github.com/xxczaki) | [![Richie Bendall](https://github.com/Richienb.png?size=100)](https://github.com/Richienb) | [![Gregor Martynus](https://github.com/gr2m.png?size=100)](https://github.com/gr2m) +---|---|--- +[David Frank](https://bitinn.net/) | [Jimmy Wärting](https://jimmy.warting.se/) | [Antoni Kepinski](https://kepinski.me) | [Richie Bendall](https://www.richie-bendall.ml/) | [Gregor Martynus](https://twitter.com/gr2m) + +###### Former + +- [Timothy Gu](https://github.com/timothygu) +- [Jared Kantrowitz](https://github.com/jkantr) ## License From b3a9f4dd3507312187349bfa29e38028a940bd31 Mon Sep 17 00:00:00 2001 From: Antoni Kepinski Date: Wed, 30 Oct 2019 22:10:53 +0100 Subject: [PATCH 065/157] fix table --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index b57dfddeb..9c9a8daeb 100644 --- a/README.md +++ b/README.md @@ -680,7 +680,7 @@ Thanks to [github/fetch](https://github.com/github/fetch) for providing a solid ## Team [![David Frank](https://github.com/bitinn.png?size=100)](https://github.com/bitinn) | [![Jimmy Wärting](https://github.com/jimmywarting.png?size=100)](https://github.com/jimmywarting) | [![Antoni Kepinski](https://github.com/xxczaki.png?size=100)](https://github.com/xxczaki) | [![Richie Bendall](https://github.com/Richienb.png?size=100)](https://github.com/Richienb) | [![Gregor Martynus](https://github.com/gr2m.png?size=100)](https://github.com/gr2m) ----|---|--- +---|---|---|---|--- [David Frank](https://bitinn.net/) | [Jimmy Wärting](https://jimmy.warting.se/) | [Antoni Kepinski](https://kepinski.me) | [Richie Bendall](https://www.richie-bendall.ml/) | [Gregor Martynus](https://twitter.com/gr2m) ###### Former From a025ddec0588b506b15b5afe38ee062eadd5dc94 Mon Sep 17 00:00:00 2001 From: Antoni Kepinski Date: Thu, 31 Oct 2019 16:22:52 +0100 Subject: [PATCH 066/157] Format example code to use xo style --- ERROR-HANDLING.md | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/ERROR-HANDLING.md b/ERROR-HANDLING.md index 89d5691c1..fcbc5e6a5 100644 --- a/ERROR-HANDLING.md +++ b/ERROR-HANDLING.md @@ -9,11 +9,13 @@ The basics: - A cancelled request is rejected with an [`AbortError`](https://github.com/bitinn/node-fetch/blob/master/README.md#class-aborterror). You can check if the reason for rejection was that the request was aborted by checking the `Error`'s `name` is `AbortError`. ```js -fetch(url, { signal }).catch(err => { - if (err.name === 'AbortError') { - // request was aborted +const fetch = required('node-fetch'); + +fetch(url, {signal}).catch(error => { + if (error.name === 'AbortError') { + console.log('request was aborted'); } -}) +}); ``` - All [operational errors][joyent-guide] *other than aborted requests* are rejected with a [FetchError](https://github.com/bitinn/node-fetch/blob/master/README.md#class-fetcherror). You can handle them all through the promise `catch` clause. From c4b4f90c4be503d72dcf0cfbc18ced4f55e0a528 Mon Sep 17 00:00:00 2001 From: Antoni Kepinski Date: Fri, 1 Nov 2019 18:47:51 +0100 Subject: [PATCH 067/157] chore: v3 release changelog --- CHANGELOG.md | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 188fcd399..389b603c1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,31 @@ Changelog ========= +# 3.x release + +## v3.0.0 + + + +- **Breaking:** minimum supported Node.js version is now 10. +- Enhance: added new node-fetch-only option: `highWaterMark`. +- Enhance: `AbortError` now uses a w3c defined message. +- Enhance: data URI support. +- Enhance: drop existing blob implementation code and use fetch-blob as dependency instead. +- Enhance: modernise the code behind `FetchError` and `AbortError`. +- Enhance: replace deprecated `url.parse()` and `url.replace()` with the new WHATWG `new URL()` +- Fix: `Response.statusText` no longer sets a default message derived from the HTTP status code. +- Fix: missing response stream error events. +- Fix: do not use constructor.name to check object. +- Fix: convert `Content-Encoding` to lowercase. +- Fix: propagate size and timeout to cloned response. +- Other: bundle TypeScript types. +- Other: replace Rollup with Microbundle. +- Other: introduce linting to the project. +- Other: simplify Travis CI build matrix. +- Other: dev dependency update. +- Other: readme update. + # 2.x release From bb3f6c439203f4e0f1fabea8fdc89f1a072bf3a0 Mon Sep 17 00:00:00 2001 From: Antoni Kepinski Date: Fri, 1 Nov 2019 18:57:54 +0100 Subject: [PATCH 068/157] Add the recommended way to fix `highWaterMark` issues --- README.md | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 9c9a8daeb..8da1ee07e 100644 --- a/README.md +++ b/README.md @@ -468,9 +468,24 @@ const options = { #### Custom highWaterMark -Stream on Node.js have a smaller internal buffer size (16Kb, aka `highWaterMark`) from client-side browsers (>1Mb, not consistent across browsers). Because of that, when you are writing an isomorphic app and using `res.clone()`, it will hang with large response in Node. +Stream on Node.js have a smaller internal buffer size (16Kb, aka `highWaterMark`) from client-side browsers (>1Mb, not consistent across browsers). Because of that, when you are writing an isomorphic app and using `res.clone()`, it will hang with large response in Node. -Since `3.x` you are able to modify the `highWaterMark` option: +The recommended way to fix this problem is to resolve cloned response in parallel: + +```js +const fetch = require('node-fetch'); + +fetch('https://example.com').then(res => { + const r1 = res.clone(); + + return Promise.all([res.json(), r1.text()]).then(results => { + console.log(results[0]); + console.log(results[1]); + }); +}); +``` + +If for some reason you don't like the solution above, since `3.x` you are able to modify the `highWaterMark` option: ```js const fetch = require('node-fetch'); From de11379f8cddbc4d2a0464b9698a7e61e4353af9 Mon Sep 17 00:00:00 2001 From: Richie Bendall Date: Sat, 2 Nov 2019 13:17:12 +1300 Subject: [PATCH 069/157] docs: Add simple Runkit example --- package.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index 6e76990c7..e2736b9c9 100644 --- a/package.json +++ b/package.json @@ -112,5 +112,6 @@ ], "sourceMap": false, "instrument": false - } + }, + "runkitExample": "const fetch = require('node-fetch')" } From 0770f83298e49335ba2846b11e0959dfeeda0327 Mon Sep 17 00:00:00 2001 From: Richie Bendall Date: Sun, 3 Nov 2019 08:46:12 +1300 Subject: [PATCH 070/157] fix: Properly set the name of the errors. Signed-off-by: Richie Bendall --- src/errors/abort-error.js | 1 + src/errors/fetch-error.js | 1 + src/utils/is.js | 2 +- 3 files changed, 3 insertions(+), 1 deletion(-) diff --git a/src/errors/abort-error.js b/src/errors/abort-error.js index f3d8f096b..27c7545ff 100644 --- a/src/errors/abort-error.js +++ b/src/errors/abort-error.js @@ -19,6 +19,7 @@ export default class AbortError extends Error { 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); diff --git a/src/errors/fetch-error.js b/src/errors/fetch-error.js index 28a2ba577..3fdd378e9 100644 --- a/src/errors/fetch-error.js +++ b/src/errors/fetch-error.js @@ -19,6 +19,7 @@ export default class FetchError extends Error { this.message = message; this.type = type; this.name = 'FetchError'; + this[Symbol.toStringTag] = 'FetchError'; // When err.type is `system`, err.erroredSysCall contains system error and err.code contains system error code if (systemError) { diff --git a/src/utils/is.js b/src/utils/is.js index 3242d2d4b..ce745bd30 100644 --- a/src/utils/is.js +++ b/src/utils/is.js @@ -74,5 +74,5 @@ export function isArrayBuffer(obj) { * @return {boolean} */ export function isAbortError(obj) { - return obj.name === 'AbortError'; + return obj[NAME] === 'AbortError'; } From 4006f233e34ec1f690aa9b7c0b114a762fb6ac3f Mon Sep 17 00:00:00 2001 From: Richie Bendall Date: Sun, 3 Nov 2019 08:52:25 +1300 Subject: [PATCH 071/157] docs: Add AbortError to documented types Signed-off-by: Richie Bendall --- types/index.d.ts | 293 ++++++++++++++++++++++++----------------------- 1 file changed, 151 insertions(+), 142 deletions(-) diff --git a/types/index.d.ts b/types/index.d.ts index 9089f61f7..7ad448c73 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -17,198 +17,207 @@ import { URLSearchParams, URL } from "url"; import { AbortSignal } from "./externals"; export class Request extends Body { - constructor(input: string | { href: string } | Request, init?: RequestInit); - clone(): Request; - context: RequestContext; - headers: Headers; - method: string; - redirect: RequestRedirect; - referrer: string; - url: string; - - // node-fetch extensions to the whatwg/fetch spec - agent?: Agent | ((parsedUrl: URL) => Agent); - compress: boolean; - counter: number; - follow: number; - hostname: string; - port?: number; - protocol: string; - size: number; - timeout: number; - highWaterMark?: number; + constructor(input: string | { href: string } | Request, init?: RequestInit); + clone(): Request; + context: RequestContext; + headers: Headers; + method: string; + redirect: RequestRedirect; + referrer: string; + url: string; + + // node-fetch extensions to the whatwg/fetch spec + agent?: Agent | ((parsedUrl: URL) => Agent); + compress: boolean; + counter: number; + follow: number; + hostname: string; + port?: number; + protocol: string; + size: number; + timeout: number; + highWaterMark?: number; } export interface RequestInit { - // whatwg/fetch standard options - body?: BodyInit; - headers?: HeadersInit; - method?: string; - redirect?: RequestRedirect; - signal?: AbortSignal | null; - - // node-fetch extensions - agent?: Agent | ((parsedUrl: URL) => Agent); // =null http.Agent instance, allows custom proxy, certificate etc. - compress?: boolean; // =true support gzip/deflate content encoding. false to disable - follow?: number; // =20 maximum redirect count. 0 to not follow redirect - size?: number; // =0 maximum response body size in bytes. 0 to disable - timeout?: number; // =0 req/res timeout in ms, it resets on redirect. 0 to disable (OS limit applies) - highWaterMark?: number; // =16384 the maximum number of bytes to store in the internal buffer before ceasing to read from the underlying resource. - - // node-fetch does not support mode, cache or credentials options + // whatwg/fetch standard options + body?: BodyInit; + headers?: HeadersInit; + method?: string; + redirect?: RequestRedirect; + signal?: AbortSignal | null; + + // node-fetch extensions + agent?: Agent | ((parsedUrl: URL) => Agent); // =null http.Agent instance, allows custom proxy, certificate etc. + compress?: boolean; // =true support gzip/deflate content encoding. false to disable + follow?: number; // =20 maximum redirect count. 0 to not follow redirect + size?: number; // =0 maximum response body size in bytes. 0 to disable + timeout?: number; // =0 req/res timeout in ms, it resets on redirect. 0 to disable (OS limit applies) + highWaterMark?: number; // =16384 the maximum number of bytes to store in the internal buffer before ceasing to read from the underlying resource. + + // node-fetch does not support mode, cache or credentials options } export type RequestContext = - "audio" - | "beacon" - | "cspreport" - | "download" - | "embed" - | "eventsource" - | "favicon" - | "fetch" - | "font" - | "form" - | "frame" - | "hyperlink" - | "iframe" - | "image" - | "imageset" - | "import" - | "internal" - | "location" - | "manifest" - | "object" - | "ping" - | "plugin" - | "prefetch" - | "script" - | "serviceworker" - | "sharedworker" - | "style" - | "subresource" - | "track" - | "video" - | "worker" - | "xmlhttprequest" - | "xslt"; + "audio" + | "beacon" + | "cspreport" + | "download" + | "embed" + | "eventsource" + | "favicon" + | "fetch" + | "font" + | "form" + | "frame" + | "hyperlink" + | "iframe" + | "image" + | "imageset" + | "import" + | "internal" + | "location" + | "manifest" + | "object" + | "ping" + | "plugin" + | "prefetch" + | "script" + | "serviceworker" + | "sharedworker" + | "style" + | "subresource" + | "track" + | "video" + | "worker" + | "xmlhttprequest" + | "xslt"; export type RequestMode = "cors" | "no-cors" | "same-origin"; export type RequestRedirect = "error" | "follow" | "manual"; export type RequestCredentials = "omit" | "include" | "same-origin"; export type RequestCache = - "default" - | "force-cache" - | "no-cache" - | "no-store" - | "only-if-cached" - | "reload"; + "default" + | "force-cache" + | "no-cache" + | "no-store" + | "only-if-cached" + | "reload"; export class Headers implements Iterable<[string, string]> { - constructor(init?: HeadersInit); - forEach(callback: (value: string, name: string) => void): void; - append(name: string, value: string): void; - delete(name: string): void; - get(name: string): string | null; - getAll(name: string): string[]; - has(name: string): boolean; - raw(): { [k: string]: string[] }; - set(name: string, value: string): void; - - // Iterator methods - entries(): Iterator<[string, string]>; - keys(): Iterator; - values(): Iterator<[string]>; - [Symbol.iterator](): Iterator<[string, string]>; + constructor(init?: HeadersInit); + forEach(callback: (value: string, name: string) => void): void; + append(name: string, value: string): void; + delete(name: string): void; + get(name: string): string | null; + getAll(name: string): string[]; + has(name: string): boolean; + raw(): { [k: string]: string[] }; + set(name: string, value: string): void; + + // Iterator methods + entries(): Iterator<[string, string]>; + keys(): Iterator; + values(): Iterator<[string]>; + [Symbol.iterator](): Iterator<[string, string]>; } type BlobPart = ArrayBuffer | ArrayBufferView | Blob | string; interface BlobOptions { - type?: string; - endings?: "transparent" | "native"; + type?: string; + endings?: "transparent" | "native"; } export class Blob { - constructor(blobParts?: BlobPart[], options?: BlobOptions); - readonly type: string; - readonly size: number; - slice(start?: number, end?: number): Blob; + constructor(blobParts?: BlobPart[], options?: BlobOptions); + readonly type: string; + readonly size: number; + slice(start?: number, end?: number): Blob; } export class Body { - constructor(body?: any, opts?: { size?: number; timeout?: number }); - arrayBuffer(): Promise; - blob(): Promise; - body: NodeJS.ReadableStream; - bodyUsed: boolean; - buffer(): Promise; - json(): Promise; - size: number; - text(): Promise; - textConverted(): Promise; - timeout: number; + constructor(body?: any, opts?: { size?: number; timeout?: number }); + arrayBuffer(): Promise; + blob(): Promise; + body: NodeJS.ReadableStream; + bodyUsed: boolean; + buffer(): Promise; + json(): Promise; + size: number; + text(): Promise; + textConverted(): Promise; + timeout: number; } export class FetchError extends Error { - name: "FetchError"; - constructor(message: string, type: string, systemError?: string); - type: string; - code?: string; - errno?: string; + name: "FetchError"; + [Symbol.toStringTag]: "FetchError" + constructor(message: string, type: string, systemError?: string); + type: string; + code?: string; + errno?: string; +} + +export class AbortError extends Error { + name: "AbortError"; + [Symbol.toStringTag]: "AbortError" + constructor(message: string, type: string, systemError?: string); + type: string; + message: string; } export class Response extends Body { - constructor(body?: BodyInit, init?: ResponseInit); - static error(): Response; - static redirect(url: string, status: number): Response; - clone(): Response; - headers: Headers; - ok: boolean; - redirected: boolean; - status: number; - statusText: string; - type: ResponseType; + constructor(body?: BodyInit, init?: ResponseInit); + static error(): Response; + static redirect(url: string, status: number): Response; + clone(): Response; + headers: Headers; + ok: boolean; + redirected: boolean; + status: number; + statusText: string; + type: ResponseType; url: string; size: number; timeout: number; } export type ResponseType = - "basic" - | "cors" - | "default" - | "error" - | "opaque" - | "opaqueredirect"; + "basic" + | "cors" + | "default" + | "error" + | "opaque" + | "opaqueredirect"; export interface ResponseInit { - headers?: HeadersInit; - size?: number; - status?: number; - statusText?: string; - timeout?: number; - url?: string; + headers?: HeadersInit; + size?: number; + status?: number; + statusText?: string; + timeout?: number; + url?: string; } export type HeadersInit = Headers | string[][] | { [key: string]: string }; // HeaderInit is exported to support backwards compatibility. See PR #34382 export type HeaderInit = HeadersInit; export type BodyInit = - ArrayBuffer - | ArrayBufferView - | NodeJS.ReadableStream - | string - | URLSearchParams; + ArrayBuffer + | ArrayBufferView + | NodeJS.ReadableStream + | string + | URLSearchParams; export type RequestInfo = string | Request; declare function fetch( - url: RequestInfo, - init?: RequestInit + url: RequestInfo, + init?: RequestInit ): Promise; declare namespace fetch { - function isRedirect(code: number): boolean; + function isRedirect(code: number): boolean; } export default fetch; From d9c5c27d90b3ddc28f85d47099bd20e0bdf28638 Mon Sep 17 00:00:00 2001 From: Richie Bendall Date: Sun, 3 Nov 2019 08:54:13 +1300 Subject: [PATCH 072/157] docs: AbortError proper typing parameters Signed-off-by: Richie Bendall --- types/index.d.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/types/index.d.ts b/types/index.d.ts index 7ad448c73..8a503d43c 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -162,7 +162,7 @@ export class FetchError extends Error { export class AbortError extends Error { name: "AbortError"; [Symbol.toStringTag]: "AbortError" - constructor(message: string, type: string, systemError?: string); + constructor(message: string); type: string; message: string; } From 40fedfbc65e63e3b3644bed6b7fa268637f52582 Mon Sep 17 00:00:00 2001 From: Richie Bendall Date: Sun, 3 Nov 2019 09:52:16 +1300 Subject: [PATCH 073/157] docs: Add example code for Runkit Signed-off-by: Richie Bendall --- example.js | 27 +++++++++++++++++++++++++++ package.json | 6 ++++-- 2 files changed, 31 insertions(+), 2 deletions(-) create mode 100644 example.js diff --git a/example.js b/example.js new file mode 100644 index 000000000..ba41eda38 --- /dev/null +++ b/example.js @@ -0,0 +1,27 @@ +const fetch = require('node-fetch'); + +// Plain text or HTML +fetch('https://github.com/') + .then(res => res.text()) + .then(body => console.log(body)); + +// JSON +fetch('https://api.github.com/users/github') + .then(res => res.json()) + .then(json => console.log(json)); + +// Simple Post +fetch('https://httpbin.org/post', {method: 'POST', body: 'a=1'}) + .then(res => res.json()) + .then(json => console.log(json)); + +// Post with JSON +const body = {a: 1}; + +fetch('https://httpbin.org/post', { + method: 'post', + body: JSON.stringify(body), + headers: {'Content-Type': 'application/json'} +}) + .then(res => res.json()) + .then(json => console.log(json)); diff --git a/package.json b/package.json index 6e76990c7..82707b4dd 100644 --- a/package.json +++ b/package.json @@ -91,7 +91,8 @@ }, "ignores": [ "dist", - "test" + "test", + "example.js" ] }, "babel": { @@ -112,5 +113,6 @@ ], "sourceMap": false, "instrument": false - } + }, + "runkitExampleFilename": "example.js" } From aee95a743517301f6d63b2b72284d3414f0a14bf Mon Sep 17 00:00:00 2001 From: Antoni Kepinski Date: Mon, 4 Nov 2019 22:52:28 +0100 Subject: [PATCH 074/157] Replace microbundle with @pika/pack (#689) * gitignore the pkg/ directory * Move TypeScript types to the root of the project * Replace microbundle with @pika/pack * chore: Remove @pika/plugin-build-web and revert ./dist output directory Signed-off-by: Richie Bendall Co-authored-by: Richie Bendall --- types/externals.d.ts => externals.d.ts | 0 types/index.d.ts => index.d.ts | 2 +- package.json | 33 +++++++++++++++++++++----- 3 files changed, 28 insertions(+), 7 deletions(-) rename types/externals.d.ts => externals.d.ts (100%) rename types/index.d.ts => index.d.ts (99%) diff --git a/types/externals.d.ts b/externals.d.ts similarity index 100% rename from types/externals.d.ts rename to externals.d.ts diff --git a/types/index.d.ts b/index.d.ts similarity index 99% rename from types/index.d.ts rename to index.d.ts index 9089f61f7..dbd2a96a6 100644 --- a/types/index.d.ts +++ b/index.d.ts @@ -14,7 +14,7 @@ import { Agent } from "http"; import { URLSearchParams, URL } from "url"; -import { AbortSignal } from "./externals"; +import { AbortSignal } from "../externals"; export class Request extends Body { constructor(input: string | { href: string } | Request, init?: RequestInit); diff --git a/package.json b/package.json index e2736b9c9..050108485 100644 --- a/package.json +++ b/package.json @@ -14,7 +14,7 @@ "node": ">=10.0.0" }, "scripts": { - "build": "microbundle --external http,https,stream,zlib,utf8,fetch-blob --name fetch --target node --format es,cjs", + "build": "pika-pack --out dist/", "prepare": "npm run build", "test": "cross-env BABEL_ENV=test mocha --require @babel/register --throw-deprecation test/test.js", "report": "cross-env BABEL_ENV=coverage nyc --reporter lcov --reporter text mocha -R spec test/test.js", @@ -40,6 +40,11 @@ "@babel/core": "^7.6.4", "@babel/preset-env": "^7.6.3", "@babel/register": "^7.6.2", + "@pika/pack": "^0.5.0", + "@pika/plugin-build-node": "^0.7.1", + "@pika/plugin-build-types": "^0.7.1", + "@pika/plugin-copy-assets": "^0.7.1", + "@pika/plugin-standard-pkg": "^0.7.1", "abort-controller": "^3.0.0", "abortcontroller-polyfill": "^1.3.0", "chai": "^4.2.0", @@ -49,7 +54,6 @@ "codecov": "^3.6.1", "cross-env": "^6.0.3", "form-data": "^2.5.1", - "microbundle": "^0.11.0", "mocha": "^6.2.2", "nyc": "^14.1.1", "parted": "^0.1.1", @@ -65,9 +69,26 @@ "optionalDependencies": { "encoding": "^0.1.12" }, - "resolutions": { - "microbundle/rollup-plugin-typescript2/rollup-pluginutils/micromatch/braces": "^2.3.1", - "microbundle/rollup-plugin-postcss/cssnano/postcss-svgo/svgo/js-yaml": "^3.13.1" + "@pika/pack": { + "pipeline": [ + [ + "@pika/plugin-standard-pkg" + ], + [ + "@pika/plugin-build-node" + ], + [ + "@pika/plugin-build-types" + ], + [ + "@pika/plugin-copy-assets", + { + "files": [ + "externals.d.ts" + ] + } + ] + ] }, "xo": { "envs": [ @@ -90,7 +111,7 @@ "node/no-deprecated-api": 1 }, "ignores": [ - "dist", + "pkg", "test" ] }, From 0be82572726f4f32b78515c82a17b618870af7e7 Mon Sep 17 00:00:00 2001 From: Antoni Kepinski Date: Mon, 4 Nov 2019 23:34:17 +0100 Subject: [PATCH 075/157] fix incorrect statement in changelog --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 389b603c1..3fb456cd4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,7 +21,7 @@ Changelog - Fix: convert `Content-Encoding` to lowercase. - Fix: propagate size and timeout to cloned response. - Other: bundle TypeScript types. -- Other: replace Rollup with Microbundle. +- Other: replace Rollup with @pika/pack. - Other: introduce linting to the project. - Other: simplify Travis CI build matrix. - Other: dev dependency update. From 0ca9547fb8628a72b3d0aa5c434a53add208756a Mon Sep 17 00:00:00 2001 From: Antoni Kepinski Date: Mon, 4 Nov 2019 23:34:32 +0100 Subject: [PATCH 076/157] chore: v3.x upgrade guide --- UPGRADE-GUIDE.md | 105 ++++++++--------------------------------------- 1 file changed, 16 insertions(+), 89 deletions(-) diff --git a/UPGRADE-GUIDE.md b/UPGRADE-GUIDE.md index 22aab748b..92c0261d3 100644 --- a/UPGRADE-GUIDE.md +++ b/UPGRADE-GUIDE.md @@ -1,109 +1,36 @@ -# Upgrade to node-fetch v2.x +# Upgrade to node-fetch v3.x -node-fetch v2.x brings about many changes that increase the compliance of +node-fetch v3.x brings about many changes that increase the compliance of WHATWG's [Fetch Standard][whatwg-fetch]. However, many of these changes mean -that apps written for node-fetch v1.x needs to be updated to work with -node-fetch v2.x and be conformant with the Fetch Standard. This document helps +that apps written for node-fetch v2.x needs to be updated to work with +node-fetch v3.x and be conformant with the Fetch Standard. This document helps you make this transition. -Note that this document is not an exhaustive list of all changes made in v2.x, +Note that this document is not an exhaustive list of all changes made in v3.x, but rather that of the most important breaking changes. See our [changelog] for other comparatively minor modifications. -## `.text()` no longer tries to detect encoding +## Minimum supported Node.js version is now 10 -In v1.x, `response.text()` attempts to guess the text encoding of the input -material and decode it for the user. However, it runs counter to the Fetch -Standard which demands `.text()` to always use UTF-8. +Since Node.js will deprecate version 8 at the end of 2019, we decided that node-fetch v3.x will not only drop support for Node.js 4 and 6 (which were supported in v2.x), but also for Node.js 8. We strongly encourage you to upgrade, if you still haven't done so. Check out Node.js' official [LTS plan] for more information on Node.js' support lifetime. -In "response" to that, we have changed `.text()` to use UTF-8. A new function -**`response.textConverted()`** is created that maintains the behavior of -`.text()` in v1.x. +## `AbortError` now uses a w3c defined message -## Internal methods hidden +To stay spec-compliant, we changed the `AbortError` message to `The operation was aborted.`. -In v1.x, the user can access internal methods such as `_clone()`, `_decode()`, -and `_convert()` on the `response` object. While these methods should never -have been used, node-fetch v2.x makes these functions completely inaccessible. -If your app makes use of these functions, it may break when upgrading to v2.x. +## Data URI support -If you have a use case that requires these methods to be available, feel free -to file an issue and we will be happy to help you solve the problem. +Previously, node-fetch only supported http url scheme. However, the Fetch Standard recently introduced the `data:` URI support. Following the specification, we implemented this feature in v3.x. Read more about `data:` URLs [here][data-url]. -## Headers +## `Response.statusText` no longer sets a default message derived from the HTTP status code -The main goal we have for the `Headers` class in v2.x is to make it completely -spec-compliant. These changes are done in conjunction with GitHub's -[`whatwg-fetch`][gh-fetch] polyfill, [Chrome][chrome-headers], and -[Firefox][firefox-headers]. +If the server didn't respond with status text, node-fetch would set a default message derived from the HTTP status code. This behavior was not spec-compliant and now the `statusText` will remain blank instead. -```js -////////////////////////////////////////////////////////////////////////////// -// `get()` now returns **all** headers, joined by a comma, instead of only the -// first one. Its original behavior can be emulated using -// `get().split(',')[0]`. +## Bundled TypeScript types -const headers = new Headers({ - 'Abc': 'string', - 'Multi': [ 'header1', 'header2' ] -}); - -// before after -headers.get('Abc') => headers.get('Abc') => - 'string' 'string' -headers.get('Multi') => headers.get('Multi') => - 'header1'; 'header1,header2'; - headers.get('Multi').split(',')[0] => - 'header1'; - - -////////////////////////////////////////////////////////////////////////////// -// `getAll()` is removed. Its behavior in v1 can be emulated with -// `get().split(',')`. - -const headers = new Headers({ - 'Abc': 'string', - 'Multi': [ 'header1', 'header2' ] -}); - -// before after -headers.getAll('Multi') => headers.getAll('Multi') => - [ 'header1', 'header2' ]; throws ReferenceError - headers.get('Multi').split(',') => - [ 'header1', 'header2' ]; - - -////////////////////////////////////////////////////////////////////////////// -// All method parameters are now stringified. -const headers = new Headers(); -headers.set('null-header', null); -headers.set('undefined', undefined); - -// before after -headers.get('null-header') headers.get('null-header') - => null => 'null' -headers.get(undefined) headers.get(undefined) - => throws => 'undefined' - - -////////////////////////////////////////////////////////////////////////////// -// Invalid HTTP header names and values are now rejected outright. -const headers = new Headers(); -headers.set('Héy', 'ok'); // now throws -headers.get('Héy'); // now throws -new Headers({ 'Héy': 'ok' }); // now throws -``` - -## Node.js v0.x support dropped - -If you are still using Node.js v0.10 or v0.12, upgrade ASAP. Not only has it -become too much work for us to maintain, Node.js has also dropped support for -those release branches in 2016. Check out Node.js' official [LTS plan] for more -information on Node.js' support lifetime. +Since v3.x you no longer need to install `@types/node-fetch` package in order to use `node-fetch` with TypeScript. [whatwg-fetch]: https://fetch.spec.whatwg.org/ +[data-url]: https://fetch.spec.whatwg.org/#data-url-processor [LTS plan]: https://github.com/nodejs/LTS#lts-plan -[gh-fetch]: https://github.com/github/fetch -[chrome-headers]: https://crbug.com/645492 -[firefox-headers]: https://bugzilla.mozilla.org/show_bug.cgi?id=1278275 [changelog]: CHANGELOG.md From 6d2a67d50ae1ee3a52c00afdcba44541dc92c8cf Mon Sep 17 00:00:00 2001 From: Antoni Kepinski Date: Mon, 4 Nov 2019 23:44:12 +0100 Subject: [PATCH 077/157] Change the Open Collective button --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 8da1ee07e..1d540271c 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,9 @@ A light-weight module that brings `window.fetch` to Node.js **Consider supporting us on our Open Collective:** -[![Backers][opencollective-image]][opencollective-url] + + + --- @@ -717,8 +719,6 @@ MIT [install-size-url]: https://packagephobia.now.sh/result?p=node-fetch [discord-image]: https://img.shields.io/discord/619915844268326952?color=%237289DA&label=Discord&style=flat-square [discord-url]: https://discord.gg/Zxbndcm -[opencollective-image]: https://opencollective.com/node-fetch/backers.svg -[opencollective-url]: https://opencollective.com/node-fetch [whatwg-fetch]: https://fetch.spec.whatwg.org/ [response-init]: https://fetch.spec.whatwg.org/#responseinit [node-readable]: https://nodejs.org/api/stream.html#stream_readable_streams From 53f0932ef0a5a08b771f27afb4054f6cf3bc7e93 Mon Sep 17 00:00:00 2001 From: Richie Bendall Date: Tue, 5 Nov 2019 16:17:21 +1300 Subject: [PATCH 078/157] docs: Encode support button as Markdown instead of HTML --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 1d540271c..462b50574 100644 --- a/README.md +++ b/README.md @@ -10,9 +10,7 @@ A light-weight module that brings `window.fetch` to Node.js **Consider supporting us on our Open Collective:** - - - +[![Donate to our collective][opencollective-image]][opencollective-url] --- @@ -715,6 +713,8 @@ MIT [travis-url]: https://travis-ci.org/bitinn/node-fetch [codecov-image]: https://flat.badgen.net/codecov/c/github/bitinn/node-fetch/master [codecov-url]: https://codecov.io/gh/bitinn/node-fetch +[opencollective-image]: https://opencollective.com/node-fetch/donate/button.png?color=blue +[opencollective-url]: https://opencollective.com/node-fetch [install-size-image]: https://flat.badgen.net/packagephobia/install/node-fetch [install-size-url]: https://packagephobia.now.sh/result?p=node-fetch [discord-image]: https://img.shields.io/discord/619915844268326952?color=%237289DA&label=Discord&style=flat-square From 8386746edef64b6fee78576ec9985a383d030c61 Mon Sep 17 00:00:00 2001 From: Richie Bendall Date: Tue, 5 Nov 2019 16:21:58 +1300 Subject: [PATCH 079/157] chore: Ignore proper directory in xo --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 050108485..361d9682b 100644 --- a/package.json +++ b/package.json @@ -111,7 +111,7 @@ "node/no-deprecated-api": 1 }, "ignores": [ - "pkg", + "dist", "test" ] }, From e65970716e3c5d74f99ebda9b8a4bb0f26031df9 Mon Sep 17 00:00:00 2001 From: Antoni Kepinski Date: Wed, 6 Nov 2019 20:29:26 +0100 Subject: [PATCH 080/157] Add an "Upgrading" section to readme --- README.md | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 462b50574..8ce22dec7 100644 --- a/README.md +++ b/README.md @@ -22,6 +22,7 @@ A light-weight module that brings `window.fetch` to Node.js - [Difference from client-side fetch](#difference-from-client-side-fetch) - [Installation](#installation) - [Loading and configuring the module](#loading-and-configuring-the-module) + - [Upgrading](#upgrading) - [Common Usage](#common-usage) - [Plain text or HTML](#plain-text-or-html) - [JSON](#json) @@ -143,9 +144,17 @@ For versions of node earlier than 12.x, use this `globalThis` [polyfill](https:/ }()); ``` +## Upgrading + +Using an old version of node-fetch? Check out the following files: + +- [2.x to 3.x upgrade guide](v3-UPGRADE-GUIDE.md) +- [1.x to 2.x upgrade guide](v2-UPGRADE-GUIDE.md) +- [Changelog](CHANGELOG.md) + ## Common Usage -NOTE: The documentation below is up-to-date with `3.x` releases, [see `2.x` readme](https://github.com/bitinn/node-fetch/blob/2.x/README.md), [changelog](https://github.com/bitinn/node-fetch/blob/2.x/CHANGELOG.md) and [3.x upgrade guide](UPGRADE-GUIDE.md) for the differences. +NOTE: The documentation below is up-to-date with `3.x` releases, if you are using an older version, please check how to [upgrade](#upgrading). ### Plain text or HTML From c521b79346a1078feca4c12fbb3e38409f150d36 Mon Sep 17 00:00:00 2001 From: Antoni Kepinski Date: Wed, 6 Nov 2019 20:33:16 +0100 Subject: [PATCH 081/157] Split the upgrade guide into 2 files & add the missing changes about v3.x --- v2-UPGRADE-GUIDE.md | 109 ++++++++++++++++++++++++ UPGRADE-GUIDE.md => v3-UPGRADE-GUIDE.md | 43 +++++++++- 2 files changed, 148 insertions(+), 4 deletions(-) create mode 100644 v2-UPGRADE-GUIDE.md rename UPGRADE-GUIDE.md => v3-UPGRADE-GUIDE.md (61%) diff --git a/v2-UPGRADE-GUIDE.md b/v2-UPGRADE-GUIDE.md new file mode 100644 index 000000000..3660dfb3a --- /dev/null +++ b/v2-UPGRADE-GUIDE.md @@ -0,0 +1,109 @@ +# Upgrade to node-fetch v2.x + +node-fetch v2.x brings about many changes that increase the compliance of +WHATWG's [Fetch Standard][whatwg-fetch]. However, many of these changes mean +that apps written for node-fetch v1.x needs to be updated to work with +node-fetch v2.x and be conformant with the Fetch Standard. This document helps +you make this transition. + +Note that this document is not an exhaustive list of all changes made in v2.x, +but rather that of the most important breaking changes. See our [changelog] for +other comparatively minor modifications. + +## `.text()` no longer tries to detect encoding + +In v1.x, `response.text()` attempts to guess the text encoding of the input +material and decode it for the user. However, it runs counter to the Fetch +Standard which demands `.text()` to always use UTF-8. + +In "response" to that, we have changed `.text()` to use UTF-8. A new function +**`response.textConverted()`** is created that maintains the behavior of +`.text()` in v1.x. + +## Internal methods hidden + +In v1.x, the user can access internal methods such as `_clone()`, `_decode()`, +and `_convert()` on the `response` object. While these methods should never +have been used, node-fetch v2.x makes these functions completely inaccessible. +If your app makes use of these functions, it may break when upgrading to v2.x. + +If you have a use case that requires these methods to be available, feel free +to file an issue and we will be happy to help you solve the problem. + +## Headers + +The main goal we have for the `Headers` class in v2.x is to make it completely +spec-compliant. These changes are done in conjunction with GitHub's +[`whatwg-fetch`][gh-fetch] polyfill, [Chrome][chrome-headers], and +[Firefox][firefox-headers]. + +```js +////////////////////////////////////////////////////////////////////////////// +// `get()` now returns **all** headers, joined by a comma, instead of only the +// first one. Its original behavior can be emulated using +// `get().split(',')[0]`. + +const headers = new Headers({ + 'Abc': 'string', + 'Multi': ['header1', 'header2'] +}); + +// before after +headers.get('Abc') => headers.get('Abc') => + 'string' 'string' +headers.get('Multi') => headers.get('Multi') => + 'header1'; 'header1,header2'; + headers.get('Multi').split(',')[0] => + 'header1'; + + +////////////////////////////////////////////////////////////////////////////// +// `getAll()` is removed. Its behavior in v1 can be emulated with +// `get().split(',')`. + +const headers = new Headers({ + 'Abc': 'string', + 'Multi': ['header1', 'header2'] +}); + +// before after +headers.getAll('Multi') => headers.getAll('Multi') => + [ 'header1', 'header2' ]; throws ReferenceError + headers.get('Multi').split(',') => + ['header1', 'header2']; + + +////////////////////////////////////////////////////////////////////////////// +// All method parameters are now stringified. +const headers = new Headers(); +headers.set('null-header', null); +headers.set('undefined', undefined); + +// before after +headers.get('null-header') headers.get('null-header') + => null => 'null' +headers.get(undefined) headers.get(undefined) + => throws => 'undefined' + + +////////////////////////////////////////////////////////////////////////////// +// Invalid HTTP header names and values are now rejected outright. +const headers = new Headers(); +headers.set('Héy', 'ok'); // now throws +headers.get('Héy'); // now throws +new Headers({'Héy': 'ok'}); // now throws +``` + +## Node.js v0.x support dropped + +If you are still using Node.js v0.10 or v0.12, upgrade ASAP. Not only has it +become too much work for us to maintain, Node.js has also dropped support for +those release branches in 2016. Check out Node.js' official [LTS plan] for more +information on Node.js' support lifetime. + +[whatwg-fetch]: https://fetch.spec.whatwg.org/ +[LTS plan]: https://github.com/nodejs/LTS#lts-plan +[gh-fetch]: https://github.com/github/fetch +[chrome-headers]: https://crbug.com/645492 +[firefox-headers]: https://bugzilla.mozilla.org/show_bug.cgi?id=1278275 +[changelog]: CHANGELOG.md diff --git a/UPGRADE-GUIDE.md b/v3-UPGRADE-GUIDE.md similarity index 61% rename from UPGRADE-GUIDE.md rename to v3-UPGRADE-GUIDE.md index 92c0261d3..10968de01 100644 --- a/UPGRADE-GUIDE.md +++ b/v3-UPGRADE-GUIDE.md @@ -10,21 +10,52 @@ Note that this document is not an exhaustive list of all changes made in v3.x, but rather that of the most important breaking changes. See our [changelog] for other comparatively minor modifications. +- [Breaking Changes](#breaking) +- [Enhancements](#enhancements) + +--- + + + +# Breaking Changes + ## Minimum supported Node.js version is now 10 Since Node.js will deprecate version 8 at the end of 2019, we decided that node-fetch v3.x will not only drop support for Node.js 4 and 6 (which were supported in v2.x), but also for Node.js 8. We strongly encourage you to upgrade, if you still haven't done so. Check out Node.js' official [LTS plan] for more information on Node.js' support lifetime. -## `AbortError` now uses a w3c defined message +## `Response.statusText` no longer sets a default message derived from the HTTP status code -To stay spec-compliant, we changed the `AbortError` message to `The operation was aborted.`. +If the server didn't respond with status text, node-fetch would set a default message derived from the HTTP status code. This behavior was not spec-compliant and now the `statusText` will remain blank instead. + +## Dropped the `browser` field in package.json + +Prior to v3.x, we included a `browser` field in the package.json file. Since node-fetch is intended to be used on the server, we have removed this field. If you are using node-fetch client-side, consider switching to something like [cross-fetch]. + +## Dropped the `res.textConverted()` function + +If you want charset encoding detection, please use [fetch-charset-detection] package. + +# Enhancements ## Data URI support Previously, node-fetch only supported http url scheme. However, the Fetch Standard recently introduced the `data:` URI support. Following the specification, we implemented this feature in v3.x. Read more about `data:` URLs [here][data-url]. -## `Response.statusText` no longer sets a default message derived from the HTTP status code +## New & exposed Blob implementation -If the server didn't respond with status text, node-fetch would set a default message derived from the HTTP status code. This behavior was not spec-compliant and now the `statusText` will remain blank instead. +Blob implementation is now [fetch-blob] and hence is exposed, unlikely previously, where Blob type was only internal and not exported. + +## Better UTF-8 URL handling + +We now use the new Node.js [WHATWG-compliant URL API][whatwg-nodejs-url], so UTF-8 URLs are handles properly. + +## Request errors are now piped using `stream.pipeline` + +Since the v3.x required at least Node.js 10, we can utilise the new API. + +## `AbortError` now uses a w3c defined message + +To stay spec-compliant, we changed the `AbortError` message to `The operation was aborted.`. ## Bundled TypeScript types @@ -33,4 +64,8 @@ Since v3.x you no longer need to install `@types/node-fetch` package in order to [whatwg-fetch]: https://fetch.spec.whatwg.org/ [data-url]: https://fetch.spec.whatwg.org/#data-url-processor [LTS plan]: https://github.com/nodejs/LTS#lts-plan +[cross-fetch]: https://github.com/lquixada/cross-fetch +[fetch-charset-detection]: https://github.com/Richienb/fetch-charset-detection +[fetch-blob]: https://github.com/bitinn/fetch-blob#readme +[whatwg-nodejs-url]: https://nodejs.org/api/url.html#url_the_whatwg_url_api [changelog]: CHANGELOG.md From ab0e939fd1f19e19a5b6b5515abd79777202336a Mon Sep 17 00:00:00 2001 From: Richie Bendall Date: Sun, 10 Nov 2019 17:04:29 +1300 Subject: [PATCH 082/157] style: Lint test and example files Signed-off-by: Richie Bendall --- package.json | 30 +++++-- test/chai-timeout.js | 2 +- test/server.js | 29 ++++++- test/test.js | 194 +++++++++++++++++++++---------------------- 4 files changed, 149 insertions(+), 106 deletions(-) diff --git a/package.json b/package.json index 82707b4dd..eb96a170e 100644 --- a/package.json +++ b/package.json @@ -72,8 +72,7 @@ "xo": { "envs": [ "node", - "browser", - "mocha" + "browser" ], "rules": { "valid-jsdoc": 0, @@ -90,9 +89,30 @@ "node/no-deprecated-api": 1 }, "ignores": [ - "dist", - "test", - "example.js" + "dist" + ], + "overrides": [ + { + "files": "test/**/*.js", + "envs": [ + "node", + "mocha" + ], + "rules": { + "max-nested-callbacks": 0, + "no-unused-expressions": 0, + "eslint-comments/no-unused-disable": 0, + "new-cap": 0, + "guard-for-in": 0, + "no-new": 0 + } + }, + { + "files": "example.js", + "rules": { + "import/no-extraneous-dependencies": 0 + } + } ] }, "babel": { diff --git a/test/chai-timeout.js b/test/chai-timeout.js index 0fbce7b59..6fed2cfa4 100644 --- a/test/chai-timeout.js +++ b/test/chai-timeout.js @@ -1,4 +1,4 @@ -export default ({ Assertion }, utils) => { +export default ({Assertion}, utils) => { utils.addProperty(Assertion.prototype, 'timeout', function () { return new Promise(resolve => { const timer = setTimeout(() => resolve(true), 150); diff --git a/test/server.js b/test/server.js index 0b0e96530..250b69014 100644 --- a/test/server.js +++ b/test/server.js @@ -5,9 +5,8 @@ import {multipart as Multipart} from 'parted'; let convert; try { - /* eslint-disable-next-line import/no-unresolved */ - convert = require('encoding').convert; -} catch (error) {} + convert = require('encoding').convert; // eslint-disable-line import/no-unresolved +} catch (_) { } export default class TestServer { constructor() { @@ -87,6 +86,10 @@ export default class TestServer { res.setHeader('Content-Type', 'text/plain'); res.setHeader('Content-Encoding', 'gzip'); zlib.gzip('hello world', (err, buffer) => { + if (err) { + throw err; + } + res.end(buffer); }); } @@ -96,6 +99,10 @@ export default class TestServer { res.setHeader('Content-Type', 'text/plain'); res.setHeader('Content-Encoding', 'gzip'); zlib.gzip('hello world', (err, buffer) => { + if (err) { + throw err; + } + // Truncate the CRC checksum and size check at the end of the stream res.end(buffer.slice(0, buffer.length - 8)); }); @@ -106,6 +113,10 @@ export default class TestServer { res.setHeader('Content-Type', 'text/plain'); res.setHeader('Content-Encoding', 'GZip'); zlib.gzip('hello world', (err, buffer) => { + if (err) { + throw err; + } + res.end(buffer); }); } @@ -115,6 +126,10 @@ export default class TestServer { res.setHeader('Content-Type', 'text/plain'); res.setHeader('Content-Encoding', 'deflate'); zlib.deflate('hello world', (err, buffer) => { + if (err) { + throw err; + } + res.end(buffer); }); } @@ -125,6 +140,10 @@ export default class TestServer { if (typeof zlib.createBrotliDecompress === 'function') { res.setHeader('Content-Encoding', 'br'); zlib.brotliCompress('hello world', (err, buffer) => { + if (err) { + throw err; + } + res.end(buffer); }); } @@ -135,6 +154,10 @@ export default class TestServer { res.setHeader('Content-Type', 'text/plain'); res.setHeader('Content-Encoding', 'deflate'); zlib.deflateRaw('hello world', (err, buffer) => { + if (err) { + throw err; + } + res.end(buffer); }); } diff --git a/test/test.js b/test/test.js index cdf03eaca..4a5652d0e 100644 --- a/test/test.js +++ b/test/test.js @@ -1,5 +1,14 @@ // Test tools import zlib from 'zlib'; +import crypto from 'crypto'; +import {spawn} from 'child_process'; +import * as http from 'http'; +import * as fs from 'fs'; +import * as path from 'path'; +import * as stream from 'stream'; +import {parse as parseURL} from 'url'; +import {lookup} from 'dns'; +import vm from 'vm'; import chai from 'chai'; import chaiPromised from 'chai-as-promised'; import chaiIterator from 'chai-iterator'; @@ -9,11 +18,11 @@ import resumer from 'resumer'; import FormData from 'form-data'; import stringToArrayBuffer from 'string-to-arraybuffer'; -import { AbortController } from 'abortcontroller-polyfill/dist/abortcontroller'; +import {AbortController} from 'abortcontroller-polyfill/dist/abortcontroller'; import AbortController2 from 'abort-controller'; -import crypto from 'crypto'; // Test subjects +import Blob from 'fetch-blob'; import fetch, { FetchError, Headers, @@ -21,22 +30,12 @@ import fetch, { Response } from '../src'; import FetchErrorOrig from '../src/errors/fetch-error'; -import HeadersOrig, { createHeadersLenient } from '../src/headers'; +import HeadersOrig, {createHeadersLenient} from '../src/headers'; import RequestOrig from '../src/request'; import ResponseOrig from '../src/response'; -import Body, { getTotalBytes, extractContentType } from '../src/body'; -import Blob from 'fetch-blob'; +import Body, {getTotalBytes, extractContentType} from '../src/body'; import TestServer from './server'; -import { spawn } from 'child_process'; -import * as http from 'http'; -import * as fs from 'fs'; -import * as path from 'path'; -import * as stream from 'stream'; -import { parse as parseURL, URLSearchParams } from 'url'; -import { lookup } from 'dns'; -import vm from 'vm'; - const { Uint8Array: VMUint8Array } = vm.runInNewContext('this'); @@ -44,7 +43,7 @@ const { let convert; try { convert = require('encoding').convert; -} catch (error) { } +} catch (_) { } import chaiTimeout from './chai-timeout'; @@ -52,7 +51,7 @@ chai.use(chaiPromised); chai.use(chaiIterator); chai.use(chaiString); chai.use(chaiTimeout); -const { expect } = chai; +const {expect} = chai; const local = new TestServer(); const base = `http://${local.hostname}:${local.port}/`; @@ -65,7 +64,7 @@ after(done => { local.stop(done); }); -const itIf = val => val ? it : it.skip +const itIf = val => val ? it : it.skip; describe('node-fetch', () => { it('should return a promise', () => { @@ -122,31 +121,31 @@ describe('node-fetch', () => { return expect(fetch(url)).to.eventually.be.rejectedWith(TypeError, 'Only HTTP(S) protocols are supported'); }); - itIf(process.platform !== "win32")('should reject with error on network failure', () => { + itIf(process.platform !== 'win32')('should reject with error on network failure', () => { const url = 'http://localhost:50000/'; return expect(fetch(url)).to.eventually.be.rejected .and.be.an.instanceOf(FetchError) - .and.include({ type: 'system', code: 'ECONNREFUSED', errno: 'ECONNREFUSED' }); + .and.include({type: 'system', code: 'ECONNREFUSED', errno: 'ECONNREFUSED'}); }); - it('error should contain system error if one occurred', function() { + it('error should contain system error if one occurred', () => { const err = new FetchError('a message', 'system', new Error('an error')); return expect(err).to.have.property('erroredSysCall'); }); - it('error should not contain system error if none occurred', function() { + it('error should not contain system error if none occurred', () => { const err = new FetchError('a message', 'a type'); return expect(err).to.not.have.property('erroredSysCall'); }); - itIf(process.platform !== "win32")('system error is extracted from failed requests', function() { + itIf(process.platform !== 'win32')('system error is extracted from failed requests', () => { const url = 'http://localhost:50000/'; return expect(fetch(url)).to.eventually.be.rejected .and.be.an.instanceOf(FetchError) .and.have.property('erroredSysCall'); - }) + }); - it('should resolve into response', function() { + it('should resolve into response', () => { const url = `${base}hello`; return fetch(url).then(res => { expect(res).to.be.an.instanceof(Response); @@ -192,7 +191,7 @@ describe('node-fetch', () => { return res.json().then(result => { expect(res.bodyUsed).to.be.true; expect(result).to.be.an('object'); - expect(result).to.deep.equal({ name: 'value' }); + expect(result).to.deep.equal({name: 'value'}); }); }); }); @@ -200,7 +199,7 @@ describe('node-fetch', () => { it('should send request with custom headers', () => { const url = `${base}inspect`; const opts = { - headers: { 'x-custom-header': 'abc' } + headers: {'x-custom-header': 'abc'} }; return fetch(url, opts).then(res => { return res.json(); @@ -212,7 +211,7 @@ describe('node-fetch', () => { it('should accept headers instance', () => { const url = `${base}inspect`; const opts = { - headers: new Headers({ 'x-custom-header': 'abc' }) + headers: new Headers({'x-custom-header': 'abc'}) }; return fetch(url, opts).then(res => { return res.json(); @@ -473,7 +472,7 @@ describe('node-fetch', () => { it('should follow redirect code 301 and keep existing headers', () => { const url = `${base}redirect/301`; const opts = { - headers: new Headers({ 'x-custom-header': 'abc' }) + headers: new Headers({'x-custom-header': 'abc'}) }; return fetch(url, opts).then(res => { expect(res.url).to.equal(`${base}inspect`); @@ -580,7 +579,7 @@ describe('node-fetch', () => { expect(res.headers.get('content-type')).to.equal('application/json'); return expect(res.json()).to.eventually.be.rejected .and.be.an.instanceOf(FetchError) - .and.include({ type: 'invalid-json' }); + .and.include({type: 'invalid-json'}); }); }); @@ -605,7 +604,7 @@ describe('node-fetch', () => { expect(res.ok).to.be.true; return expect(res.json()).to.eventually.be.rejected .and.be.an.instanceOf(FetchError) - .and.include({ type: 'invalid-json' }); + .and.include({type: 'invalid-json'}); }); }); @@ -675,13 +674,13 @@ describe('node-fetch', () => { it('should make capitalised Content-Encoding lowercase', () => { const url = `${base}gzip-capital`; return fetch(url).then(res => { - expect(res.headers.get('content-encoding')).to.equal("gzip"); + expect(res.headers.get('content-encoding')).to.equal('gzip'); return res.text().then(result => { expect(result).to.be.a('string'); expect(result).to.equal('hello world'); }); - }) - }) + }); + }); it('should decompress deflate response', () => { const url = `${base}deflate`; @@ -882,8 +881,8 @@ describe('node-fetch', () => { const controller2 = new AbortController2(); const fetches = [ - fetch(`${base}timeout`, { signal: controller.signal }), - fetch(`${base}timeout`, { signal: controller2.signal }), + fetch(`${base}timeout`, {signal: controller.signal}), + fetch(`${base}timeout`, {signal: controller2.signal}), fetch( `${base}timeout`, { @@ -891,7 +890,7 @@ describe('node-fetch', () => { signal: controller.signal, headers: { 'Content-Type': 'application/json', - body: JSON.stringify({ hello: 'world' }) + body: JSON.stringify({hello: 'world'}) } } ) @@ -946,10 +945,10 @@ describe('node-fetch', () => { it('should remove internal AbortSignal event listener after request is aborted', () => { const controller = new AbortController(); - const { signal } = controller; + const {signal} = controller; const promise = fetch( `${base}timeout`, - { signal } + {signal} ); const result = expect(promise).to.eventually.be.rejected .and.be.an.instanceof(Error) @@ -991,11 +990,11 @@ describe('node-fetch', () => { it('should remove internal AbortSignal event listener after request and response complete without aborting', () => { const controller = new AbortController(); - const { signal } = controller; - const fetchHtml = fetch(`${base}html`, { signal }) + const {signal} = controller; + const fetchHtml = fetch(`${base}html`, {signal}) .then(res => res.text()); - const fetchResponseError = fetch(`${base}error/reset`, { signal }); - const fetchRedirect = fetch(`${base}redirect/301`, { signal }).then(res => res.json()); + const fetchResponseError = fetch(`${base}error/reset`, {signal}); + const fetchRedirect = fetch(`${base}redirect/301`, {signal}).then(res => res.json()); return Promise.all([ expect(fetchHtml).to.eventually.be.fulfilled.and.equal(''), expect(fetchResponseError).to.be.eventually.rejected, @@ -1009,7 +1008,7 @@ describe('node-fetch', () => { const controller = new AbortController(); return expect(fetch( `${base}slow`, - { signal: controller.signal } + {signal: controller.signal} )) .to.eventually.be.fulfilled .then(res => { @@ -1026,7 +1025,7 @@ describe('node-fetch', () => { const controller = new AbortController(); return expect(fetch( `${base}slow`, - { signal: controller.signal } + {signal: controller.signal} )) .to.eventually.be.fulfilled .then(res => { @@ -1042,7 +1041,7 @@ describe('node-fetch', () => { const controller = new AbortController(); expect(fetch( `${base}slow`, - { signal: controller.signal } + {signal: controller.signal} )) .to.eventually.be.fulfilled .then(res => { @@ -1058,11 +1057,11 @@ describe('node-fetch', () => { it('should cancel request body of type Stream with AbortError when aborted', () => { const controller = new AbortController(); - const body = new stream.Readable({ objectMode: true }); + const body = new stream.Readable({objectMode: true}); body._read = () => { }; const promise = fetch( `${base}slow`, - { signal: controller.signal, body, method: 'POST' } + {signal: controller.signal, body, method: 'POST'} ); const result = Promise.all([ @@ -1088,15 +1087,15 @@ describe('node-fetch', () => { it('should throw a TypeError if a signal is not of type AbortSignal', () => { return Promise.all([ - expect(fetch(`${base}inspect`, { signal: {} })) + expect(fetch(`${base}inspect`, {signal: {}})) .to.be.eventually.rejected .and.be.an.instanceof(TypeError) .and.have.property('message').includes('AbortSignal'), - expect(fetch(`${base}inspect`, { signal: '' })) + expect(fetch(`${base}inspect`, {signal: ''})) .to.be.eventually.rejected .and.be.an.instanceof(TypeError) .and.have.property('message').includes('AbortSignal'), - expect(fetch(`${base}inspect`, { signal: Object.create(null) })) + expect(fetch(`${base}inspect`, {signal: Object.create(null)})) .to.be.eventually.rejected .and.be.an.instanceof(TypeError) .and.have.property('message').includes('AbortSignal') @@ -1205,7 +1204,7 @@ describe('node-fetch', () => { }); }); - it('should allow POST request with ArrayBuffer body from a VM context', function () { + it('should allow POST request with ArrayBuffer body from a VM context', () => { const url = `${base}inspect`; const opts = { method: 'POST', @@ -1250,7 +1249,7 @@ describe('node-fetch', () => { }); }); - it('should allow POST request with ArrayBufferView (Uint8Array) body from a VM context', function () { + it('should allow POST request with ArrayBufferView (Uint8Array) body from a VM context', () => { const url = `${base}inspect`; const opts = { method: 'POST', @@ -1355,7 +1354,7 @@ describe('node-fetch', () => { }); }); - itIf(process.platform !== "win32")('should allow POST request with form-data using stream as body', () => { + itIf(process.platform !== 'win32')('should allow POST request with form-data using stream as body', () => { const form = new FormData(); form.append('my_field', fs.createReadStream(path.join(__dirname, 'dummy.txt'))); @@ -1372,7 +1371,7 @@ describe('node-fetch', () => { expect(res.headers['content-type']).to.startWith('multipart/form-data;boundary='); expect(res.headers['content-length']).to.be.undefined; expect(res.body).to.contain('my_field='); - }) + }); }); it('should allow POST request with form-data as body and custom headers', () => { @@ -1404,7 +1403,7 @@ describe('node-fetch', () => { // Note that fetch simply calls tostring on an object const opts = { method: 'POST', - body: { a: 1 } + body: {a: 1} }; return fetch(url, opts).then(res => { return res.json(); @@ -1416,22 +1415,20 @@ describe('node-fetch', () => { }); }); - const itUSP = typeof URLSearchParams === 'function' ? it : it.skip; - - itUSP('constructing a Response with URLSearchParams as body should have a Content-Type', () => { + it('constructing a Response with URLSearchParams as body should have a Content-Type', () => { const params = new URLSearchParams(); const res = new Response(params); res.headers.get('Content-Type'); expect(res.headers.get('Content-Type')).to.equal('application/x-www-form-urlencoded;charset=UTF-8'); }); - itUSP('constructing a Request with URLSearchParams as body should have a Content-Type', () => { + it('constructing a Request with URLSearchParams as body should have a Content-Type', () => { const params = new URLSearchParams(); - const req = new Request(base, { method: 'POST', body: params }); + const req = new Request(base, {method: 'POST', body: params}); expect(req.headers.get('Content-Type')).to.equal('application/x-www-form-urlencoded;charset=UTF-8'); }); - itUSP('Reading a body with URLSearchParams should echo back the result', () => { + it('Reading a body with URLSearchParams should echo back the result', () => { const params = new URLSearchParams(); params.append('a', '1'); return new Response(params).text().then(text => { @@ -1440,16 +1437,16 @@ describe('node-fetch', () => { }); // Body should been cloned... - itUSP('constructing a Request/Response with URLSearchParams and mutating it should not affected body', () => { + it('constructing a Request/Response with URLSearchParams and mutating it should not affected body', () => { const params = new URLSearchParams(); - const req = new Request(`${base}inspect`, { method: 'POST', body: params }); + const req = new Request(`${base}inspect`, {method: 'POST', body: params}); params.append('a', '1'); return req.text().then(text => { expect(text).to.equal(''); }); }); - itUSP('should allow POST request with URLSearchParams as body', () => { + it('should allow POST request with URLSearchParams as body', () => { const params = new URLSearchParams(); params.append('a', '1'); @@ -1468,7 +1465,7 @@ describe('node-fetch', () => { }); }); - itUSP('should still recognize URLSearchParams when extended', () => { + it('should still recognize URLSearchParams when extended', () => { class CustomSearchParams extends URLSearchParams { } const params = new CustomSearchParams(); params.append('a', '1'); @@ -1709,7 +1706,7 @@ describe('node-fetch', () => { return fetch(url).then(res => { const r1 = res.clone(); return Promise.all([res.json(), r1.text()]).then(results => { - expect(results[0]).to.deep.equal({ name: 'value' }); + expect(results[0]).to.deep.equal({name: 'value'}); expect(results[1]).to.equal('{"name":"value"}'); }); }); @@ -1720,7 +1717,7 @@ describe('node-fetch', () => { return fetch(url).then(res => { const r1 = res.clone(); return res.json().then(result => { - expect(result).to.deep.equal({ name: 'value' }); + expect(result).to.deep.equal({name: 'value'}); return r1.text().then(result => { expect(result).to.equal('{"name":"value"}'); }); @@ -1735,7 +1732,7 @@ describe('node-fetch', () => { return r1.text().then(result => { expect(result).to.equal('{"name":"value"}'); return res.json().then(result => { - expect(result).to.deep.equal({ name: 'value' }); + expect(result).to.deep.equal({name: 'value'}); }); }); }); @@ -1932,7 +1929,7 @@ describe('node-fetch', () => { method: 'POST', body: blob }); - }).then(res => res.json()).then(({ body, headers }) => { + }).then(res => res.json()).then(({body, headers}) => { expect(body).to.equal('world'); expect(headers['content-type']).to.equal(type); expect(headers['content-length']).to.equal(String(length)); @@ -2004,7 +2001,10 @@ describe('node-fetch', () => { body = body.pipe(new stream.PassThrough()); const res = new Response(body); const bufferConcat = Buffer.concat; - const restoreBufferConcat = () => Buffer.concat = bufferConcat; + const restoreBufferConcat = () => { + Buffer.concat = bufferConcat; + }; + Buffer.concat = () => { throw new Error('embedded error'); }; @@ -2015,7 +2015,7 @@ describe('node-fetch', () => { return expect(textPromise).to.eventually.be.rejected .and.be.an.instanceOf(FetchError) - .and.include({ type: 'system' }) + .and.include({type: 'system'}) .and.have.property('message').that.includes('Could not create Buffer') .and.that.includes('embedded error'); }); @@ -2028,8 +2028,8 @@ describe('node-fetch', () => { return lookup(hostname, options, callback); } - const agent = http.Agent({ lookup: lookupSpy }); - return fetch(url, { agent }).then(() => { + const agent = http.Agent({lookup: lookupSpy}); + return fetch(url, {agent}).then(() => { expect(called).to.equal(2); }); }); @@ -2043,8 +2043,8 @@ describe('node-fetch', () => { return lookup(hostname, {}, callback); } - const agent = http.Agent({ lookup: lookupSpy, family }); - return fetch(url, { agent }).then(() => { + const agent = http.Agent({lookup: lookupSpy, family}); + return fetch(url, {agent}).then(() => { expect(families).to.have.length(2); expect(families[0]).to.equal(family); expect(families[1]).to.equal(family); @@ -2087,7 +2087,7 @@ describe('node-fetch', () => { size: 1024 }); - const blobBody = new Blob([bodyContent], { type: 'text/plain' }); + const blobBody = new Blob([bodyContent], {type: 'text/plain'}); const blobRequest = new Request(url, { method: 'POST', body: blobBody, @@ -2246,9 +2246,9 @@ describe('Headers', () => { it('should reject illegal header', () => { const headers = new Headers(); - expect(() => new Headers({ 'He y': 'ok' })).to.throw(TypeError); - expect(() => new Headers({ 'Hé-y': 'ok' })).to.throw(TypeError); - expect(() => new Headers({ 'He-y': 'ăk' })).to.throw(TypeError); + expect(() => new Headers({'He y': 'ok'})).to.throw(TypeError); + expect(() => new Headers({'Hé-y': 'ok'})).to.throw(TypeError); + expect(() => new Headers({'He-y': 'ăk'})).to.throw(TypeError); expect(() => headers.append('Hé-y', 'ok')).to.throw(TypeError); expect(() => headers.delete('Hé-y')).to.throw(TypeError); expect(() => headers.get('Hé-y')).to.throw(TypeError); @@ -2258,7 +2258,7 @@ describe('Headers', () => { expect(() => headers.append('', 'ok')).to.throw(TypeError); // 'o k' is valid value but invalid name - new Headers({ 'He-y': 'o k' }); + new Headers({'He-y': 'o k'}); }); it('should ignore unsupported attributes while reading headers', () => { @@ -2274,7 +2274,7 @@ describe('Headers', () => { res.d = []; res.e = 1; res.f = [1, 2]; - res.g = { a: 1 }; + res.g = {a: 1}; res.h = undefined; res.i = null; res.j = NaN; @@ -2364,7 +2364,7 @@ describe('Headers', () => { expect(() => new Headers([['b', '2', 'huh?']])).to.throw(TypeError); expect(() => new Headers(['b2'])).to.throw(TypeError); expect(() => new Headers('b2')).to.throw(TypeError); - expect(() => new Headers({ [Symbol.iterator]: 42 })).to.throw(TypeError); + expect(() => new Headers({[Symbol.iterator]: 42})).to.throw(TypeError); }); }); @@ -2595,7 +2595,7 @@ describe('Request', () => { const form = new FormData(); form.append('a', '1'); - const { signal } = new AbortController(); + const {signal} = new AbortController(); const r1 = new Request(url, { method: 'POST', @@ -2644,17 +2644,17 @@ describe('Request', () => { }); it('should throw error with GET/HEAD requests with body', () => { - expect(() => new Request('.', { body: '' })) + expect(() => new Request('.', {body: ''})) .to.throw(TypeError); - expect(() => new Request('.', { body: 'a' })) + expect(() => new Request('.', {body: 'a'})) .to.throw(TypeError); - expect(() => new Request('.', { body: '', method: 'HEAD' })) + expect(() => new Request('.', {body: '', method: 'HEAD'})) .to.throw(TypeError); - expect(() => new Request('.', { body: 'a', method: 'HEAD' })) + expect(() => new Request('.', {body: 'a', method: 'HEAD'})) .to.throw(TypeError); - expect(() => new Request('.', { body: 'a', method: 'get' })) + expect(() => new Request('.', {body: 'a', method: 'get'})) .to.throw(TypeError); - expect(() => new Request('.', { body: 'a', method: 'head' })) + expect(() => new Request('.', {body: 'a', method: 'head'})) .to.throw(TypeError); }); @@ -2750,7 +2750,7 @@ describe('Request', () => { let body = resumer().queue('a=1').end(); body = body.pipe(new stream.PassThrough()); const agent = new http.Agent(); - const { signal } = new AbortController(); + const {signal} = new AbortController(); const req = new Request(url, { body, method: 'POST', @@ -2959,26 +2959,26 @@ describe('external encoding', () => { it('should accept data uri', () => { return fetch('data:image/gif;base64,R0lGODlhAQABAIAAAAUEBAAAACwAAAAAAQABAAACAkQBADs=').then(r => { expect(r.status).to.equal(200); - expect(r.headers.get('Content-Type')).to.equal("image/gif") + expect(r.headers.get('Content-Type')).to.equal('image/gif'); return r.buffer().then(b => { - expect(b).to.be.an.instanceOf(Buffer) + expect(b).to.be.an.instanceOf(Buffer); }); }); }); it('should accept data uri of plain text', () => { - return fetch("data:,Hello%20World!").then(r => { + return fetch('data:,Hello%20World!').then(r => { expect(r.status).to.equal(200); - expect(r.headers.get('Content-Type')).to.equal("text/plain") - return r.text().then(t => expect(t).to.equal("Hello World!")) + expect(r.headers.get('Content-Type')).to.equal('text/plain'); + return r.text().then(t => expect(t).to.equal('Hello World!')); }); - }) + }); it('should reject invalid data uri', () => { return fetch('data:@@@@').catch(e => { expect(e).to.exist; - expect(e.message).to.include('invalid URL') + expect(e.message).to.include('invalid URL'); }); }); }); From f7b37c1764a3c125bd770caac7c4c14bb4157adb Mon Sep 17 00:00:00 2001 From: Antoni Kepinski Date: Wed, 13 Nov 2019 21:46:50 +0100 Subject: [PATCH 083/157] Move *.md files to the `docs` folder (except README.md) --- CHANGELOG.md => docs/CHANGELOG.md | 0 ERROR-HANDLING.md => docs/ERROR-HANDLING.md | 0 v2-UPGRADE-GUIDE.md => docs/v2-UPGRADE-GUIDE.md | 0 v3-UPGRADE-GUIDE.md => docs/v3-UPGRADE-GUIDE.md | 0 4 files changed, 0 insertions(+), 0 deletions(-) rename CHANGELOG.md => docs/CHANGELOG.md (100%) rename ERROR-HANDLING.md => docs/ERROR-HANDLING.md (100%) rename v2-UPGRADE-GUIDE.md => docs/v2-UPGRADE-GUIDE.md (100%) rename v3-UPGRADE-GUIDE.md => docs/v3-UPGRADE-GUIDE.md (100%) diff --git a/CHANGELOG.md b/docs/CHANGELOG.md similarity index 100% rename from CHANGELOG.md rename to docs/CHANGELOG.md diff --git a/ERROR-HANDLING.md b/docs/ERROR-HANDLING.md similarity index 100% rename from ERROR-HANDLING.md rename to docs/ERROR-HANDLING.md diff --git a/v2-UPGRADE-GUIDE.md b/docs/v2-UPGRADE-GUIDE.md similarity index 100% rename from v2-UPGRADE-GUIDE.md rename to docs/v2-UPGRADE-GUIDE.md diff --git a/v3-UPGRADE-GUIDE.md b/docs/v3-UPGRADE-GUIDE.md similarity index 100% rename from v3-UPGRADE-GUIDE.md rename to docs/v3-UPGRADE-GUIDE.md From 084ecf36acf764ae9770bbd773fd8cfded6947cd Mon Sep 17 00:00:00 2001 From: Antoni Kepinski Date: Wed, 13 Nov 2019 21:47:05 +0100 Subject: [PATCH 084/157] Update references to files --- README.md | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 8ce22dec7..a7e87e163 100644 --- a/README.md +++ b/README.md @@ -85,11 +85,13 @@ See Matt Andrews' [isomorphic-fetch](https://github.com/matthew-andrews/isomorph - Use native promise, but allow substituting it with [insert your favorite promise library]. - Use native Node streams for body, on both request and response. - Decode content encoding (gzip/deflate) properly, and convert string output (such as `res.text()` and `res.json()`) to UTF-8 automatically. -- Useful extensions such as timeout, redirect limit, response size limit, [explicit errors](ERROR-HANDLING.md) for troubleshooting. +- Useful extensions such as timeout, redirect limit, response size limit, [explicit errors][error-handling.md] for troubleshooting. ## Difference from client-side fetch -- See [Known Differences](LIMITS.md) for details. +- See known differences: + - [As of v3.x](docs/v3-LIMITS.md) + - [As of v2.x](docs/v2-LIMITS.md) - If you happen to use a missing feature that `window.fetch` offers, feel free to open an issue. - Pull requests are welcomed too! @@ -148,9 +150,9 @@ For versions of node earlier than 12.x, use this `globalThis` [polyfill](https:/ Using an old version of node-fetch? Check out the following files: -- [2.x to 3.x upgrade guide](v3-UPGRADE-GUIDE.md) -- [1.x to 2.x upgrade guide](v2-UPGRADE-GUIDE.md) -- [Changelog](CHANGELOG.md) +- [2.x to 3.x upgrade guide](docs/v3-UPGRADE-GUIDE.md) +- [1.x to 2.x upgrade guide](docs/v2-UPGRADE-GUIDE.md) +- [Changelog](docs/CHANGELOG.md) ## Common Usage @@ -223,7 +225,7 @@ fetch('https://httpbin.org/post', {method: 'POST', body: params}) NOTE: 3xx-5xx responses are _NOT_ exceptions, and should be handled in `then()`, see the next section. -Adding a catch to the fetch promise chain will catch _all_ exceptions, such as errors originating from node core libraries, like network errors, and operational errors which are instances of FetchError. See the [error handling document](ERROR-HANDLING.md) for more details. +Adding a catch to the fetch promise chain will catch _all_ exceptions, such as errors originating from node core libraries, like network errors, and operational errors which are instances of FetchError. See the [error handling document][error-handling.md] for more details. ```js const fetch = require('node-fetch'); @@ -732,6 +734,4 @@ MIT [response-init]: https://fetch.spec.whatwg.org/#responseinit [node-readable]: https://nodejs.org/api/stream.html#stream_readable_streams [mdn-headers]: https://developer.mozilla.org/en-US/docs/Web/API/Headers -[limits.md]: https://github.com/bitinn/node-fetch/blob/master/LIMITS.md -[error-handling.md]: https://github.com/bitinn/node-fetch/blob/master/ERROR-HANDLING.md -[upgrade-guide.md]: https://github.com/bitinn/node-fetch/blob/master/UPGRADE-GUIDE.md +[error-handling.md]: https://github.com/bitinn/node-fetch/blob/master/docs/ERROR-HANDLING.md From 5315421e01097f5da3777adf6a85dc10fc0cd3d5 Mon Sep 17 00:00:00 2001 From: Antoni Kepinski Date: Wed, 13 Nov 2019 21:47:24 +0100 Subject: [PATCH 085/157] Split LIMITS.md into 2 files (as of v2.x and v3.x) --- LIMITS.md => docs/v2-LIMITS.md | 4 ++-- docs/v3-LIMITS.md | 31 +++++++++++++++++++++++++++++++ 2 files changed, 33 insertions(+), 2 deletions(-) rename LIMITS.md => docs/v2-LIMITS.md (94%) create mode 100644 docs/v3-LIMITS.md diff --git a/LIMITS.md b/docs/v2-LIMITS.md similarity index 94% rename from LIMITS.md rename to docs/v2-LIMITS.md index 9c4b8c0c8..d0f12e493 100644 --- a/LIMITS.md +++ b/docs/v2-LIMITS.md @@ -26,7 +26,7 @@ Known differences - If you are using `res.clone()` and writing an isomorphic app, note that stream on Node.js have a smaller internal buffer size (16Kb, aka `highWaterMark`) from client-side browsers (>1Mb, not consistent across browsers). -- Because node.js stream doesn't expose a [*disturbed*](https://fetch.spec.whatwg.org/#concept-readablestream-disturbed) property like Stream spec, using a consumed stream for `new Response(body)` will not set `bodyUsed` flag correctly. +- Because Node.js stream doesn't expose a [*disturbed*](https://fetch.spec.whatwg.org/#concept-readablestream-disturbed) property like Stream spec, using a consumed stream for `new Response(body)` will not set `bodyUsed` flag correctly. [readable-stream]: https://nodejs.org/api/stream.html#stream_readable_streams -[ERROR-HANDLING.md]: https://github.com/bitinn/node-fetch/blob/master/ERROR-HANDLING.md +[ERROR-HANDLING.md]: https://github.com/bitinn/node-fetch/blob/master/docs/ERROR-HANDLING.md diff --git a/docs/v3-LIMITS.md b/docs/v3-LIMITS.md new file mode 100644 index 000000000..459a3506b --- /dev/null +++ b/docs/v3-LIMITS.md @@ -0,0 +1,31 @@ + +Known differences +================= + +*As of 3.x release* + +- Topics such as Cross-Origin, Content Security Policy, Mixed Content, Service Workers are ignored, given our server-side context. + +- On the upside, there are no forbidden headers. + +- `res.url` contains the final url when following redirects. + +- For convenience, `res.body` is a Node.js [Readable stream][readable-stream], so decoding can be handled independently. + +- Similarly, `req.body` can either be `null`, a string, a buffer or a Readable stream. + +- Also, you can handle rejected fetch requests through checking `err.type` and `err.code`. See [ERROR-HANDLING.md][] for more info. + +- Only support `res.text()`, `res.json()`, `res.blob()`, `res.arraybuffer()`, `res.buffer()` + +- There is currently no built-in caching, as server-side caching varies by use-cases. + +- Current implementation lacks server-side cookie store, you will need to extract `Set-Cookie` headers manually. + +- If you are using `res.clone()` and writing an isomorphic app, note that stream on Node.js have a smaller internal buffer size (16Kb, aka `highWaterMark`) from client-side browsers (>1Mb, not consistent across browsers). Learn [how to get around this][highwatermark-fix]. + +- Because Node.js stream doesn't expose a [*disturbed*](https://fetch.spec.whatwg.org/#concept-readablestream-disturbed) property like Stream spec, using a consumed stream for `new Response(body)` will not set `bodyUsed` flag correctly. + +[readable-stream]: https://nodejs.org/api/stream.html#stream_readable_streams +[ERROR-HANDLING.md]: https://github.com/bitinn/node-fetch/blob/master/docs/ERROR-HANDLING.md +[highwatermark-fix]: https://github.com/bitinn/node-fetch/blob/master/README.md#custom-highwatermark From 86b32d0a6ae1964978616a52f1a17bd053888ad0 Mon Sep 17 00:00:00 2001 From: Richie Bendall Date: Sun, 17 Nov 2019 18:07:07 +1300 Subject: [PATCH 086/157] chore: Remove logging statement Signed-off-by: Richie Bendall --- src/request.js | 262 ++++++++++++++++++++++++------------------------- 1 file changed, 130 insertions(+), 132 deletions(-) diff --git a/src/request.js b/src/request.js index c6c14421e..530c67195 100644 --- a/src/request.js +++ b/src/request.js @@ -7,16 +7,16 @@ * All spec algorithm step numbers are based on https://fetch.spec.whatwg.org/commit-snapshots/ae716822cb3a61843226cd090eefc6589446c1d2/. */ -import {parse as parseUrl, format as formatUrl} from 'url'; -import Stream from 'stream'; -import utf8 from 'utf8'; -import Headers, {exportNodeCompatibleHeaders} from './headers'; -import Body, {clone, extractContentType, getTotalBytes} from './body'; -import {isAbortSignal} from './utils/is'; +import { parse as parseUrl, format as formatUrl } from "url"; +import Stream from "stream"; +import utf8 from "utf8"; +import Headers, { exportNodeCompatibleHeaders } from "./headers"; +import Body, { clone, extractContentType, getTotalBytes } from "./body"; +import { isAbortSignal } from "./utils/is"; -const INTERNALS = Symbol('Request internals'); +const INTERNALS = Symbol("Request internals") -const streamDestructionSupported = 'destroy' in Stream.Readable.prototype; +const streamDestructionSupported = "destroy" in Stream.Readable.prototype /** * Check if `obj` is an instance of Request. @@ -25,10 +25,10 @@ const streamDestructionSupported = 'destroy' in Stream.Readable.prototype; * @return {boolean} */ function isRequest(obj) { - return ( - typeof obj === 'object' && - typeof obj[INTERNALS] === 'object' - ); + return ( + typeof obj === "object" && + typeof obj[INTERNALS] === "object" + ) } /** @@ -39,132 +39,132 @@ function isRequest(obj) { * @return Void */ export default class Request { - constructor(input, init = {}) { - let parsedURL; + constructor(input, init = {}) { + let parsedURL // Normalize input and force URL to be encoded as UTF-8 (https://github.com/bitinn/node-fetch/issues/245) if (!isRequest(input)) { - 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(utf8.encode(input.href)); + 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(utf8.encode(input.href)) } else { - // Coerce input to a string before attempting to parse - parsedURL = parseUrl(utf8.encode(`${input}`)); + // Coerce input to a string before attempting to parse + parsedURL = parseUrl(utf8.encode(`${input}`)) } - input = {}; + input = {} } else { - parsedURL = parseUrl(utf8.encode(input.url)); + parsedURL = parseUrl(utf8.encode(input.url)) } - let method = init.method || input.method || 'GET'; - method = method.toUpperCase(); + let method = init.method || input.method || "GET"; + method = method.toUpperCase() if ((init.body != null || isRequest(input) && input.body !== null) && - (method === 'GET' || method === 'HEAD')) { - throw new TypeError('Request with GET/HEAD method cannot have body'); + (method === "GET" || method === "HEAD")) { + throw new TypeError("Request with GET/HEAD method cannot have body") } - const inputBody = init.body != null ? - init.body : - (isRequest(input) && input.body !== null ? - clone(input) : - null); + const inputBody = init.body != null ? + init.body : + (isRequest(input) && input.body !== null ? + clone(input) : + null) Body.call(this, inputBody, { - timeout: init.timeout || input.timeout || 0, - size: init.size || input.size || 0 - }); + timeout: init.timeout || input.timeout || 0, + size: init.size || input.size || 0, + }) - const headers = new Headers(init.headers || input.headers || {}); + const headers = new Headers(init.headers || input.headers || {}) - if (inputBody != null && !headers.has('Content-Type')) { - const contentType = extractContentType(inputBody); + if (inputBody != null && !headers.has("Content-Type")) { + const contentType = extractContentType(inputBody) if (contentType) { - headers.append('Content-Type', contentType); + headers.append("Content-Type", contentType) } - } + } - let signal = isRequest(input) ? - input.signal : - null; - if ('signal' in init) { - signal = init.signal; + let signal = isRequest(input) ? + input.signal : + null + if ("signal" in init) { + signal = init.signal } - if (signal != null && !isAbortSignal(signal)) { - throw new TypeError('Expected signal to be an instanceof AbortSignal'); + if (signal != null && !isAbortSignal(signal)) { + throw new TypeError("Expected signal to be an instanceof AbortSignal") } - this[INTERNALS] = { - method, - redirect: init.redirect || input.redirect || 'follow', - headers, - parsedURL, - signal - }; + this[INTERNALS] = { + method, + redirect: init.redirect || input.redirect || "follow", + headers, + parsedURL, + signal, + } // Node-fetch-only options this.follow = init.follow !== undefined ? - init.follow : (input.follow !== undefined ? - input.follow : 20); + init.follow : (input.follow !== undefined ? + input.follow : 20) this.compress = init.compress !== undefined ? - init.compress : (input.compress !== undefined ? - input.compress : true); - this.counter = init.counter || input.counter || 0; - this.agent = init.agent || input.agent; - this.highWaterMark = init.highWaterMark || input.highWaterMark; + init.compress : (input.compress !== undefined ? + input.compress : true) + this.counter = init.counter || input.counter || 0 + this.agent = init.agent || input.agent + this.highWaterMark = init.highWaterMark || input.highWaterMark } - get method() { - return this[INTERNALS].method; + get method() { + return this[INTERNALS].method } - get url() { - return formatUrl(this[INTERNALS].parsedURL); + get url() { + return formatUrl(this[INTERNALS].parsedURL) } - get headers() { - return this[INTERNALS].headers; + get headers() { + return this[INTERNALS].headers } - get redirect() { - return this[INTERNALS].redirect; + get redirect() { + return this[INTERNALS].redirect } - get signal() { - return this[INTERNALS].signal; + get signal() { + return this[INTERNALS].signal } - /** + /** * Clone this request * * @return Request */ - clone() { - return new Request(this); + clone() { + return new Request(this) } } -Body.mixIn(Request.prototype); +Body.mixIn(Request.prototype) Object.defineProperty(Request.prototype, Symbol.toStringTag, { - value: 'Request', - writable: false, - enumerable: false, - configurable: true -}); + value: "Request", + writable: false, + enumerable: false, + configurable: true, +}) Object.defineProperties(Request.prototype, { - method: {enumerable: true}, - url: {enumerable: true}, - headers: {enumerable: true}, - redirect: {enumerable: true}, - clone: {enumerable: true}, - signal: {enumerable: true} -}); + method: { enumerable: true }, + url: { enumerable: true }, + headers: { enumerable: true }, + redirect: { enumerable: true }, + clone: { enumerable: true }, + signal: { enumerable: true }, +}) /** * Convert a Request to Node.js http request options. @@ -173,76 +173,74 @@ Object.defineProperties(Request.prototype, { * @return Object The options object to be passed to http.request */ export function getNodeRequestOptions(request) { - const {parsedURL} = request[INTERNALS]; - const headers = new Headers(request[INTERNALS].headers); + const { parsedURL } = request[INTERNALS] + const headers = new Headers(request[INTERNALS].headers) // Fetch step 1.3 - if (!headers.has('Accept')) { - headers.set('Accept', '*/*'); + if (!headers.has("Accept")) { + headers.set("Accept", "*/*") } - // Console.log(parsedURL.protocol, parsedURL.hostname) - - // Basic fetch - if (!parsedURL.protocol || !parsedURL.hostname) { - throw new TypeError('Only absolute URLs are supported'); + // Basic fetch + if (!parsedURL.protocol || !parsedURL.hostname) { + throw new TypeError("Only absolute URLs are supported") } - if (!/^https?:$/.test(parsedURL.protocol)) { - throw new TypeError('Only HTTP(S) protocols are supported'); + if (!/^https?:$/.test(parsedURL.protocol)) { + throw new TypeError("Only HTTP(S) protocols are supported") } - if ( - request.signal && + if ( + request.signal && request.body instanceof Stream.Readable && !streamDestructionSupported - ) { - throw new Error('Cancellation of streamed requests with AbortSignal is not supported'); + ) { + throw new Error("Cancellation of streamed requests with AbortSignal is not supported") } - // HTTP-network-or-cache fetch steps 2.4-2.7 - let contentLengthValue = null; + // HTTP-network-or-cache fetch steps 2.4-2.7 + let contentLengthValue = null if (request.body == null && /^(POST|PUT)$/i.test(request.method)) { - contentLengthValue = '0'; - } + contentLengthValue = "0"; + } - if (request.body != null) { - const totalBytes = getTotalBytes(request); - if (typeof totalBytes === 'number') { - contentLengthValue = String(totalBytes); + if (request.body != null) { + const totalBytes = getTotalBytes(request) + if (typeof totalBytes === "number") { + contentLengthValue = String(totalBytes) } - } + } - if (contentLengthValue) { - headers.set('Content-Length', contentLengthValue); + if (contentLengthValue) { + headers.set("Content-Length", contentLengthValue) } - // HTTP-network-or-cache fetch step 2.11 - if (!headers.has('User-Agent')) { - headers.set('User-Agent', 'node-fetch/1.0 (+https://github.com/bitinn/node-fetch)'); + // HTTP-network-or-cache fetch step 2.11 + if (!headers.has("User-Agent")) { + headers.set("User-Agent", "node-fetch/1.0 (+https://github.com/bitinn/node-fetch)") } - // HTTP-network-or-cache fetch step 2.15 - if (request.compress && !headers.has('Accept-Encoding')) { - headers.set('Accept-Encoding', 'gzip,deflate'); + // HTTP-network-or-cache fetch step 2.15 + if (request.compress && !headers.has("Accept-Encoding")) { + headers.set("Accept-Encoding", "gzip,deflate") } - let {agent} = request; - if (typeof agent === 'function') { - agent = agent(parsedURL); + let { agent } = request + if (typeof agent === "function") { + agent = agent(parsedURL) } - if (!headers.has('Connection') && !agent) { - headers.set('Connection', 'close'); + if (!headers.has("Connection") && !agent) { + headers.set("Connection", "close") } - // HTTP-network fetch step 4.2 - // chunked encoding is handled by Node.js + // HTTP-network fetch step 4.2 + // chunked encoding is handled by Node.js - return { - ...parsedURL, - method: request.method, - headers: exportNodeCompatibleHeaders(headers), - agent - }; + return { + ...parsedURL, + method: request.method, + headers: exportNodeCompatibleHeaders(headers), + agent, + } } From a0a12005d0d36c4886609cfbd1b22907f6ffe2f3 Mon Sep 17 00:00:00 2001 From: Richie Bendall Date: Mon, 18 Nov 2019 21:30:53 +1300 Subject: [PATCH 087/157] style: Fix lint --- src/request.js | 260 ++++++++++++++++++++++++------------------------- 1 file changed, 130 insertions(+), 130 deletions(-) diff --git a/src/request.js b/src/request.js index 530c67195..468c7bdad 100644 --- a/src/request.js +++ b/src/request.js @@ -7,16 +7,16 @@ * All spec algorithm step numbers are based on https://fetch.spec.whatwg.org/commit-snapshots/ae716822cb3a61843226cd090eefc6589446c1d2/. */ -import { parse as parseUrl, format as formatUrl } from "url"; -import Stream from "stream"; -import utf8 from "utf8"; -import Headers, { exportNodeCompatibleHeaders } from "./headers"; -import Body, { clone, extractContentType, getTotalBytes } from "./body"; -import { isAbortSignal } from "./utils/is"; +import {parse as parseUrl, format as formatUrl} from 'url'; +import Stream from 'stream'; +import utf8 from 'utf8'; +import Headers, {exportNodeCompatibleHeaders} from './headers'; +import Body, {clone, extractContentType, getTotalBytes} from './body'; +import {isAbortSignal} from './utils/is'; -const INTERNALS = Symbol("Request internals") +const INTERNALS = Symbol('Request internals'); -const streamDestructionSupported = "destroy" in Stream.Readable.prototype +const streamDestructionSupported = 'destroy' in Stream.Readable.prototype; /** * Check if `obj` is an instance of Request. @@ -25,10 +25,10 @@ const streamDestructionSupported = "destroy" in Stream.Readable.prototype * @return {boolean} */ function isRequest(obj) { - return ( - typeof obj === "object" && - typeof obj[INTERNALS] === "object" - ) + return ( + typeof obj === 'object' && + typeof obj[INTERNALS] === 'object' + ); } /** @@ -39,132 +39,132 @@ function isRequest(obj) { * @return Void */ export default class Request { - constructor(input, init = {}) { - let parsedURL + constructor(input, init = {}) { + let parsedURL; // Normalize input and force URL to be encoded as UTF-8 (https://github.com/bitinn/node-fetch/issues/245) if (!isRequest(input)) { - 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(utf8.encode(input.href)) + 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(utf8.encode(input.href)); } else { - // Coerce input to a string before attempting to parse - parsedURL = parseUrl(utf8.encode(`${input}`)) + // Coerce input to a string before attempting to parse + parsedURL = parseUrl(utf8.encode(`${input}`)); } - input = {} + input = {}; } else { - parsedURL = parseUrl(utf8.encode(input.url)) + parsedURL = parseUrl(utf8.encode(input.url)); } - let method = init.method || input.method || "GET"; - method = method.toUpperCase() + let method = init.method || input.method || 'GET'; + method = method.toUpperCase(); if ((init.body != null || isRequest(input) && input.body !== null) && - (method === "GET" || method === "HEAD")) { - throw new TypeError("Request with GET/HEAD method cannot have body") + (method === 'GET' || method === 'HEAD')) { + throw new TypeError('Request with GET/HEAD method cannot have body'); } - const inputBody = init.body != null ? - init.body : - (isRequest(input) && input.body !== null ? - clone(input) : - null) + const inputBody = init.body != null ? + init.body : + (isRequest(input) && input.body !== null ? + clone(input) : + null); Body.call(this, inputBody, { - timeout: init.timeout || input.timeout || 0, - size: init.size || input.size || 0, - }) + timeout: init.timeout || input.timeout || 0, + size: init.size || input.size || 0 + }); - const headers = new Headers(init.headers || input.headers || {}) + const headers = new Headers(init.headers || input.headers || {}); - if (inputBody != null && !headers.has("Content-Type")) { - const contentType = extractContentType(inputBody) + if (inputBody != null && !headers.has('Content-Type')) { + const contentType = extractContentType(inputBody); if (contentType) { - headers.append("Content-Type", contentType) + headers.append('Content-Type', contentType); } - } + } - let signal = isRequest(input) ? - input.signal : - null - if ("signal" in init) { - signal = init.signal + let signal = isRequest(input) ? + input.signal : + null; + if ('signal' in init) { + signal = init.signal; } - if (signal != null && !isAbortSignal(signal)) { - throw new TypeError("Expected signal to be an instanceof AbortSignal") + if (signal != null && !isAbortSignal(signal)) { + throw new TypeError('Expected signal to be an instanceof AbortSignal'); } - this[INTERNALS] = { - method, - redirect: init.redirect || input.redirect || "follow", - headers, - parsedURL, - signal, - } + this[INTERNALS] = { + method, + redirect: init.redirect || input.redirect || 'follow', + headers, + parsedURL, + signal + }; // Node-fetch-only options this.follow = init.follow !== undefined ? - init.follow : (input.follow !== undefined ? - input.follow : 20) + init.follow : (input.follow !== undefined ? + input.follow : 20); this.compress = init.compress !== undefined ? - init.compress : (input.compress !== undefined ? - input.compress : true) - this.counter = init.counter || input.counter || 0 - this.agent = init.agent || input.agent - this.highWaterMark = init.highWaterMark || input.highWaterMark + init.compress : (input.compress !== undefined ? + input.compress : true); + this.counter = init.counter || input.counter || 0; + this.agent = init.agent || input.agent; + this.highWaterMark = init.highWaterMark || input.highWaterMark; } - get method() { - return this[INTERNALS].method + get method() { + return this[INTERNALS].method; } - get url() { - return formatUrl(this[INTERNALS].parsedURL) + get url() { + return formatUrl(this[INTERNALS].parsedURL); } - get headers() { - return this[INTERNALS].headers + get headers() { + return this[INTERNALS].headers; } - get redirect() { - return this[INTERNALS].redirect + get redirect() { + return this[INTERNALS].redirect; } - get signal() { - return this[INTERNALS].signal + get signal() { + return this[INTERNALS].signal; } - /** + /** * Clone this request * * @return Request */ - clone() { - return new Request(this) + clone() { + return new Request(this); } } -Body.mixIn(Request.prototype) +Body.mixIn(Request.prototype); Object.defineProperty(Request.prototype, Symbol.toStringTag, { - value: "Request", - writable: false, - enumerable: false, - configurable: true, -}) + value: 'Request', + writable: false, + enumerable: false, + configurable: true +}); Object.defineProperties(Request.prototype, { - method: { enumerable: true }, - url: { enumerable: true }, - headers: { enumerable: true }, - redirect: { enumerable: true }, - clone: { enumerable: true }, - signal: { enumerable: true }, -}) + method: {enumerable: true}, + url: {enumerable: true}, + headers: {enumerable: true}, + redirect: {enumerable: true}, + clone: {enumerable: true}, + signal: {enumerable: true} +}); /** * Convert a Request to Node.js http request options. @@ -173,74 +173,74 @@ Object.defineProperties(Request.prototype, { * @return Object The options object to be passed to http.request */ export function getNodeRequestOptions(request) { - const { parsedURL } = request[INTERNALS] - const headers = new Headers(request[INTERNALS].headers) + const {parsedURL} = request[INTERNALS]; + const headers = new Headers(request[INTERNALS].headers); // Fetch step 1.3 - if (!headers.has("Accept")) { - headers.set("Accept", "*/*") + if (!headers.has('Accept')) { + headers.set('Accept', '*/*'); } - // Basic fetch - if (!parsedURL.protocol || !parsedURL.hostname) { - throw new TypeError("Only absolute URLs are supported") + // Basic fetch + if (!parsedURL.protocol || !parsedURL.hostname) { + throw new TypeError('Only absolute URLs are supported'); } - if (!/^https?:$/.test(parsedURL.protocol)) { - throw new TypeError("Only HTTP(S) protocols are supported") + if (!/^https?:$/.test(parsedURL.protocol)) { + throw new TypeError('Only HTTP(S) protocols are supported'); } - if ( - request.signal && + if ( + request.signal && request.body instanceof Stream.Readable && !streamDestructionSupported - ) { - throw new Error("Cancellation of streamed requests with AbortSignal is not supported") + ) { + throw new Error('Cancellation of streamed requests with AbortSignal is not supported'); } - // HTTP-network-or-cache fetch steps 2.4-2.7 - let contentLengthValue = null + // HTTP-network-or-cache fetch steps 2.4-2.7 + let contentLengthValue = null; if (request.body == null && /^(POST|PUT)$/i.test(request.method)) { - contentLengthValue = "0"; - } + contentLengthValue = '0'; + } - if (request.body != null) { - const totalBytes = getTotalBytes(request) - if (typeof totalBytes === "number") { - contentLengthValue = String(totalBytes) + if (request.body != null) { + const totalBytes = getTotalBytes(request); + if (typeof totalBytes === 'number') { + contentLengthValue = String(totalBytes); } - } + } - if (contentLengthValue) { - headers.set("Content-Length", contentLengthValue) + if (contentLengthValue) { + headers.set('Content-Length', contentLengthValue); } - // HTTP-network-or-cache fetch step 2.11 - if (!headers.has("User-Agent")) { - headers.set("User-Agent", "node-fetch/1.0 (+https://github.com/bitinn/node-fetch)") + // HTTP-network-or-cache fetch step 2.11 + if (!headers.has('User-Agent')) { + headers.set('User-Agent', 'node-fetch/1.0 (+https://github.com/bitinn/node-fetch)'); } - // HTTP-network-or-cache fetch step 2.15 - if (request.compress && !headers.has("Accept-Encoding")) { - headers.set("Accept-Encoding", "gzip,deflate") + // HTTP-network-or-cache fetch step 2.15 + if (request.compress && !headers.has('Accept-Encoding')) { + headers.set('Accept-Encoding', 'gzip,deflate'); } - let { agent } = request - if (typeof agent === "function") { - agent = agent(parsedURL) + let {agent} = request; + if (typeof agent === 'function') { + agent = agent(parsedURL); } - if (!headers.has("Connection") && !agent) { - headers.set("Connection", "close") + if (!headers.has('Connection') && !agent) { + headers.set('Connection', 'close'); } - // HTTP-network fetch step 4.2 - // chunked encoding is handled by Node.js + // HTTP-network fetch step 4.2 + // chunked encoding is handled by Node.js - return { - ...parsedURL, - method: request.method, - headers: exportNodeCompatibleHeaders(headers), - agent, - } + return { + ...parsedURL, + method: request.method, + headers: exportNodeCompatibleHeaders(headers), + agent + }; } From 5334d91f7cfbf63bf1982d6d8bbb996e3dbfa423 Mon Sep 17 00:00:00 2001 From: Richie Bendall Date: Sat, 23 Nov 2019 00:01:41 +1300 Subject: [PATCH 088/157] docs: Correct typings for systemError in FetchError (Fixes #697) --- index.d.ts | 2 +- src/errors/fetch-error.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/index.d.ts b/index.d.ts index 31c57e695..b9077ce27 100644 --- a/index.d.ts +++ b/index.d.ts @@ -153,7 +153,7 @@ export class Body { export class FetchError extends Error { name: "FetchError"; [Symbol.toStringTag]: "FetchError" - constructor(message: string, type: string, systemError?: string); + constructor(message: string, type: string, systemError?: object); type: string; code?: string; errno?: string; diff --git a/src/errors/fetch-error.js b/src/errors/fetch-error.js index 3fdd378e9..4ead12317 100644 --- a/src/errors/fetch-error.js +++ b/src/errors/fetch-error.js @@ -9,7 +9,7 @@ * * @param String message Error message for human * @param String type Error type for machine - * @param String systemError For Node.js system error + * @param Object systemError For Node.js system error * @return FetchError */ export default class FetchError extends Error { From af7008787c8f452e938ba2f8d6ed516a986bf169 Mon Sep 17 00:00:00 2001 From: Richie Bendall Date: Sun, 24 Nov 2019 10:32:11 +1300 Subject: [PATCH 089/157] refactor: Replace `encoding` with `fetch-charset-detection`. (#694) * refactor: Replace `encoding` with `fetch-charset-detection`. Signed-off-by: Richie Bendall * refactor: Move writing to stream back to body.js Signed-off-by: Richie Bendall * refactor: Only put convertBody in fetch-charset-detection and refactor others. Signed-off-by: Richie Bendall * test: Readd tests for getTotalBytes and extractContentType Signed-off-by: Richie Bendall * chore: Revert package.json indention Signed-off-by: Richie Bendall * chore: Remove optional dependency * docs: Replace code for fetch-charset-detection with documentation. Signed-off-by: Richie Bendall * chore: Remove iconv-lite * fix: Use default export instead of named export for convertBody Signed-off-by: Richie Bendall * chore: Remove unneeded installation of fetch-charset-detection in the build --- .travis.yml | 2 - README.md | 11 --- docs/v3-UPGRADE-GUIDE.md | 12 +++- index.d.ts | 1 - package.json | 3 - src/body.js | 150 +++++++-------------------------------- test/server.js | 62 ---------------- test/test.js | 134 ---------------------------------- 8 files changed, 38 insertions(+), 337 deletions(-) diff --git a/.travis.yml b/.travis.yml index 892ddf3da..a47d5ab9b 100644 --- a/.travis.yml +++ b/.travis.yml @@ -14,5 +14,3 @@ cache: npm script: - npm run coverage - - npm install encoding - - npm run coverage diff --git a/README.md b/README.md index a7e87e163..d415bfce8 100644 --- a/README.md +++ b/README.md @@ -61,7 +61,6 @@ A light-weight module that brings `window.fetch` to Node.js - [body.json()](#bodyjson) - [body.text()](#bodytext) - [body.buffer()](#bodybuffer) - - [body.textConverted()](#bodytextconverted) - [Class: FetchError](#class-fetcherror) - [Class: AbortError](#class-aborterror) - [TypeScript](#typescript) @@ -663,16 +662,6 @@ Consume the body and return a promise that will resolve to one of these formats. Consume the body and return a promise that will resolve to a Buffer. -#### body.textConverted() - -_(node-fetch extension)_ - -- Returns: `Promise` - -Identical to `body.text()`, except instead of always converting to UTF-8, encoding sniffing will be performed and text converted to UTF-8, if possible. - -(This API requires an optional dependency on npm package [encoding](https://www.npmjs.com/package/encoding), which you need to install manually. `webpack` users may see [a warning message](https://github.com/bitinn/node-fetch/issues/412#issuecomment-379007792) due to this optional dependency.) - ### Class: FetchError diff --git a/docs/v3-UPGRADE-GUIDE.md b/docs/v3-UPGRADE-GUIDE.md index 10968de01..07f5c2f7a 100644 --- a/docs/v3-UPGRADE-GUIDE.md +++ b/docs/v3-UPGRADE-GUIDE.md @@ -33,7 +33,16 @@ Prior to v3.x, we included a `browser` field in the package.json file. Since nod ## Dropped the `res.textConverted()` function -If you want charset encoding detection, please use [fetch-charset-detection] package. +If you want charset encoding detection, please use the [fetch-charset-detection] package ([documentation][fetch-charset-detection-docs]). + +```js +const fetch = require("node-fetch"); +const convertBody = require("fetch-charset-detection"); + +fetch("https://somewebsite.com").then(res => { + const text = convertBody(res.buffer(), res.headers); +}); +``` # Enhancements @@ -66,6 +75,7 @@ Since v3.x you no longer need to install `@types/node-fetch` package in order to [LTS plan]: https://github.com/nodejs/LTS#lts-plan [cross-fetch]: https://github.com/lquixada/cross-fetch [fetch-charset-detection]: https://github.com/Richienb/fetch-charset-detection +[fetch-charset-detection-docs]: https://richienb.github.io/fetch-charset-detection/globals.html#convertbody [fetch-blob]: https://github.com/bitinn/fetch-blob#readme [whatwg-nodejs-url]: https://nodejs.org/api/url.html#url_the_whatwg_url_api [changelog]: CHANGELOG.md diff --git a/index.d.ts b/index.d.ts index b9077ce27..cabeb6004 100644 --- a/index.d.ts +++ b/index.d.ts @@ -146,7 +146,6 @@ export class Body { json(): Promise; size: number; text(): Promise; - textConverted(): Promise; timeout: number; } diff --git a/package.json b/package.json index 87a2a0372..453040e08 100644 --- a/package.json +++ b/package.json @@ -66,9 +66,6 @@ "fetch-blob": "^1.0.4", "utf8": "^3.0.0" }, - "optionalDependencies": { - "encoding": "^0.1.12" - }, "@pika/pack": { "pipeline": [ [ diff --git a/src/body.js b/src/body.js index 4f75966b3..02c28df10 100644 --- a/src/body.js +++ b/src/body.js @@ -11,11 +11,6 @@ import Blob from 'fetch-blob'; import FetchError from './errors/fetch-error'; import {isBlob, isURLSearchParams, isArrayBuffer, isAbortError} from './utils/is'; -let convert; -try { - convert = require('encoding').convert; -} catch (_) { } - const INTERNALS = Symbol('Body internals'); /** @@ -135,16 +130,6 @@ Body.prototype = { */ buffer() { return consumeBody.call(this); - }, - - /** - * Decode response as text, while automatically detecting the encoding and - * trying to decode to UTF-8 (non-spec api) - * - * @return Promise - */ - textConverted() { - return consumeBody.call(this).then(buffer => convertBody(buffer, this.headers)); } }; @@ -269,71 +254,6 @@ function consumeBody() { }); } -/** - * Detect buffer encoding and convert to target encoding - * ref: http://www.w3.org/TR/2011/WD-html5-20110113/parsing.html#determining-the-character-encoding - * - * @param Buffer buffer Incoming buffer - * @param String encoding Target encoding - * @return String - */ -function convertBody(buffer, headers) { - if (typeof convert !== 'function') { - throw new TypeError('The package `encoding` must be installed to use the textConverted() function'); - } - - const ct = headers.get('content-type'); - let charset = 'utf-8'; - let res; - let str; - - // Header - if (ct) { - res = /charset=([^;]*)/i.exec(ct); - } - - // No charset in content type, peek at response body for at most 1024 bytes - /* eslint-disable-next-line prefer-const */ - str = buffer.slice(0, 1024).toString(); - - // Html5 - if (!res && str) { - res = /
中文
', 'gbk')); - } - - if (p === '/encoding/gb2312') { - res.statusCode = 200; - res.setHeader('Content-Type', 'text/html'); - res.end(convert('
中文
', 'gb2312')); - } - - if (p === '/encoding/shift-jis') { - res.statusCode = 200; - res.setHeader('Content-Type', 'text/html; charset=Shift-JIS'); - res.end(convert('
日本語
', 'Shift_JIS')); - } - - if (p === '/encoding/euc-jp') { - res.statusCode = 200; - res.setHeader('Content-Type', 'text/xml'); - res.end(convert('日本語', 'EUC-JP')); - } - - if (p === '/encoding/utf8') { - res.statusCode = 200; - res.end('中文'); - } - - if (p === '/encoding/order1') { - res.statusCode = 200; - res.setHeader('Content-Type', 'charset=gbk; text/plain'); - res.end(convert('中文', 'gbk')); - } - - if (p === '/encoding/order2') { - res.statusCode = 200; - res.setHeader('Content-Type', 'text/plain; charset=gbk; qs=1'); - res.end(convert('中文', 'gbk')); - } - - if (p === '/encoding/chunked') { - res.statusCode = 200; - res.setHeader('Content-Type', 'text/html'); - res.setHeader('Transfer-Encoding', 'chunked'); - res.write('a'.repeat(10)); - res.end(convert('
日本語
', 'Shift_JIS')); - } - - if (p === '/encoding/invalid') { - res.statusCode = 200; - res.setHeader('Content-Type', 'text/html'); - res.setHeader('Transfer-Encoding', 'chunked'); - res.write('a'.repeat(1200)); - res.end(convert('中文', 'gbk')); - } - if (p === '/redirect/301') { res.statusCode = 301; res.setHeader('Location', '/inspect'); diff --git a/test/test.js b/test/test.js index 4a5652d0e..751ed99f3 100644 --- a/test/test.js +++ b/test/test.js @@ -40,11 +40,6 @@ const { Uint8Array: VMUint8Array } = vm.runInNewContext('this'); -let convert; -try { - convert = require('encoding').convert; -} catch (_) { } - import chaiTimeout from './chai-timeout'; chai.use(chaiPromised); @@ -2826,135 +2821,6 @@ function streamToPromise(stream, dataHandler) { } describe('external encoding', () => { - const hasEncoding = typeof convert === 'function'; - - describe('with optional `encoding`', () => { - before(function () { - if (!hasEncoding) { - this.skip(); - } - }); - - it('should only use UTF-8 decoding with text()', () => { - const url = `${base}encoding/euc-jp`; - return fetch(url).then(res => { - expect(res.status).to.equal(200); - return res.text().then(result => { - expect(result).to.equal('\uFFFD\uFFFD\uFFFD\u0738\ufffd'); - }); - }); - }); - - it('should support encoding decode, xml dtd detect', () => { - const url = `${base}encoding/euc-jp`; - return fetch(url).then(res => { - expect(res.status).to.equal(200); - return res.textConverted().then(result => { - expect(result).to.equal('日本語'); - }); - }); - }); - - it('should support encoding decode, content-type detect', () => { - const url = `${base}encoding/shift-jis`; - return fetch(url).then(res => { - expect(res.status).to.equal(200); - return res.textConverted().then(result => { - expect(result).to.equal('
日本語
'); - }); - }); - }); - - it('should support encoding decode, html5 detect', () => { - const url = `${base}encoding/gbk`; - return fetch(url).then(res => { - expect(res.status).to.equal(200); - return res.textConverted().then(result => { - expect(result).to.equal('
中文
'); - }); - }); - }); - - it('should support encoding decode, html4 detect', () => { - const url = `${base}encoding/gb2312`; - return fetch(url).then(res => { - expect(res.status).to.equal(200); - return res.textConverted().then(result => { - expect(result).to.equal('
中文
'); - }); - }); - }); - - it('should default to utf8 encoding', () => { - const url = `${base}encoding/utf8`; - return fetch(url).then(res => { - expect(res.status).to.equal(200); - expect(res.headers.get('content-type')).to.be.null; - return res.textConverted().then(result => { - expect(result).to.equal('中文'); - }); - }); - }); - - it('should support uncommon content-type order, charset in front', () => { - const url = `${base}encoding/order1`; - return fetch(url).then(res => { - expect(res.status).to.equal(200); - return res.textConverted().then(result => { - expect(result).to.equal('中文'); - }); - }); - }); - - it('should support uncommon content-type order, end with qs', () => { - const url = `${base}encoding/order2`; - return fetch(url).then(res => { - expect(res.status).to.equal(200); - return res.textConverted().then(result => { - expect(result).to.equal('中文'); - }); - }); - }); - - it('should support chunked encoding, html4 detect', () => { - const url = `${base}encoding/chunked`; - return fetch(url).then(res => { - expect(res.status).to.equal(200); - const padding = 'a'.repeat(10); - return res.textConverted().then(result => { - expect(result).to.equal(`${padding}
日本語
`); - }); - }); - }); - - it('should only do encoding detection up to 1024 bytes', () => { - const url = `${base}encoding/invalid`; - return fetch(url).then(res => { - expect(res.status).to.equal(200); - const padding = 'a'.repeat(1200); - return res.textConverted().then(result => { - expect(result).to.not.equal(`${padding}中文`); - }); - }); - }); - }); - - describe('without optional `encoding`', () => { - before(function () { - if (hasEncoding) { - this.skip(); - } - }); - - it('should throw a FetchError if res.textConverted() is called without `encoding` in require cache', () => { - const url = `${base}hello`; - return fetch(url).then(res => { - return expect(res.textConverted()).to.eventually.be.rejected - .and.have.property('message').which.includes('encoding'); - }); - }); - }); - describe('data uri', () => { it('should accept data uri', () => { return fetch('data:image/gif;base64,R0lGODlhAQABAIAAAAUEBAAAACwAAAAAAQABAAACAkQBADs=').then(r => { From fdd052598370a599625a10368886cc1ab03bc6d0 Mon Sep 17 00:00:00 2001 From: Richie Bendall Date: Mon, 25 Nov 2019 00:21:00 +1300 Subject: [PATCH 090/157] docs: Fix typo --- docs/v3-UPGRADE-GUIDE.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/v3-UPGRADE-GUIDE.md b/docs/v3-UPGRADE-GUIDE.md index 07f5c2f7a..1d95ea5fa 100644 --- a/docs/v3-UPGRADE-GUIDE.md +++ b/docs/v3-UPGRADE-GUIDE.md @@ -56,7 +56,7 @@ Blob implementation is now [fetch-blob] and hence is exposed, unlikely previousl ## Better UTF-8 URL handling -We now use the new Node.js [WHATWG-compliant URL API][whatwg-nodejs-url], so UTF-8 URLs are handles properly. +We now use the new Node.js [WHATWG-compliant URL API][whatwg-nodejs-url], so UTF-8 URLs are handled properly. ## Request errors are now piped using `stream.pipeline` From 3f3d76d0dd604972270e1356909542b387d07c43 Mon Sep 17 00:00:00 2001 From: Richie Bendall Date: Fri, 29 Nov 2019 19:41:59 +1300 Subject: [PATCH 091/157] =?UTF-8?q?fix:=20Throw=20SyntaxError=20instead=20?= =?UTF-8?q?of=20FetchError=20in=20case=20of=20invalid=E2=80=A6=20(#700)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: Throw SyntaxError instead of FetchError in case of invalid JSON Signed-off-by: Richie Bendall * docs: Add to upgrade guide Signed-off-by: Richie Bendall --- docs/v3-UPGRADE-GUIDE.md | 11 +++++++++++ src/body.js | 8 +------- test/test.js | 8 ++------ 3 files changed, 14 insertions(+), 13 deletions(-) diff --git a/docs/v3-UPGRADE-GUIDE.md b/docs/v3-UPGRADE-GUIDE.md index 1d95ea5fa..eae9a64f0 100644 --- a/docs/v3-UPGRADE-GUIDE.md +++ b/docs/v3-UPGRADE-GUIDE.md @@ -44,6 +44,17 @@ fetch("https://somewebsite.com").then(res => { }); ``` +## JSON parsing errors from `res.json()` are of type `SyntaxError` instead of `FetchError` + +When attemping to parse invalid json via `res.json()`, a `SyntaxError` will now be thrown instead of a `FetchError` to align better with the spec. + +```js +const fetch = require("node-fetch"); + +fetch("https://somewebsitereturninginvalidjson.com").then(res => res.json()) +// Throws 'Uncaught SyntaxError: Unexpected end of JSON input' or similar. +``` + # Enhancements ## Data URI support diff --git a/src/body.js b/src/body.js index 02c28df10..c684008af 100644 --- a/src/body.js +++ b/src/body.js @@ -105,13 +105,7 @@ Body.prototype = { * @return Promise */ json() { - return consumeBody.call(this).then(buffer => { - try { - return JSON.parse(buffer.toString()); - } catch (error) { - return Body.Promise.reject(new FetchError(`invalid json response body at ${this.url} reason: ${error.message}`, 'invalid-json')); - } - }); + return consumeBody.call(this).then(buffer => JSON.parse(buffer.toString())); }, /** diff --git a/test/test.js b/test/test.js index 751ed99f3..b2423f31f 100644 --- a/test/test.js +++ b/test/test.js @@ -572,9 +572,7 @@ describe('node-fetch', () => { const url = `${base}error/json`; return fetch(url).then(res => { expect(res.headers.get('content-type')).to.equal('application/json'); - return expect(res.json()).to.eventually.be.rejected - .and.be.an.instanceOf(FetchError) - .and.include({type: 'invalid-json'}); + return expect(res.json()).to.eventually.be.rejectedWith(Error); }); }); @@ -597,9 +595,7 @@ describe('node-fetch', () => { expect(res.status).to.equal(204); expect(res.statusText).to.equal('No Content'); expect(res.ok).to.be.true; - return expect(res.json()).to.eventually.be.rejected - .and.be.an.instanceOf(FetchError) - .and.include({type: 'invalid-json'}); + return expect(res.json()).to.eventually.be.rejectedWith(Error); }); }); From d76595788341a11a136f0d04fe72ad9519ac5708 Mon Sep 17 00:00:00 2001 From: Antoni Kepinski Date: Mon, 2 Dec 2019 19:40:40 +0100 Subject: [PATCH 092/157] Remove deprecated url.parse from test --- test/test.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/test/test.js b/test/test.js index b2423f31f..1a7d6ddd7 100644 --- a/test/test.js +++ b/test/test.js @@ -6,7 +6,6 @@ import * as http from 'http'; import * as fs from 'fs'; import * as path from 'path'; import * as stream from 'stream'; -import {parse as parseURL} from 'url'; import {lookup} from 'dns'; import vm from 'vm'; import chai from 'chai'; @@ -1858,7 +1857,7 @@ describe('node-fetch', () => { it('should support fetch with Node.js URL object', () => { const url = `${base}hello`; - const urlObj = parseURL(url); + const urlObj = new URL(url); const req = new Request(urlObj); return fetch(req).then(res => { expect(res.url).to.equal(url); From fbbfc2b1ed2f75258e6257fa7c9a60e6fec75936 Mon Sep 17 00:00:00 2001 From: Antoni Kepinski Date: Mon, 2 Dec 2019 19:41:28 +0100 Subject: [PATCH 093/157] Remove deprecated url.parse from server --- test/server.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/test/server.js b/test/server.js index dad2af5b5..318f3adc8 100644 --- a/test/server.js +++ b/test/server.js @@ -1,5 +1,4 @@ import * as http from 'http'; -import {parse} from 'url'; import * as zlib from 'zlib'; import {multipart as Multipart} from 'parted'; @@ -33,7 +32,7 @@ export default class TestServer { } router(req, res) { - const p = parse(req.url).pathname; + const p = req.url; if (p === '/mocked') { if (this.nextResponseHandler) { From ac3e66b03f7d1a608e26d1dbb9dfa396452edb3f Mon Sep 17 00:00:00 2001 From: Richie Bendall Date: Fri, 13 Dec 2019 22:04:34 +1300 Subject: [PATCH 094/157] docs: Users will never use NodeJS v8 because minimum supported is v10 --- README.md | 4 ---- 1 file changed, 4 deletions(-) diff --git a/README.md b/README.md index e0b5266b4..302ac2ed6 100644 --- a/README.md +++ b/README.md @@ -369,10 +369,6 @@ fetch('https://httpbin.org/post', {method: 'POST', body: form}) .then(json => console.log(json)); ``` -#### Request cancellation with AbortSignal - -> NOTE: You may only cancel streamed requests on Node >= v8.0.0 - ### Request cancellation with AbortSignal You may cancel requests with `AbortController`. A suggested implementation is [`abort-controller`](https://www.npmjs.com/package/abort-controller). From df077ba50fc106362103ce7c6cd7b762f7342bf8 Mon Sep 17 00:00:00 2001 From: Richie Bendall Date: Fri, 13 Dec 2019 22:05:07 +1300 Subject: [PATCH 095/157] style: Fix indention --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index dc2d76979..7191c082e 100644 --- a/package.json +++ b/package.json @@ -54,7 +54,7 @@ "codecov": "^3.6.1", "cross-env": "^6.0.3", "form-data": "^2.5.1", - "formdata-node": "^1.5.2", + "formdata-node": "^1.5.2", "mocha": "^6.2.2", "nyc": "^14.1.1", "parted": "^0.1.1", From 61f24466596244c21449252f7e30a68c86fbf7f9 Mon Sep 17 00:00:00 2001 From: Richie Bendall Date: Fri, 13 Dec 2019 22:07:14 +1300 Subject: [PATCH 096/157] style: Sentence case for comments --- src/body.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/body.js b/src/body.js index dea0ed81b..ff1e728f8 100644 --- a/src/body.js +++ b/src/body.js @@ -48,9 +48,9 @@ export default function Body(body, { // Body is ArrayBufferView body = Buffer.from(body.buffer, body.byteOffset, body.byteLength); } else if (body instanceof Stream) { - // body is stream + // Body is stream } else if (body.stream instanceof Stream.Readable && typeof body.boundary === 'string') { - // body is an instance of formdata-node + // Body is an instance of formdata-node body = body.stream } else { // None of the above @@ -421,7 +421,7 @@ export function extractContentType(body) { } else if (body.stream instanceof Stream.Readable && typeof body.boundary === 'string') { return `multipart/form-data;boundary=${body.boundary}`; } else if (body instanceof Stream) { - // body is stream + // Body is stream // can't really do much about this return null; } From 27274138162f743e90e7acd73c064aaaf87f8a17 Mon Sep 17 00:00:00 2001 From: Richie Bendall Date: Fri, 13 Dec 2019 22:11:27 +1300 Subject: [PATCH 097/157] style: Use arrow functions and fix indent --- test/test.js | 46 +++++++++++++++++++++------------------------- 1 file changed, 21 insertions(+), 25 deletions(-) diff --git a/test/test.js b/test/test.js index 0566fd2f7..dc2fb75d1 100644 --- a/test/test.js +++ b/test/test.js @@ -1378,9 +1378,7 @@ describe('node-fetch', () => { body: form, headers }; - return fetch(url, opts).then(res => { - return res.json(); - }).then(res => { + return fetch(url, opts).then(res => res.json()).then(res => { expect(res.method).to.equal('POST'); expect(res.headers['content-type']).to.startWith('multipart/form-data; boundary='); expect(res.headers['content-length']).to.be.a('string'); @@ -1389,29 +1387,27 @@ describe('node-fetch', () => { }); }); - it('should support formdata-node as POST body', function() { - const form = new FormDataNode(); - - form.set('field', "some text"); - form.set('file', fs.createReadStream(path.join(__dirname, 'dummy.txt'))); - - const url = `${base}multipart`; - const opts = { - method: 'POST', - body: form - }; - - return fetch(url, opts) - .then(res => res.json()) - .then(res => { - expect(res.method).to.equal('POST'); - expect(res.headers['content-type']).to.equal(`multipart/form-data;boundary=${form.boundary}`); - expect(res.body).to.contain('field='); - expect(res.body).to.contain('file='); - }); - }); + it('should support formdata-node as POST body', () => { + const form = new FormDataNode(); + + form.set('field', "some text"); + form.set('file', fs.createReadStream(path.join(__dirname, 'dummy.txt'))) + + const url = `${base}multipart`; + const opts = { + method: 'POST', + body: form + }; + + return fetch(url, opts).then(res => res.json()).then(res => { + expect(res.method).to.equal('POST'); + expect(res.headers['content-type']).to.equal(`multipart/form-data;boundary=${form.boundary}`); + expect(res.body).to.contain('field='); + expect(res.body).to.contain('file='); + }); + }); - it('should allow POST request with object body', function() { + it('should allow POST request with object body', () => { const url = `${base}inspect`; // Note that fetch simply calls tostring on an object const opts = { From 1019f591a13d5854b38bf57e0dd6d9d024b72ad0 Mon Sep 17 00:00:00 2001 From: Nick K Date: Fri, 13 Dec 2019 12:45:02 +0300 Subject: [PATCH 098/157] Update formdata-node to 2.x version. --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 312a132de..db27edacd 100644 --- a/package.json +++ b/package.json @@ -54,7 +54,7 @@ "codecov": "^3.6.1", "cross-env": "^6.0.3", "form-data": "^2.5.1", - "formdata-node": "^1.5.2", + "formdata-node": "^2.0.0", "mocha": "^6.2.2", "nyc": "^14.1.1", "parted": "^0.1.1", From a2e8f64d42cb3761bdadffe1b73b38fa0267ce20 Mon Sep 17 00:00:00 2001 From: Richie Bendall Date: Sun, 15 Dec 2019 22:16:33 +1300 Subject: [PATCH 099/157] fix: Proper data uri to buffer conversion (#703) Signed-off-by: Richie Bendall --- package.json | 1 + src/index.js | 5 +++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 453040e08..ffe5beaee 100644 --- a/package.json +++ b/package.json @@ -63,6 +63,7 @@ "xo": "^0.25.3" }, "dependencies": { + "data-uri-to-buffer": "^3.0.0", "fetch-blob": "^1.0.4", "utf8": "^3.0.0" }, diff --git a/src/index.js b/src/index.js index e1ed83172..f8424184b 100644 --- a/src/index.js +++ b/src/index.js @@ -10,6 +10,7 @@ import http from 'http'; import https from 'https'; import zlib from 'zlib'; import Stream, {PassThrough, pipeline as pump} from 'stream'; +import dataURIToBuffer from 'data-uri-to-buffer'; import Body, {writeToStream, getTotalBytes} from './body'; import Response from './response'; @@ -36,8 +37,8 @@ export default function fetch(url, opts) { // If valid data uri if (dataUriRegex.test(url)) { - const data = Buffer.from(url.split(',')[1], 'base64'); - const res = new Response(data.body, {headers: {'Content-Type': data.mimeType || url.match(dataUriRegex)[1] || 'text/plain'}}); + const data = dataURIToBuffer(url); + const res = new Response(data, {headers: {'Content-Type': data.type}}); return fetch.Promise.resolve(res); } From 36aec778dc27c4a5974128554fad8b155344944b Mon Sep 17 00:00:00 2001 From: Richie Bendall Date: Wed, 18 Dec 2019 20:28:53 +1300 Subject: [PATCH 100/157] chore: Add funding info --- .github/FUNDING.yml | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 .github/FUNDING.yml diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 000000000..78f6bbf83 --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1,12 @@ +# These are supported funding model platforms + +github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] +patreon: # Replace with a single Patreon username +open_collective: node-fetch # Replace with a single Open Collective username +ko_fi: # Replace with a single Ko-fi username +tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel +community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry +liberapay: # Replace with a single Liberapay username +issuehunt: # Replace with a single IssueHunt username +otechie: # Replace with a single Otechie username +custom: # Replace with a single custom sponsorship URL From c701d3cf3d983e3f100f0f63f7631e69685cc54a Mon Sep 17 00:00:00 2001 From: Erick Calder Date: Wed, 18 Dec 2019 14:44:32 -0800 Subject: [PATCH 101/157] fix: Flawed property existence test (#706) Fix a problem where not all prototype methods are copied from the Body via the mixin method due to a failure to properly detect properties in the target. The current code uses the `in` operator, which may return properties lower down the inheritance chain, thus causing them to fail the copy. The new code properly calls the `.hasOwnProperty()` method to make the determination. --- src/body.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/body.js b/src/body.js index c684008af..ebb759fc9 100644 --- a/src/body.js +++ b/src/body.js @@ -140,7 +140,7 @@ Object.defineProperties(Body.prototype, { Body.mixIn = proto => { for (const name of Object.getOwnPropertyNames(Body.prototype)) { // istanbul ignore else: future proof - if (!(name in proto)) { + if (!Object.prototype.hasOwnProperty.call(proto, name)) { const desc = Object.getOwnPropertyDescriptor(Body.prototype, name); Object.defineProperty(proto, name, desc); } From 4fe44608f99ce05316d392ed73ed008bfd9d9b04 Mon Sep 17 00:00:00 2001 From: Richie Bendall Date: Sat, 28 Dec 2019 18:48:31 +1300 Subject: [PATCH 102/157] fix: Properly handle stream pipeline double-fire Signed-off-by: Richie Bendall --- docs/v3-UPGRADE-GUIDE.md | 4 ++++ test/test.js | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/docs/v3-UPGRADE-GUIDE.md b/docs/v3-UPGRADE-GUIDE.md index eae9a64f0..b2a834716 100644 --- a/docs/v3-UPGRADE-GUIDE.md +++ b/docs/v3-UPGRADE-GUIDE.md @@ -55,6 +55,10 @@ fetch("https://somewebsitereturninginvalidjson.com").then(res => res.json()) // Throws 'Uncaught SyntaxError: Unexpected end of JSON input' or similar. ``` +## A stream pipeline is now used to forward errors + +If you are listening for errors via `res.body.on('error', () => ...)`, replace it with `res.body.once('error', () => ...)` so that your callback is not [fired twice](https://github.com/bitinn/node-fetch/issues/668#issuecomment-569386115) in NodeJS >=13.5. + # Enhancements ## Data URI support diff --git a/test/test.js b/test/test.js index 1a7d6ddd7..7a4722af3 100644 --- a/test/test.js +++ b/test/test.js @@ -1035,7 +1035,7 @@ describe('node-fetch', () => { )) .to.eventually.be.fulfilled .then(res => { - res.body.on('error', err => { + res.body.once('error', err => { expect(err) .to.be.an.instanceof(Error) .and.have.property('name', 'AbortError'); From 1d1b0c3bf22400d8cf9208d726e8de7aa8af8d0e Mon Sep 17 00:00:00 2001 From: Richie Bendall Date: Sat, 28 Dec 2019 18:50:33 +1300 Subject: [PATCH 103/157] docs: Fix spelling Signed-off-by: Richie Bendall --- docs/CHANGELOG.md | 6 +++--- docs/v3-UPGRADE-GUIDE.md | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 3fb456cd4..ebfd100c2 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -65,7 +65,7 @@ Changelog ## v2.2.1 - Fix: `compress` flag shouldn't overwrite existing `Accept-Encoding` header. -- Fix: multiple `import` rules, where `PassThrough` etc. doesn't have a named export when using node <10 and `--exerimental-modules` flag. +- Fix: multiple `import` rules, where `PassThrough` etc. doesn't have a named export when using node <10 and `--experimental-modules` flag. - Other: Better README. ## v2.2.0 @@ -124,7 +124,7 @@ This is a major release. Check [our upgrade guide](https://github.com/bitinn/nod ### Response and Request classes - Major: `response.text()` no longer attempts to detect encoding, instead always opting for UTF-8 (per spec); use `response.textConverted()` for the v1 behavior -- Major: make `response.json()` throw error instead of returning an empty object on 204 no-content respose (per spec; reverts behavior changed in v1.6.2) +- Major: make `response.json()` throw error instead of returning an empty object on 204 no-content response (per spec; reverts behavior changed in v1.6.2) - Major: internal methods are no longer exposed - Major: throw error when a `GET` or `HEAD` Request is constructed with a non-null body (per spec) - Enhance: add `response.arrayBuffer()` (also applies to Requests) @@ -149,7 +149,7 @@ This is a major release. Check [our upgrade guide](https://github.com/bitinn/nod # 1.x release -## backport releases (v1.7.0 and beyond) +## Backport releases (v1.7.0 and beyond) See [changelog on 1.x branch](https://github.com/bitinn/node-fetch/blob/1.x/CHANGELOG.md) for details. diff --git a/docs/v3-UPGRADE-GUIDE.md b/docs/v3-UPGRADE-GUIDE.md index b2a834716..16e0aae6f 100644 --- a/docs/v3-UPGRADE-GUIDE.md +++ b/docs/v3-UPGRADE-GUIDE.md @@ -46,7 +46,7 @@ fetch("https://somewebsite.com").then(res => { ## JSON parsing errors from `res.json()` are of type `SyntaxError` instead of `FetchError` -When attemping to parse invalid json via `res.json()`, a `SyntaxError` will now be thrown instead of a `FetchError` to align better with the spec. +When attempting to parse invalid json via `res.json()`, a `SyntaxError` will now be thrown instead of a `FetchError` to align better with the spec. ```js const fetch = require("node-fetch"); From b53a08a300403f84e81bddf39a8f5ad990ed6dee Mon Sep 17 00:00:00 2001 From: octet-stream Date: Sat, 28 Dec 2019 23:49:45 +0300 Subject: [PATCH 104/157] Add isFormData utility to check if given object is a spec-compliant FormData. --- src/utils/is.js | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/src/utils/is.js b/src/utils/is.js index ce745bd30..f92208635 100644 --- a/src/utils/is.js +++ b/src/utils/is.js @@ -44,6 +44,28 @@ export function isBlob(obj) { ); } +/** + * Check if `obj` is a spec-compliant `FormData` object + * + * @param {*} + * @return {boolean} + */ +export function isFormData(obj) { + return ( + typeof obj === 'object' && + typeof obj.append === 'function' && + typeof obj.set === 'function' && + typeof obj.get === 'function' && + typeof obj.getAll === 'function' && + typeof obj.delete === 'function' && + typeof obj.keys === 'function' && + typeof obj.values === 'function' && + typeof obj.entries === 'function' && + typeof obj.constructor === 'function' && + obj.constructor.name === 'FormData' + ); +} + /** * Check if `obj` is an instance of AbortSignal. * From 184c0ef5e0dc14d4065d4562739064bc377cc9ef Mon Sep 17 00:00:00 2001 From: Antoni Kepinski Date: Mon, 30 Dec 2019 11:50:01 +0100 Subject: [PATCH 105/157] chore: Add `funding` field to package.json (#708) --- package.json | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/package.json b/package.json index ffe5beaee..beef5cb6c 100644 --- a/package.json +++ b/package.json @@ -36,6 +36,10 @@ "url": "https://github.com/bitinn/node-fetch/issues" }, "homepage": "https://github.com/bitinn/node-fetch", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/node-fetch" + }, "devDependencies": { "@babel/core": "^7.6.4", "@babel/preset-env": "^7.6.3", From 73da445ae7fead4cfd1a5e257f6e847a13971696 Mon Sep 17 00:00:00 2001 From: Yaacov Rydzinski Date: Tue, 31 Dec 2019 08:20:33 -0500 Subject: [PATCH 106/157] Fix: Do not set ContentLength to NaN (#709) * do not set ContentLength to NaN * lint --- src/request.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/request.js b/src/request.js index 468c7bdad..4a9a7e753 100644 --- a/src/request.js +++ b/src/request.js @@ -206,7 +206,8 @@ export function getNodeRequestOptions(request) { if (request.body != 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); } } From 63370cc8647ea09d1972f6b8a4dcdabf8bca0387 Mon Sep 17 00:00:00 2001 From: octet-stream Date: Wed, 1 Jan 2020 23:26:04 +0300 Subject: [PATCH 107/157] Add FormDataStream class to implement form-data encoding. --- package.json | 1 + src/body.js | 22 ++++++++----- src/request.js | 2 +- src/utils/FormDataStream.js | 61 +++++++++++++++++++++++++++++++++++++ src/utils/boundary.js | 10 ++++++ test/test.js | 16 ++++++---- 6 files changed, 98 insertions(+), 14 deletions(-) create mode 100644 src/utils/FormDataStream.js create mode 100644 src/utils/boundary.js diff --git a/package.json b/package.json index 77c6e64ba..edb564950 100644 --- a/package.json +++ b/package.json @@ -66,6 +66,7 @@ "dependencies": { "data-uri-to-buffer": "^3.0.0", "fetch-blob": "^1.0.4", + "nanoid": "^2.1.8", "utf8": "^3.0.0" }, "@pika/pack": { diff --git a/src/body.js b/src/body.js index fd530a6de..27d98cebc 100644 --- a/src/body.js +++ b/src/body.js @@ -8,8 +8,10 @@ import Stream, {PassThrough} from 'stream'; import Blob from 'fetch-blob'; +import getBoundary from './utils/boundary'; import FetchError from './errors/fetch-error'; -import {isBlob, isURLSearchParams, isArrayBuffer, isAbortError} from './utils/is'; +import FormDataStream from './utils/FormDataStream'; +import {isBlob, isURLSearchParams, isArrayBuffer, isAbortError, isFormData} from './utils/is'; const INTERNALS = Symbol('Body internals'); @@ -26,6 +28,8 @@ export default function Body(body, { size = 0, timeout = 0 } = {}) { + let boundary = null; + if (body == null) { // Body is undefined or null body = null; @@ -44,9 +48,10 @@ export default function Body(body, { body = Buffer.from(body.buffer, body.byteOffset, body.byteLength); } else if (body instanceof Stream) { // Body is stream - } else if (body.stream instanceof Stream.Readable && typeof body.boundary === 'string') { + } else if (isFormData(body)) { // Body is an instance of formdata-node - body = body.stream + boundary = `NodeFetchFormDataBoundary${getBoundary()}`; + body = new FormDataStream(body, boundary) } else { // None of the above // coerce to string then buffer @@ -55,6 +60,7 @@ export default function Body(body, { this[INTERNALS] = { body, + boundary, disturbed: false, error: null }; @@ -155,7 +161,7 @@ Body.mixIn = proto => { * * Ref: https://fetch.spec.whatwg.org/#concept-body-consume-body * - * @return Promise + * @return Promise */ function consumeBody() { if (this[INTERNALS].disturbed) { @@ -323,8 +329,10 @@ export function extractContentType(body) { // Detect form data input from form-data module if (body && typeof body.getBoundary === 'function') { return `multipart/form-data;boundary=${body.getBoundary()}`; - } else if (body.stream instanceof Stream.Readable && typeof body.boundary === 'string') { - return `multipart/form-data;boundary=${body.boundary}`; + } + + if (isFormData(body)) { + return `multipart/form-data; boundary=${this[INTERNALS].boundary}`; } // Body is stream - can't really do much about this @@ -377,7 +385,7 @@ export function getTotalBytes({body}) { * @param obj.body Body object from the Body instance. * @returns {void} */ -export function writeToStream(dest, {body}) { +export function writeToStream(dest, {body, headers}) { if (body == null) { // Body is null dest.end(); diff --git a/src/request.js b/src/request.js index 468c7bdad..eb403f6fc 100644 --- a/src/request.js +++ b/src/request.js @@ -81,7 +81,7 @@ export default class Request { const headers = new Headers(init.headers || input.headers || {}); if (inputBody != null && !headers.has('Content-Type')) { - const contentType = extractContentType(inputBody); + const contentType = extractContentType.call(this, inputBody); if (contentType) { headers.append('Content-Type', contentType); } diff --git a/src/utils/FormDataStream.js b/src/utils/FormDataStream.js new file mode 100644 index 000000000..275842b45 --- /dev/null +++ b/src/utils/FormDataStream.js @@ -0,0 +1,61 @@ +import {Readable} from 'stream'; + +import {isBlob} from './is'; + +class FormDataStream extends Readable { + constructor(form, boundary) { + super() + + this._carriage = '\r\n'; + this._dashes = '-'.repeat(2); + this._boundary = boundary; + this._form = form; + this._curr = this._readField(); + } + + _getHeader(name, field) { + let header = ''; + + header += `${this._dashes}${this._boundary}${this._carriage}`; + header += `Content-Disposition: form-data; name="${name}"`; + + if (isBlob(field)) { + header += `; filename="${field.name}"${this._carriage}`; + header += `Content-Type: ${field.type || "application/octet-stream"}`; + } + + return `${header}${this._carriage.repeat(2)}`; + } + + async* _readField() { + for (const [name, field] of this._form) { + yield this._getHeader(name, field) + + if (isBlob(field)) { + yield* field.stream() + } else { + yield field + } + + yield this._carriage + } + + yield `${this._dashes}${this._boundary}${this._dashes}${this._carriage.repeat(2)}`; + } + + _read() { + const onFulfilled = ({done, value}) => { + if (done) { + return this.push(null); + } + + this.push(Buffer.isBuffer(value) ? value : Buffer.from(String(value))); + }; + + const onRejected = err => this.emit('error', err); + + this._curr.next().then(onFulfilled).catch(onRejected) + } +} + +export default FormDataStream diff --git a/src/utils/boundary.js b/src/utils/boundary.js new file mode 100644 index 000000000..da0d51df2 --- /dev/null +++ b/src/utils/boundary.js @@ -0,0 +1,10 @@ +import generate from 'nanoid/generate'; + +const alpha = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'; + +/** + * @api private + */ +const boundary = () => generate(alpha, 22); + +export default boundary; diff --git a/test/test.js b/test/test.js index d2937a687..92907783f 100644 --- a/test/test.js +++ b/test/test.js @@ -1387,21 +1387,25 @@ describe('node-fetch', () => { }); }); - it('should support formdata-node as POST body', () => { + it('should support formdata-node as POST body', async () => { const form = new FormDataNode(); - + + const filename = path.join(__dirname, 'dummy.txt'); + form.set('field', "some text"); - form.set('file', fs.createReadStream(path.join(__dirname, 'dummy.txt'))) - + form.set('file', fs.createReadStream(filename), { + size: await fs.promises.stat(filename) + }) + const url = `${base}multipart`; const opts = { method: 'POST', body: form }; - + return fetch(url, opts).then(res => res.json()).then(res => { expect(res.method).to.equal('POST'); - expect(res.headers['content-type']).to.equal(`multipart/form-data;boundary=${form.boundary}`); + // expect(res.headers['content-type']).to.equal(`multipart/form-data; boundary=${form.boundary}`); expect(res.body).to.contain('field='); expect(res.body).to.contain('file='); }); From 265167c2f613b927e0c532419eb70910d25d2cb5 Mon Sep 17 00:00:00 2001 From: octet-stream Date: Wed, 1 Jan 2020 23:33:14 +0300 Subject: [PATCH 108/157] Fix lint errors. --- src/body.js | 6 ++-- src/utils/FormDataStream.js | 61 ------------------------------------- test/test.js | 8 ++--- 3 files changed, 7 insertions(+), 68 deletions(-) delete mode 100644 src/utils/FormDataStream.js diff --git a/src/body.js b/src/body.js index 27d98cebc..bbd42ac5e 100644 --- a/src/body.js +++ b/src/body.js @@ -10,7 +10,7 @@ import Stream, {PassThrough} from 'stream'; import Blob from 'fetch-blob'; import getBoundary from './utils/boundary'; import FetchError from './errors/fetch-error'; -import FormDataStream from './utils/FormDataStream'; +import FormDataStream from './utils/form-data-stream'; import {isBlob, isURLSearchParams, isArrayBuffer, isAbortError, isFormData} from './utils/is'; const INTERNALS = Symbol('Body internals'); @@ -51,7 +51,7 @@ export default function Body(body, { } else if (isFormData(body)) { // Body is an instance of formdata-node boundary = `NodeFetchFormDataBoundary${getBoundary()}`; - body = new FormDataStream(body, boundary) + body = new FormDataStream(body, boundary); } else { // None of the above // coerce to string then buffer @@ -385,7 +385,7 @@ export function getTotalBytes({body}) { * @param obj.body Body object from the Body instance. * @returns {void} */ -export function writeToStream(dest, {body, headers}) { +export function writeToStream(dest, {body}) { if (body == null) { // Body is null dest.end(); diff --git a/src/utils/FormDataStream.js b/src/utils/FormDataStream.js deleted file mode 100644 index 275842b45..000000000 --- a/src/utils/FormDataStream.js +++ /dev/null @@ -1,61 +0,0 @@ -import {Readable} from 'stream'; - -import {isBlob} from './is'; - -class FormDataStream extends Readable { - constructor(form, boundary) { - super() - - this._carriage = '\r\n'; - this._dashes = '-'.repeat(2); - this._boundary = boundary; - this._form = form; - this._curr = this._readField(); - } - - _getHeader(name, field) { - let header = ''; - - header += `${this._dashes}${this._boundary}${this._carriage}`; - header += `Content-Disposition: form-data; name="${name}"`; - - if (isBlob(field)) { - header += `; filename="${field.name}"${this._carriage}`; - header += `Content-Type: ${field.type || "application/octet-stream"}`; - } - - return `${header}${this._carriage.repeat(2)}`; - } - - async* _readField() { - for (const [name, field] of this._form) { - yield this._getHeader(name, field) - - if (isBlob(field)) { - yield* field.stream() - } else { - yield field - } - - yield this._carriage - } - - yield `${this._dashes}${this._boundary}${this._dashes}${this._carriage.repeat(2)}`; - } - - _read() { - const onFulfilled = ({done, value}) => { - if (done) { - return this.push(null); - } - - this.push(Buffer.isBuffer(value) ? value : Buffer.from(String(value))); - }; - - const onRejected = err => this.emit('error', err); - - this._curr.next().then(onFulfilled).catch(onRejected) - } -} - -export default FormDataStream diff --git a/test/test.js b/test/test.js index 92907783f..08b5dac45 100644 --- a/test/test.js +++ b/test/test.js @@ -15,7 +15,7 @@ import chaiString from 'chai-string'; import then from 'promise'; import resumer from 'resumer'; import FormData from 'form-data'; -import FormDataNode from "formdata-node"; +import FormDataNode from 'formdata-node'; import stringToArrayBuffer from 'string-to-arraybuffer'; import {AbortController} from 'abortcontroller-polyfill/dist/abortcontroller'; @@ -1392,10 +1392,10 @@ describe('node-fetch', () => { const filename = path.join(__dirname, 'dummy.txt'); - form.set('field', "some text"); + form.set('field', 'some text'); form.set('file', fs.createReadStream(filename), { size: await fs.promises.stat(filename) - }) + }); const url = `${base}multipart`; const opts = { @@ -1405,7 +1405,7 @@ describe('node-fetch', () => { return fetch(url, opts).then(res => res.json()).then(res => { expect(res.method).to.equal('POST'); - // expect(res.headers['content-type']).to.equal(`multipart/form-data; boundary=${form.boundary}`); + expect(res.headers['content-type']).to.startWith('multipart/form-data'); expect(res.body).to.contain('field='); expect(res.body).to.contain('file='); }); From 7cd812c0e8fdd603599facaac61b9cebe6c8b49d Mon Sep 17 00:00:00 2001 From: octet-stream Date: Wed, 1 Jan 2020 23:37:24 +0300 Subject: [PATCH 109/157] Add form-data-node file. --- src/utils/form-data-stream.js | 61 +++++++++++++++++++++++++++++++++++ 1 file changed, 61 insertions(+) create mode 100644 src/utils/form-data-stream.js diff --git a/src/utils/form-data-stream.js b/src/utils/form-data-stream.js new file mode 100644 index 000000000..ae00a6d94 --- /dev/null +++ b/src/utils/form-data-stream.js @@ -0,0 +1,61 @@ +import {Readable} from 'stream'; + +import {isBlob} from './is'; + +class FormDataStream extends Readable { + constructor(form, boundary) { + super(); + + this._carriage = '\r\n'; + this._dashes = '-'.repeat(2); + this._boundary = boundary; + this._form = form; + this._curr = this._readField(); + } + + _getHeader(name, field) { + let header = ''; + + header += `${this._dashes}${this._boundary}${this._carriage}`; + header += `Content-Disposition: form-data; name="${name}"`; + + if (isBlob(field)) { + header += `; filename="${field.name}"${this._carriage}`; + header += `Content-Type: ${field.type || 'application/octet-stream'}`; + } + + return `${header}${this._carriage.repeat(2)}`; + } + + async * _readField() { + for (const [name, field] of this._form) { + yield this._getHeader(name, field); + + if (isBlob(field)) { + yield * field.stream(); + } else { + yield field; + } + + yield this._carriage; + } + + yield `${this._dashes}${this._boundary}${this._dashes}${this._carriage.repeat(2)}`; + } + + _read() { + const onFulfilled = ({done, value}) => { + if (done) { + return this.push(null); + } + + this.push(Buffer.isBuffer(value) ? value : Buffer.from(String(value))); + }; + + const onRejected = err => this.emit('error', err); + + this._curr.next().then(onFulfilled).catch(onRejected); + } +} + +export default FormDataStream; From c81fde922d2a244b2ea12d2ebf4f92f9e6b460ef Mon Sep 17 00:00:00 2001 From: octet-stream Date: Thu, 2 Jan 2020 01:47:16 +0300 Subject: [PATCH 110/157] Fix for size option at formdata-node body test. --- test/test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/test.js b/test/test.js index 08b5dac45..2cb9d1df2 100644 --- a/test/test.js +++ b/test/test.js @@ -1394,7 +1394,7 @@ describe('node-fetch', () => { form.set('field', 'some text'); form.set('file', fs.createReadStream(filename), { - size: await fs.promises.stat(filename) + size: await fs.promises.stat(filename).then(({size}) => size) }); const url = `${base}multipart`; From 8401c5143b53d8b0ef1708f48f8fab1804ed38d8 Mon Sep 17 00:00:00 2001 From: octet-stream Date: Fri, 3 Jan 2020 01:33:27 +0300 Subject: [PATCH 111/157] Fix description in formdata-node example. --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 4296c6ed6..4031f338f 100644 --- a/README.md +++ b/README.md @@ -354,7 +354,7 @@ fetch('https://httpbin.org/post', options) .then(json => console.log(json)); ``` -node-fetch also supports [formdata-node](https://github.com/octet-stream/form-data) as an alternative: +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'); From f1757bce6f4f88bf8c34e9a86e12a50aa17737c5 Mon Sep 17 00:00:00 2001 From: Richie Bendall Date: Sat, 4 Jan 2020 15:48:04 +1300 Subject: [PATCH 112/157] docs: Add logo Signed-off-by: Richie Bendall --- .gitignore | 3 + README.md | 118 ++++++++++++++++++------------------ docs/media/Banner.svg | 21 +++++++ docs/media/Logo.svg | 21 +++++++ docs/media/NodeFetch.sketch | Bin 0 -> 33025 bytes 5 files changed, 104 insertions(+), 59 deletions(-) create mode 100644 docs/media/Banner.svg create mode 100644 docs/media/Logo.svg create mode 100644 docs/media/NodeFetch.sketch diff --git a/.gitignore b/.gitignore index 565dda366..a73d7bf4a 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,6 @@ +# Sketch temporary file +~*.sketch + # Generated files dist/ diff --git a/README.md b/README.md index d415bfce8..43c52d01e 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,15 @@ -# node-fetch +
+ Node Fetch +
+

A light-weight module that brings window.fetch to Node.js.

+
-[![npm version][npm-image]][npm-url] [![build status][travis-image]][travis-url] [![coverage status][codecov-image]][codecov-url] [![install size][install-size-image]][install-size-url] [![Discord][discord-image]][discord-url] -A light-weight module that brings `window.fetch` to Node.js +[![NPM](https://nodei.co/npm/node-fetch.png)](https://nodei.co/npm/node-fetch/) **Consider supporting us on our Open Collective:** @@ -16,58 +19,57 @@ A light-weight module that brings `window.fetch` to Node.js -- [node-fetch](#node-fetch) - - [Motivation](#motivation) - - [Features](#features) - - [Difference from client-side fetch](#difference-from-client-side-fetch) - - [Installation](#installation) - - [Loading and configuring the module](#loading-and-configuring-the-module) - - [Upgrading](#upgrading) - - [Common Usage](#common-usage) - - [Plain text or HTML](#plain-text-or-html) - - [JSON](#json) - - [Simple Post](#simple-post) - - [Post with JSON](#post-with-json) - - [Post with form parameters](#post-with-form-parameters) - - [Handling exceptions](#handling-exceptions) - - [Handling client and server errors](#handling-client-and-server-errors) - - [Advanced Usage](#advanced-usage) - - [Streams](#streams) - - [Buffer](#buffer) - - [Accessing Headers and other Meta data](#accessing-headers-and-other-meta-data) - - [Extract Set-Cookie Header](#extract-set-cookie-header) - - [Post data using a file stream](#post-data-using-a-file-stream) - - [Post with form-data (detect multipart)](#post-with-form-data-detect-multipart) - - [Request cancellation with AbortSignal](#request-cancellation-with-abortsignal) - - [API](#api) - - [fetch(url[, options])](#fetchurl-options) - - [Options](#options) - - [Default Headers](#default-headers) - - [Custom Agent](#custom-agent) - - [Custom highWaterMark](#custom-highwatermark) - - [Class: Request](#class-request) - - [new Request(input[, options])](#new-requestinput-options) - - [Class: Response](#class-response) - - [new Response([body[, options]])](#new-responsebody-options) - - [response.ok](#responseok) - - [response.redirected](#responseredirected) - - [Class: Headers](#class-headers) - - [new Headers([init])](#new-headersinit) - - [Interface: Body](#interface-body) - - [body.body](#bodybody) - - [body.bodyUsed](#bodybodyused) - - [body.arrayBuffer()](#bodyarraybuffer) - - [body.blob()](#bodyblob) - - [body.json()](#bodyjson) - - [body.text()](#bodytext) - - [body.buffer()](#bodybuffer) - - [Class: FetchError](#class-fetcherror) - - [Class: AbortError](#class-aborterror) - - [TypeScript](#typescript) - - [Acknowledgement](#acknowledgement) - - [Team](#team) - - [Former](#former) - - [License](#license) +- [Motivation](#motivation) +- [Features](#features) +- [Difference from client-side fetch](#difference-from-client-side-fetch) +- [Installation](#installation) +- [Loading and configuring the module](#loading-and-configuring-the-module) +- [Upgrading](#upgrading) +- [Common Usage](#common-usage) + - [Plain text or HTML](#plain-text-or-html) + - [JSON](#json) + - [Simple Post](#simple-post) + - [Post with JSON](#post-with-json) + - [Post with form parameters](#post-with-form-parameters) + - [Handling exceptions](#handling-exceptions) + - [Handling client and server errors](#handling-client-and-server-errors) +- [Advanced Usage](#advanced-usage) + - [Streams](#streams) + - [Buffer](#buffer) + - [Accessing Headers and other Meta data](#accessing-headers-and-other-meta-data) + - [Extract Set-Cookie Header](#extract-set-cookie-header) + - [Post data using a file stream](#post-data-using-a-file-stream) + - [Post with form-data (detect multipart)](#post-with-form-data-detect-multipart) + - [Request cancellation with AbortSignal](#request-cancellation-with-abortsignal) +- [API](#api) + - [fetch(url[, options])](#fetchurl-options) + - [Options](#options) + - [Default Headers](#default-headers) + - [Custom Agent](#custom-agent) + - [Custom highWaterMark](#custom-highwatermark) + - [Class: Request](#class-request) + - [new Request(input[, options])](#new-requestinput-options) + - [Class: Response](#class-response) + - [new Response([body[, options]])](#new-responsebody-options) + - [response.ok](#responseok) + - [response.redirected](#responseredirected) + - [Class: Headers](#class-headers) + - [new Headers([init])](#new-headersinit) + - [Interface: Body](#interface-body) + - [body.body](#bodybody) + - [body.bodyUsed](#bodybodyused) + - [body.arrayBuffer()](#bodyarraybuffer) + - [body.blob()](#bodyblob) + - [body.json()](#bodyjson) + - [body.text()](#bodytext) + - [body.buffer()](#bodybuffer) + - [Class: FetchError](#class-fetcherror) + - [Class: AbortError](#class-aborterror) +- [TypeScript](#typescript) +- [Acknowledgement](#acknowledgement) +- [Team](#team) + - [Former](#former) +- [License](#license) @@ -707,11 +709,9 @@ Thanks to [github/fetch](https://github.com/github/fetch) for providing a solid MIT -[npm-image]: https://flat.badgen.net/npm/v/node-fetch -[npm-url]: https://www.npmjs.com/package/node-fetch -[travis-image]: https://flat.badgen.net/travis/bitinn/node-fetch +[travis-image]: https://img.shields.io/travis/bitinn/node-fetch/master?style=flat-square [travis-url]: https://travis-ci.org/bitinn/node-fetch -[codecov-image]: https://flat.badgen.net/codecov/c/github/bitinn/node-fetch/master +[codecov-image]: https://img.shields.io/codecov/c/gh/bitinn/node-fetch/master?style=flat-square [codecov-url]: https://codecov.io/gh/bitinn/node-fetch [opencollective-image]: https://opencollective.com/node-fetch/donate/button.png?color=blue [opencollective-url]: https://opencollective.com/node-fetch diff --git a/docs/media/Banner.svg b/docs/media/Banner.svg new file mode 100644 index 000000000..b9c079783 --- /dev/null +++ b/docs/media/Banner.svg @@ -0,0 +1,21 @@ + + + Created with Lunacy + + + + + + + + \ No newline at end of file diff --git a/docs/media/Logo.svg b/docs/media/Logo.svg new file mode 100644 index 000000000..8d1a2c9e8 --- /dev/null +++ b/docs/media/Logo.svg @@ -0,0 +1,21 @@ + + + Created with Lunacy + + + + + + + + \ No newline at end of file diff --git a/docs/media/NodeFetch.sketch b/docs/media/NodeFetch.sketch new file mode 100644 index 0000000000000000000000000000000000000000..ad858e7bff071e6514c10dd72a9bb4cc25eaffbe GIT binary patch literal 33025 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 AUH||9 literal 0 HcmV?d00001 From 2e8c4aa8c25bb2593dac2e17e0899c6d494884c8 Mon Sep 17 00:00:00 2001 From: Richie Bendall Date: Sun, 5 Jan 2020 17:58:17 +1300 Subject: [PATCH 113/157] chore: Update repository name from bitinn/node-fetch to node-fetch/node-fetch. Signed-off-by: Richie Bendall --- README.md | 14 +++++++------- docs/CHANGELOG.md | 4 ++-- docs/ERROR-HANDLING.md | 6 +++--- docs/v2-LIMITS.md | 2 +- docs/v3-LIMITS.md | 4 ++-- docs/v3-UPGRADE-GUIDE.md | 6 +++++- index.d.ts | 2 +- package.json | 6 +++--- src/request.js | 4 ++-- src/utils/is.js | 2 +- 10 files changed, 27 insertions(+), 23 deletions(-) diff --git a/README.md b/README.md index 43c52d01e..057df845e 100644 --- a/README.md +++ b/README.md @@ -388,7 +388,7 @@ fetch('https://example.com', {signal: controller.signal}) }); ``` -See [test cases](https://github.com/bitinn/node-fetch/blob/master/test/test.js) for more examples. +See [test cases](https://github.com/node-fetch/node-fetch/blob/master/test/test.js) for more examples. ## API @@ -438,7 +438,7 @@ If no values are set, the following request headers will be sent automatically: | `Connection` | `close` _(when no `options.agent` is present)_ | | `Content-Length` | _(automatically calculated, if possible)_ | | `Transfer-Encoding` | `chunked` _(when `req.body` is a stream)_ | -| `User-Agent` | `node-fetch/1.0 (+https://github.com/bitinn/node-fetch)` | +| `User-Agent` | `node-fetch (+https://github.com/node-fetch/node-fetch)` | Note: when `body` is a `Stream`, `Content-Length` is not set automatically. @@ -709,10 +709,10 @@ Thanks to [github/fetch](https://github.com/github/fetch) for providing a solid MIT -[travis-image]: https://img.shields.io/travis/bitinn/node-fetch/master?style=flat-square -[travis-url]: https://travis-ci.org/bitinn/node-fetch -[codecov-image]: https://img.shields.io/codecov/c/gh/bitinn/node-fetch/master?style=flat-square -[codecov-url]: https://codecov.io/gh/bitinn/node-fetch +[travis-image]: https://img.shields.io/travis/node-fetch/node-fetch/master?style=flat-square +[travis-url]: https://travis-ci.org/node-fetch/node-fetch +[codecov-image]: https://img.shields.io/codecov/c/gh/node-fetch/node-fetch/master?style=flat-square +[codecov-url]: https://codecov.io/gh/node-fetch/node-fetch [opencollective-image]: https://opencollective.com/node-fetch/donate/button.png?color=blue [opencollective-url]: https://opencollective.com/node-fetch [install-size-image]: https://flat.badgen.net/packagephobia/install/node-fetch @@ -723,4 +723,4 @@ MIT [response-init]: https://fetch.spec.whatwg.org/#responseinit [node-readable]: https://nodejs.org/api/stream.html#stream_readable_streams [mdn-headers]: https://developer.mozilla.org/en-US/docs/Web/API/Headers -[error-handling.md]: https://github.com/bitinn/node-fetch/blob/master/docs/ERROR-HANDLING.md +[error-handling.md]: https://github.com/node-fetch/node-fetch/blob/master/docs/ERROR-HANDLING.md diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index ebfd100c2..fb7a91d4a 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -99,7 +99,7 @@ Fix packaging errors in v2.1.0. ## v2.0.0 -This is a major release. Check [our upgrade guide](https://github.com/bitinn/node-fetch/blob/master/UPGRADE-GUIDE.md) for an overview on some key differences between v1 and v2. +This is a major release. Check [our upgrade guide](https://github.com/node-fetch/node-fetch/blob/master/UPGRADE-GUIDE.md) for an overview on some key differences between v1 and v2. ### General changes @@ -151,7 +151,7 @@ This is a major release. Check [our upgrade guide](https://github.com/bitinn/nod ## Backport releases (v1.7.0 and beyond) -See [changelog on 1.x branch](https://github.com/bitinn/node-fetch/blob/1.x/CHANGELOG.md) for details. +See [changelog on 1.x branch](https://github.com/node-fetch/node-fetch/blob/1.x/CHANGELOG.md) for details. ## v1.6.3 diff --git a/docs/ERROR-HANDLING.md b/docs/ERROR-HANDLING.md index fcbc5e6a5..bda35d169 100644 --- a/docs/ERROR-HANDLING.md +++ b/docs/ERROR-HANDLING.md @@ -6,7 +6,7 @@ Because `window.fetch` isn't designed to be transparent about the cause of reque The basics: -- A cancelled request is rejected with an [`AbortError`](https://github.com/bitinn/node-fetch/blob/master/README.md#class-aborterror). You can check if the reason for rejection was that the request was aborted by checking the `Error`'s `name` is `AbortError`. +- A cancelled request is rejected with an [`AbortError`](https://github.com/node-fetch/node-fetch/blob/master/README.md#class-aborterror). You can check if the reason for rejection was that the request was aborted by checking the `Error`'s `name` is `AbortError`. ```js const fetch = required('node-fetch'); @@ -18,7 +18,7 @@ fetch(url, {signal}).catch(error => { }); ``` -- All [operational errors][joyent-guide] *other than aborted requests* are rejected with a [FetchError](https://github.com/bitinn/node-fetch/blob/master/README.md#class-fetcherror). You can handle them all through the promise `catch` clause. +- All [operational errors][joyent-guide] *other than aborted requests* are rejected with a [FetchError](https://github.com/node-fetch/node-fetch/blob/master/README.md#class-fetcherror). You can handle them all through the promise `catch` clause. - All errors come with an `err.message` detailing the cause of errors. @@ -30,6 +30,6 @@ fetch(url, {signal}).catch(error => { List of error types: -- Because we maintain 100% coverage, see [test.js](https://github.com/bitinn/node-fetch/blob/master/test/test.js) for a full list of custom `FetchError` types, as well as some of the common errors from Node.js +- Because we maintain 100% coverage, see [test.js](https://github.com/node-fetch/node-fetch/blob/master/test/test.js) for a full list of custom `FetchError` types, as well as some of the common errors from Node.js [joyent-guide]: https://www.joyent.com/node-js/production/design/errors#operational-errors-vs-programmer-errors diff --git a/docs/v2-LIMITS.md b/docs/v2-LIMITS.md index d0f12e493..849a15533 100644 --- a/docs/v2-LIMITS.md +++ b/docs/v2-LIMITS.md @@ -29,4 +29,4 @@ Known differences - Because Node.js stream doesn't expose a [*disturbed*](https://fetch.spec.whatwg.org/#concept-readablestream-disturbed) property like Stream spec, using a consumed stream for `new Response(body)` will not set `bodyUsed` flag correctly. [readable-stream]: https://nodejs.org/api/stream.html#stream_readable_streams -[ERROR-HANDLING.md]: https://github.com/bitinn/node-fetch/blob/master/docs/ERROR-HANDLING.md +[ERROR-HANDLING.md]: https://github.com/node-fetch/node-fetch/blob/master/docs/ERROR-HANDLING.md diff --git a/docs/v3-LIMITS.md b/docs/v3-LIMITS.md index 459a3506b..0913c5643 100644 --- a/docs/v3-LIMITS.md +++ b/docs/v3-LIMITS.md @@ -27,5 +27,5 @@ Known differences - Because Node.js stream doesn't expose a [*disturbed*](https://fetch.spec.whatwg.org/#concept-readablestream-disturbed) property like Stream spec, using a consumed stream for `new Response(body)` will not set `bodyUsed` flag correctly. [readable-stream]: https://nodejs.org/api/stream.html#stream_readable_streams -[ERROR-HANDLING.md]: https://github.com/bitinn/node-fetch/blob/master/docs/ERROR-HANDLING.md -[highwatermark-fix]: https://github.com/bitinn/node-fetch/blob/master/README.md#custom-highwatermark +[ERROR-HANDLING.md]: https://github.com/node-fetch/node-fetch/blob/master/docs/ERROR-HANDLING.md +[highwatermark-fix]: https://github.com/node-fetch/node-fetch/blob/master/README.md#custom-highwatermark diff --git a/docs/v3-UPGRADE-GUIDE.md b/docs/v3-UPGRADE-GUIDE.md index 16e0aae6f..ce3b1cbc0 100644 --- a/docs/v3-UPGRADE-GUIDE.md +++ b/docs/v3-UPGRADE-GUIDE.md @@ -57,7 +57,11 @@ fetch("https://somewebsitereturninginvalidjson.com").then(res => res.json()) ## A stream pipeline is now used to forward errors -If you are listening for errors via `res.body.on('error', () => ...)`, replace it with `res.body.once('error', () => ...)` so that your callback is not [fired twice](https://github.com/bitinn/node-fetch/issues/668#issuecomment-569386115) in NodeJS >=13.5. +If you are listening for errors via `res.body.on('error', () => ...)`, replace it with `res.body.once('error', () => ...)` so that your callback is not [fired twice](https://github.com/node-fetch/node-fetch/issues/668#issuecomment-569386115) in NodeJS >=13.5. + +## Changed default user agent + +The default user agent has been changed from `node-fetch/1.0 (+https://github.com/node-fetch/node-fetch)` to `node-fetch (+https://github.com/node-fetch/node-fetch)`. # Enhancements diff --git a/index.d.ts b/index.d.ts index cabeb6004..4b3482538 100644 --- a/index.d.ts +++ b/index.d.ts @@ -1,5 +1,5 @@ // Type definitions for node-fetch 2.5 -// Project: https://github.com/bitinn/node-fetch +// Project: https://github.com/node-fetch/node-fetch // Definitions by: Torsten Werner // Niklas Lindgren // Vinay Bedre diff --git a/package.json b/package.json index beef5cb6c..7722d4c6e 100644 --- a/package.json +++ b/package.json @@ -23,7 +23,7 @@ }, "repository": { "type": "git", - "url": "https://github.com/bitinn/node-fetch.git" + "url": "https://github.com/node-fetch/node-fetch.git" }, "keywords": [ "fetch", @@ -33,9 +33,9 @@ "author": "David Frank", "license": "MIT", "bugs": { - "url": "https://github.com/bitinn/node-fetch/issues" + "url": "https://github.com/node-fetch/node-fetch/issues" }, - "homepage": "https://github.com/bitinn/node-fetch", + "homepage": "https://github.com/node-fetch/node-fetch", "funding": { "type": "opencollective", "url": "https://opencollective.com/node-fetch" diff --git a/src/request.js b/src/request.js index 4a9a7e753..be6e054cf 100644 --- a/src/request.js +++ b/src/request.js @@ -42,7 +42,7 @@ export default class Request { constructor(input, init = {}) { let parsedURL; - // 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)) { if (input && input.href) { // In order to support Node.js' Url objects; though WHATWG's URL objects @@ -218,7 +218,7 @@ export function getNodeRequestOptions(request) { // HTTP-network-or-cache fetch step 2.11 if (!headers.has('User-Agent')) { - headers.set('User-Agent', 'node-fetch/1.0 (+https://github.com/bitinn/node-fetch)'); + headers.set('User-Agent', 'node-fetch (+https://github.com/node-fetch/node-fetch)'); } // HTTP-network-or-cache fetch step 2.15 diff --git a/src/utils/is.js b/src/utils/is.js index ce745bd30..dd5f6d79d 100644 --- a/src/utils/is.js +++ b/src/utils/is.js @@ -8,7 +8,7 @@ const NAME = Symbol.toStringTag; /** * Check if `obj` is a URLSearchParams object - * ref: https://github.com/bitinn/node-fetch/issues/296#issuecomment-307598143 + * ref: https://github.com/node-fetch/node-fetch/issues/296#issuecomment-307598143 * * @param {*} obj * @return {boolean} From 6b70e5befbd4ba9d188e6de7b613e279536e245f Mon Sep 17 00:00:00 2001 From: Richie Bendall Date: Sun, 5 Jan 2020 18:02:30 +1300 Subject: [PATCH 114/157] chore: Fix unit tests Signed-off-by: Richie Bendall --- test/test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/test.js b/test/test.js index 7a4722af3..983e5fda5 100644 --- a/test/test.js +++ b/test/test.js @@ -1095,7 +1095,7 @@ describe('node-fetch', () => { it('should set default User-Agent', () => { const url = `${base}inspect`; return fetch(url).then(res => res.json()).then(res => { - expect(res.headers['user-agent']).to.startWith('node-fetch/'); + expect(res.headers['user-agent']).to.startWith('node-fetch'); }); }); From 2f29e45847a21c5d3aefe8fbc7e715150c2a4ace Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Sun, 5 Jan 2020 18:14:54 +1300 Subject: [PATCH 115/157] chore(deps): Bump @pika/plugin-copy-assets from 0.7.1 to 0.8.1 (#713) Bumps [@pika/plugin-copy-assets](https://github.com/pikapkg/builders) from 0.7.1 to 0.8.1. - [Release notes](https://github.com/pikapkg/builders/releases) - [Commits](https://github.com/pikapkg/builders/compare/v0.7.1...v0.8.1) Signed-off-by: dependabot-preview[bot] --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 7722d4c6e..3e7070edc 100644 --- a/package.json +++ b/package.json @@ -47,7 +47,7 @@ "@pika/pack": "^0.5.0", "@pika/plugin-build-node": "^0.7.1", "@pika/plugin-build-types": "^0.7.1", - "@pika/plugin-copy-assets": "^0.7.1", + "@pika/plugin-copy-assets": "^0.8.1", "@pika/plugin-standard-pkg": "^0.7.1", "abort-controller": "^3.0.0", "abortcontroller-polyfill": "^1.3.0", From 0361a7c0b4ee73058c458f9ae3d0d92074e7fc77 Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Sun, 5 Jan 2020 18:20:31 +1300 Subject: [PATCH 116/157] chore(deps): Bump @pika/plugin-build-types from 0.7.1 to 0.8.1 (#710) Bumps [@pika/plugin-build-types](https://github.com/pikapkg/builders) from 0.7.1 to 0.8.1. - [Release notes](https://github.com/pikapkg/builders/releases) - [Commits](https://github.com/pikapkg/builders/compare/v0.7.1...v0.8.1) Signed-off-by: dependabot-preview[bot] --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 3e7070edc..8a69538cc 100644 --- a/package.json +++ b/package.json @@ -46,7 +46,7 @@ "@babel/register": "^7.6.2", "@pika/pack": "^0.5.0", "@pika/plugin-build-node": "^0.7.1", - "@pika/plugin-build-types": "^0.7.1", + "@pika/plugin-build-types": "^0.8.1", "@pika/plugin-copy-assets": "^0.8.1", "@pika/plugin-standard-pkg": "^0.7.1", "abort-controller": "^3.0.0", From 0a3fc4e9b88632103bb29dc91f29ff3874bfe239 Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Sun, 5 Jan 2020 18:23:50 +1300 Subject: [PATCH 117/157] Bump nyc from 14.1.1 to 15.0.0 (#714) Bumps [nyc](https://github.com/istanbuljs/nyc) from 14.1.1 to 15.0.0. - [Release notes](https://github.com/istanbuljs/nyc/releases) - [Changelog](https://github.com/istanbuljs/nyc/blob/master/CHANGELOG.md) - [Commits](https://github.com/istanbuljs/nyc/compare/v14.1.1...v15.0.0) Signed-off-by: dependabot-preview[bot] --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 8a69538cc..3bc3e1e0a 100644 --- a/package.json +++ b/package.json @@ -59,7 +59,7 @@ "cross-env": "^6.0.3", "form-data": "^2.5.1", "mocha": "^6.2.2", - "nyc": "^14.1.1", + "nyc": "^15.0.0", "parted": "^0.1.1", "promise": "^8.0.3", "resumer": "0.0.0", From d65959f6bb88c7bedb87ed877db4c9c51171bb8d Mon Sep 17 00:00:00 2001 From: Richie Bendall Date: Sun, 5 Jan 2020 18:29:16 +1300 Subject: [PATCH 118/157] chore: Update travis ci url Signed-off-by: Richie Bendall --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 057df845e..387fa7105 100644 --- a/README.md +++ b/README.md @@ -709,8 +709,8 @@ Thanks to [github/fetch](https://github.com/github/fetch) for providing a solid MIT -[travis-image]: https://img.shields.io/travis/node-fetch/node-fetch/master?style=flat-square -[travis-url]: https://travis-ci.org/node-fetch/node-fetch +[travis-image]: https://img.shields.io/travis/com/node-fetch/node-fetch/master?style=flat-square +[travis-url]: https://travis-ci.com/node-fetch/node-fetch [codecov-image]: https://img.shields.io/codecov/c/gh/node-fetch/node-fetch/master?style=flat-square [codecov-url]: https://codecov.io/gh/node-fetch/node-fetch [opencollective-image]: https://opencollective.com/node-fetch/donate/button.png?color=blue From e4dbe2d05c2e567e25aa7e6bd8b88f694faa7bf3 Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Sun, 5 Jan 2020 18:44:28 +1300 Subject: [PATCH 119/157] chore(deps): Bump mocha from 6.2.2 to 7.0.0 (#711) Bumps [mocha](https://github.com/mochajs/mocha) from 6.2.2 to 7.0.0. - [Release notes](https://github.com/mochajs/mocha/releases) - [Changelog](https://github.com/mochajs/mocha/blob/master/CHANGELOG.md) - [Commits](https://github.com/mochajs/mocha/compare/v6.2.2...v7.0.0) Signed-off-by: dependabot-preview[bot] --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 3bc3e1e0a..145cce13b 100644 --- a/package.json +++ b/package.json @@ -58,7 +58,7 @@ "codecov": "^3.6.1", "cross-env": "^6.0.3", "form-data": "^2.5.1", - "mocha": "^6.2.2", + "mocha": "^7.0.0", "nyc": "^15.0.0", "parted": "^0.1.1", "promise": "^8.0.3", From 2de32e5e3512a7de856616bdf2ba9a9dab57bdfc Mon Sep 17 00:00:00 2001 From: Richie Bendall Date: Tue, 7 Jan 2020 15:00:32 +1300 Subject: [PATCH 120/157] =?UTF-8?q?feat:=20Allow=20excluding=20a=20user=20?= =?UTF-8?q?agent=20in=20a=20fetch=20request=20by=20setting=E2=80=A6=20(#71?= =?UTF-8?q?5)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Richie Bendall --- docs/CHANGELOG.md | 1 + src/request.js | 4 +++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index fb7a91d4a..5effb95ad 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -15,6 +15,7 @@ Changelog - Enhance: drop existing blob implementation code and use fetch-blob as dependency instead. - Enhance: modernise the code behind `FetchError` and `AbortError`. - Enhance: replace deprecated `url.parse()` and `url.replace()` with the new WHATWG `new URL()` +- Enhance: allow excluding a `user-agent` in a fetch request by setting it's header to null. - Fix: `Response.statusText` no longer sets a default message derived from the HTTP status code. - Fix: missing response stream error events. - Fix: do not use constructor.name to check object. diff --git a/src/request.js b/src/request.js index be6e054cf..5de69f676 100644 --- a/src/request.js +++ b/src/request.js @@ -217,7 +217,9 @@ export function getNodeRequestOptions(request) { } // HTTP-network-or-cache fetch step 2.11 - if (!headers.has('User-Agent')) { + if (headers.get('User-Agent') === 'null') { + headers.delete('User-Agent'); + } else if (!headers.has('User-Agent') && headers.get('User-Agent') !== 'null') { headers.set('User-Agent', 'node-fetch (+https://github.com/node-fetch/node-fetch)'); } From abacff568057be696661b702d10997e96aab1ff1 Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Tue, 7 Jan 2020 15:00:45 +1300 Subject: [PATCH 121/157] Bump @pika/plugin-build-node from 0.7.1 to 0.8.1 (#717) Bumps [@pika/plugin-build-node](https://github.com/pikapkg/builders) from 0.7.1 to 0.8.1. - [Release notes](https://github.com/pikapkg/builders/releases) - [Commits](https://github.com/pikapkg/builders/compare/v0.7.1...v0.8.1) Signed-off-by: dependabot-preview[bot] --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 145cce13b..f263c3fb4 100644 --- a/package.json +++ b/package.json @@ -45,7 +45,7 @@ "@babel/preset-env": "^7.6.3", "@babel/register": "^7.6.2", "@pika/pack": "^0.5.0", - "@pika/plugin-build-node": "^0.7.1", + "@pika/plugin-build-node": "^0.8.1", "@pika/plugin-build-types": "^0.8.1", "@pika/plugin-copy-assets": "^0.8.1", "@pika/plugin-standard-pkg": "^0.7.1", From 658e5e0d43cbeea14fc5d975b899b3fceba96cd5 Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Tue, 7 Jan 2020 15:00:57 +1300 Subject: [PATCH 122/157] Bump @pika/plugin-standard-pkg from 0.7.1 to 0.8.1 (#716) Bumps [@pika/plugin-standard-pkg](https://github.com/pikapkg/builders) from 0.7.1 to 0.8.1. - [Release notes](https://github.com/pikapkg/builders/releases) - [Commits](https://github.com/pikapkg/builders/compare/v0.7.1...v0.8.1) Signed-off-by: dependabot-preview[bot] --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index f263c3fb4..cbea4477f 100644 --- a/package.json +++ b/package.json @@ -48,7 +48,7 @@ "@pika/plugin-build-node": "^0.8.1", "@pika/plugin-build-types": "^0.8.1", "@pika/plugin-copy-assets": "^0.8.1", - "@pika/plugin-standard-pkg": "^0.7.1", + "@pika/plugin-standard-pkg": "^0.8.1", "abort-controller": "^3.0.0", "abortcontroller-polyfill": "^1.3.0", "chai": "^4.2.0", From d9a095136376c7e5cc02082cd287b58dcecd947f Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Tue, 7 Jan 2020 15:04:35 +1300 Subject: [PATCH 123/157] Bump form-data from 2.5.1 to 3.0.0 (#712) Bumps [form-data](https://github.com/form-data/form-data) from 2.5.1 to 3.0.0. - [Release notes](https://github.com/form-data/form-data/releases) - [Commits](https://github.com/form-data/form-data/compare/v2.5.1...v3.0.0) Signed-off-by: dependabot-preview[bot] --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index cbea4477f..74ffb9bc6 100644 --- a/package.json +++ b/package.json @@ -57,7 +57,7 @@ "chai-string": "^1.5.0", "codecov": "^3.6.1", "cross-env": "^6.0.3", - "form-data": "^2.5.1", + "form-data": "^3.0.0", "mocha": "^7.0.0", "nyc": "^15.0.0", "parted": "^0.1.1", From 0c60d247ef38ed645ec71285365b55a994c5328e Mon Sep 17 00:00:00 2001 From: Richie Bendall Date: Sun, 24 May 2020 01:03:10 +1200 Subject: [PATCH 124/157] Delete externals.d.ts --- externals.d.ts | 21 --------------------- 1 file changed, 21 deletions(-) delete mode 100644 externals.d.ts diff --git a/externals.d.ts b/externals.d.ts deleted file mode 100644 index fb9466bf3..000000000 --- a/externals.d.ts +++ /dev/null @@ -1,21 +0,0 @@ -// `AbortSignal` is defined here to prevent a dependency on a particular -// implementation like the `abort-controller` package, and to avoid requiring -// the `dom` library in `tsconfig.json`. - -export interface AbortSignal { - aborted: boolean; - - addEventListener: (type: "abort", listener: ((this: AbortSignal, event: any) => any), options?: boolean | { - capture?: boolean, - once?: boolean, - passive?: boolean - }) => void; - - removeEventListener: (type: "abort", listener: ((this: AbortSignal, event: any) => any), options?: boolean | { - capture?: boolean - }) => void; - - dispatchEvent: (event: any) => boolean; - - onabort?: null | ((this: AbortSignal, event: any) => void); -} \ No newline at end of file From c220faf03c67ddaf00a8eebb04831647309a6841 Mon Sep 17 00:00:00 2001 From: Richie Bendall Date: Sun, 24 May 2020 01:03:23 +1200 Subject: [PATCH 125/157] Delete .travis.yml --- .travis.yml | 16 ---------------- 1 file changed, 16 deletions(-) delete mode 100644 .travis.yml diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index a47d5ab9b..000000000 --- a/.travis.yml +++ /dev/null @@ -1,16 +0,0 @@ -language: node_js - -node_js: - - "lts/*" # 10 (Latest LTS) - - "node" # 12 (Latest Stable) - -matrix: - include: - - # Linting stage - node_js: "lts/*" # Latest LTS - script: npm run lint - -cache: npm - -script: - - npm run coverage From e8c2f7d93e9e96680db4cf104941f817ff6d1ae4 Mon Sep 17 00:00:00 2001 From: Richie Bendall Date: Sun, 24 May 2020 01:04:14 +1200 Subject: [PATCH 126/157] Delete chai-timeout.js --- test/chai-timeout.js | 18 ------------------ 1 file changed, 18 deletions(-) delete mode 100644 test/chai-timeout.js diff --git a/test/chai-timeout.js b/test/chai-timeout.js deleted file mode 100644 index 6fed2cfa4..000000000 --- a/test/chai-timeout.js +++ /dev/null @@ -1,18 +0,0 @@ -export default ({Assertion}, utils) => { - utils.addProperty(Assertion.prototype, 'timeout', function () { - return new Promise(resolve => { - const timer = setTimeout(() => resolve(true), 150); - this._obj.then(() => { - clearTimeout(timer); - resolve(false); - }); - }).then(timeouted => { - this.assert( - timeouted, - 'expected promise to timeout but it was resolved', - 'expected promise not to timeout but it timed out' - ); - }); - }); -}; - From ff877de145bb099e5131125af51ece482f1c02b1 Mon Sep 17 00:00:00 2001 From: Nick K Date: Sat, 23 May 2020 17:03:41 +0300 Subject: [PATCH 127/157] Fix path.join usage in form-data tests Co-authored-by: Konstantin Vyatkin --- test/main.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/main.js b/test/main.js index 3175433a2..d4b34ecdb 100644 --- a/test/main.js +++ b/test/main.js @@ -1435,7 +1435,7 @@ describe('node-fetch', () => { it('should support formdata-node as POST body', async () => { const form = new FormDataNode(); - const filename = path.join('test/utils/dummy.txt'); + const filename = path.join('test', 'utils', 'dummy.txt'); form.set('field', 'some text'); form.set('file', fs.createReadStream(filename), { From 12228bb7bacc7328eb964c1f7856f47bed4aacfc Mon Sep 17 00:00:00 2001 From: octet-stream Date: Sat, 23 May 2020 17:41:36 +0300 Subject: [PATCH 128/157] Fix lint problem. --- test/main.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/test/main.js b/test/main.js index d4b34ecdb..29d1ba84e 100644 --- a/test/main.js +++ b/test/main.js @@ -2,6 +2,7 @@ import zlib from 'zlib'; import crypto from 'crypto'; import {spawn} from 'child_process'; +import util from 'util'; import http from 'http'; import fs from 'fs'; import stream from 'stream'; @@ -52,6 +53,9 @@ chai.use(chaiString); chai.use(chaiTimeout); const {expect} = chai; +// * Promisify fs.stat because of fs.promises is currently restricted +const stat = util.promisify(fs.stat) + const local = new TestServer(); const base = `http://${local.hostname}:${local.port}/`; @@ -1439,7 +1443,7 @@ describe('node-fetch', () => { form.set('field', 'some text'); form.set('file', fs.createReadStream(filename), { - size: await fs.promises.stat(filename).then(({size}) => size) + size: await stat(filename).then(({size}) => size) }); const url = `${base}multipart`; From b1fb555078e455e75e4c54507e65368e48434595 Mon Sep 17 00:00:00 2001 From: octet-stream Date: Sat, 23 May 2020 17:45:38 +0300 Subject: [PATCH 129/157] Add missing semicolon --- test/main.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/main.js b/test/main.js index 29d1ba84e..ae19867ea 100644 --- a/test/main.js +++ b/test/main.js @@ -54,7 +54,7 @@ chai.use(chaiTimeout); const {expect} = chai; // * Promisify fs.stat because of fs.promises is currently restricted -const stat = util.promisify(fs.stat) +const stat = util.promisify(fs.stat); const local = new TestServer(); const base = `http://${local.hostname}:${local.port}/`; From dbdb326183ef9effb1e4529e970dd0fb01586f83 Mon Sep 17 00:00:00 2001 From: octet-stream Date: Sat, 23 May 2020 17:49:48 +0300 Subject: [PATCH 130/157] Fix lint problems. --- src/utils/is.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/utils/is.js b/src/utils/is.js index 6f4f3f3b8..4e05d5a4c 100644 --- a/src/utils/is.js +++ b/src/utils/is.js @@ -63,8 +63,8 @@ export function isFormData(object) { typeof object.entries === 'function' && typeof object.constructor === 'function' && object.constructor.name === 'FormData' - ) -}; + ); +} /** * Check if `obj` is an instance of AbortSignal. From c373e15751f3c2b27fa6709741c07c9402a7ce25 Mon Sep 17 00:00:00 2001 From: octet-stream Date: Tue, 26 May 2020 00:36:00 +0300 Subject: [PATCH 131/157] Use sync version of fs.stat for tests --- test/main.js | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/test/main.js b/test/main.js index ae19867ea..2c89bb3a1 100644 --- a/test/main.js +++ b/test/main.js @@ -53,9 +53,6 @@ chai.use(chaiString); chai.use(chaiTimeout); const {expect} = chai; -// * Promisify fs.stat because of fs.promises is currently restricted -const stat = util.promisify(fs.stat); - const local = new TestServer(); const base = `http://${local.hostname}:${local.port}/`; @@ -1436,14 +1433,14 @@ describe('node-fetch', () => { }); }); - it('should support formdata-node as POST body', async () => { + it('should support formdata-node as POST body', () => { const form = new FormDataNode(); const filename = path.join('test', 'utils', 'dummy.txt'); form.set('field', 'some text'); form.set('file', fs.createReadStream(filename), { - size: await stat(filename).then(({size}) => size) + size: fs.statSync(filename).size }); const url = `${base}multipart`; From 56909afdbfd92d35decd94700920578b0ac6c851 Mon Sep 17 00:00:00 2001 From: octet-stream Date: Tue, 26 May 2020 01:20:48 +0300 Subject: [PATCH 132/157] Rewrite boundary helper. --- src/utils/boundary.js | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/src/utils/boundary.js b/src/utils/boundary.js index 8977f649a..32b50e9ca 100644 --- a/src/utils/boundary.js +++ b/src/utils/boundary.js @@ -1,13 +1,6 @@ -// ? I don't know if it necessary here, but thios module might cause errors in CRA -// ? See: https://github.com/ai/nanoid/issues/205 -// ? If anything happens, we could replace it with something else -import {customAlphabet} from 'nanoid'; - -const alpha = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'; - /** * @api private */ -const boundary = customAlphabet(alpha, 22); +const boundary = () => Math.random().toString(32).slice(2); export default boundary; From bbe17c04152308a4d4a5064e8495910b3d0ee464 Mon Sep 17 00:00:00 2001 From: octet-stream Date: Tue, 26 May 2020 02:16:38 +0300 Subject: [PATCH 133/157] Replace FormDataStream with formDataIterator. --- src/body.js | 5 +-- src/utils/form-data-iterator.js | 49 ++++++++++++++++++++++++++ src/utils/form-data-stream.js | 61 --------------------------------- test/main.js | 4 +++ 4 files changed, 56 insertions(+), 63 deletions(-) create mode 100644 src/utils/form-data-iterator.js delete mode 100644 src/utils/form-data-stream.js diff --git a/src/body.js b/src/body.js index 2acce856d..d6a359a2f 100644 --- a/src/body.js +++ b/src/body.js @@ -12,7 +12,7 @@ import Blob from 'fetch-blob'; import getBoundary from './utils/boundary.js'; import FetchError from './errors/fetch-error.js'; -import FormDataStream from './utils/form-data-stream.js'; +import formDataIterator from './utils/form-data-iterator.js' import {isBlob, isURLSearchParams, isAbortError, isFormData} from './utils/is.js'; const INTERNALS = Symbol('Body internals'); @@ -53,7 +53,8 @@ export default function Body(body, { } else if (isFormData(body)) { // Body is an instance of formdata-node boundary = `NodeFetchFormDataBoundary${getBoundary()}`; - body = new FormDataStream(body, boundary); + // body = new FormDataStream(body, boundary); + body = Stream.Readable.from(formDataIterator(body, boundary)); } else { // None of the above // coerce to string then buffer diff --git a/src/utils/form-data-iterator.js b/src/utils/form-data-iterator.js new file mode 100644 index 000000000..36f2043f0 --- /dev/null +++ b/src/utils/form-data-iterator.js @@ -0,0 +1,49 @@ +import {Readable} from 'stream'; + +import {isBlob} from './is.js'; + +const carriage = '\r\n'; +const dashes = '-'.repeat(2); + +/** + * @param {string} boundary + * @param {string} name + * @param {*} field + * + * @return {string} + */ +function getHeader(boundary, name, field) { + let header = ''; + + header += `${dashes}${boundary}${carriage}`; + header += `Content-Disposition: form-data; name="${name}"`; + + if (isBlob(field)) { + header += `; filename="${field.name}"${carriage}`; + header += `Content-Type: ${field.type || 'application/octet-stream'}`; + } + + return `${header}${carriage.repeat(2)}`; +} + +/** + * @param {FormData} form + * @param {string} boundary + */ +async function* formDataIterator(form, boundary) { + for (const [name, value] of form) { + yield getHeader(boundary, name, value); + + if (isBlob(value)) { + yield * value.stream(); + } else { + yield value; + } + + yield carriage; + } + + yield `${dashes}${boundary}${dashes}${carriage.repeat(2)}`; +} + +export default formDataIterator; diff --git a/src/utils/form-data-stream.js b/src/utils/form-data-stream.js deleted file mode 100644 index 61cdfd3c6..000000000 --- a/src/utils/form-data-stream.js +++ /dev/null @@ -1,61 +0,0 @@ -import {Readable} from 'stream'; - -import {isBlob} from './is.js'; - -class FormDataStream extends Readable { - constructor(form, boundary) { - super(); - - this._carriage = '\r\n'; - this._dashes = '-'.repeat(2); - this._boundary = boundary; - this._form = form; - this._curr = this._readField(); - } - - _getHeader(name, field) { - let header = ''; - - header += `${this._dashes}${this._boundary}${this._carriage}`; - header += `Content-Disposition: form-data; name="${name}"`; - - if (isBlob(field)) { - header += `; filename="${field.name}"${this._carriage}`; - header += `Content-Type: ${field.type || 'application/octet-stream'}`; - } - - return `${header}${this._carriage.repeat(2)}`; - } - - async * _readField() { - for (const [name, field] of this._form) { - yield this._getHeader(name, field); - - if (isBlob(field)) { - yield * field.stream(); - } else { - yield field; - } - - yield this._carriage; - } - - yield `${this._dashes}${this._boundary}${this._dashes}${this._carriage.repeat(2)}`; - } - - _read() { - const onFulfilled = ({done, value}) => { - if (done) { - return this.push(null); - } - - this.push(Buffer.isBuffer(value) ? value : Buffer.from(String(value))); - }; - - const onRejected = err => this.emit('error', err); - - this._curr.next().then(onFulfilled).catch(onRejected); - } -} - -export default FormDataStream; diff --git a/test/main.js b/test/main.js index 2c89bb3a1..5504d42fc 100644 --- a/test/main.js +++ b/test/main.js @@ -1443,6 +1443,10 @@ describe('node-fetch', () => { size: fs.statSync(filename).size }); + // for await (const ch of form) { + // console.log(String(ch)) + // } + const url = `${base}multipart`; const options = { method: 'POST', From 49f6f7df0f48a01bef58f67610aacf0120d4177c Mon Sep 17 00:00:00 2001 From: octet-stream Date: Tue, 26 May 2020 02:17:20 +0300 Subject: [PATCH 134/157] Remove unnecessary FormDataStream usage. --- src/body.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/body.js b/src/body.js index d6a359a2f..482126a76 100644 --- a/src/body.js +++ b/src/body.js @@ -53,7 +53,6 @@ export default function Body(body, { } else if (isFormData(body)) { // Body is an instance of formdata-node boundary = `NodeFetchFormDataBoundary${getBoundary()}`; - // body = new FormDataStream(body, boundary); body = Stream.Readable.from(formDataIterator(body, boundary)); } else { // None of the above From 400e75f58d59215dbc4b598375bab8167eacc26d Mon Sep 17 00:00:00 2001 From: octet-stream Date: Tue, 26 May 2020 02:29:47 +0300 Subject: [PATCH 135/157] Remove nanoid from dependencies. --- package.json | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/package.json b/package.json index ea045a072..5161fd701 100644 --- a/package.json +++ b/package.json @@ -64,8 +64,7 @@ }, "dependencies": { "data-uri-to-buffer": "^3.0.0", - "fetch-blob": "^1.0.5", - "nanoid": "^3.1.9" + "fetch-blob": "^1.0.5" }, "esm": { "sourceMap": true From 313d5291ba8a5a0f075689a357422cd15bdf4df6 Mon Sep 17 00:00:00 2001 From: octet-stream Date: Tue, 26 May 2020 14:02:46 +0300 Subject: [PATCH 136/157] Remove unnecessary comments. --- test/main.js | 4 ---- 1 file changed, 4 deletions(-) diff --git a/test/main.js b/test/main.js index 5504d42fc..2c89bb3a1 100644 --- a/test/main.js +++ b/test/main.js @@ -1443,10 +1443,6 @@ describe('node-fetch', () => { size: fs.statSync(filename).size }); - // for await (const ch of form) { - // console.log(String(ch)) - // } - const url = `${base}multipart`; const options = { method: 'POST', From 99c0b5e22933c0ed15319fc48fd600901a3a04db Mon Sep 17 00:00:00 2001 From: octet-stream Date: Tue, 26 May 2020 22:33:41 +0300 Subject: [PATCH 137/157] Code style fix for form-data-iterator. Fix for extractContentType call. --- src/body.js | 4 ++-- src/request.js | 2 +- src/utils/form-data-iterator.js | 4 +--- 3 files changed, 4 insertions(+), 6 deletions(-) diff --git a/src/body.js b/src/body.js index 482126a76..3b7e75d4d 100644 --- a/src/body.js +++ b/src/body.js @@ -304,7 +304,7 @@ export function clone(instance, highWaterMark) { * @param {any} body Any options.body input * @returns {string | null} */ -export function extractContentType(body) { +export function extractContentType(body, request) { // Body is null or undefined if (body === null) { return null; @@ -336,7 +336,7 @@ export function extractContentType(body) { } if (isFormData(body)) { - return `multipart/form-data; boundary=${this[INTERNALS].boundary}`; + return `multipart/form-data; boundary=${request[INTERNALS].boundary}`; } // Body is stream - can't really do much about this diff --git a/src/request.js b/src/request.js index 7f66120f4..3ab0f9e0b 100644 --- a/src/request.js +++ b/src/request.js @@ -100,7 +100,7 @@ export default class Request { const headers = new Headers(init.headers || input.headers || {}); if (inputBody !== null && !headers.has('Content-Type')) { - const contentType = extractContentType.call(this, inputBody); + const contentType = extractContentType(inputBody, this); if (contentType) { headers.append('Content-Type', contentType); } diff --git a/src/utils/form-data-iterator.js b/src/utils/form-data-iterator.js index 36f2043f0..dd3df67c7 100644 --- a/src/utils/form-data-iterator.js +++ b/src/utils/form-data-iterator.js @@ -30,7 +30,7 @@ function getHeader(boundary, name, field) { * @param {FormData} form * @param {string} boundary */ -async function* formDataIterator(form, boundary) { +export default async function* formDataIterator(form, boundary) { for (const [name, value] of form) { yield getHeader(boundary, name, value); @@ -45,5 +45,3 @@ async function* formDataIterator(form, boundary) { yield `${dashes}${boundary}${dashes}${carriage.repeat(2)}`; } - -export default formDataIterator; From b7497cb94e7ed52dd6f21769776923c89a65a2d0 Mon Sep 17 00:00:00 2001 From: octet-stream Date: Tue, 26 May 2020 23:19:45 +0300 Subject: [PATCH 138/157] Fix lint errors. --- src/body.js | 2 +- src/utils/form-data-iterator.js | 34 ++++++++++++++++----------------- 2 files changed, 17 insertions(+), 19 deletions(-) diff --git a/src/body.js b/src/body.js index 161db2439..c2580aaa3 100644 --- a/src/body.js +++ b/src/body.js @@ -12,7 +12,7 @@ import Blob from 'fetch-blob'; import getBoundary from './utils/boundary.js'; import FetchError from './errors/fetch-error.js'; -import formDataIterator from './utils/form-data-iterator.js' +import formDataIterator from './utils/form-data-iterator.js'; import {isBlob, isURLSearchParameters, isAbortError, isFormData} from './utils/is.js'; const INTERNALS = Symbol('Body internals'); diff --git a/src/utils/form-data-iterator.js b/src/utils/form-data-iterator.js index dd3df67c7..ed6e89db0 100644 --- a/src/utils/form-data-iterator.js +++ b/src/utils/form-data-iterator.js @@ -1,5 +1,3 @@ -import {Readable} from 'stream'; - import {isBlob} from './is.js'; const carriage = '\r\n'; @@ -15,33 +13,33 @@ const dashes = '-'.repeat(2); function getHeader(boundary, name, field) { let header = ''; - header += `${dashes}${boundary}${carriage}`; - header += `Content-Disposition: form-data; name="${name}"`; + header += `${dashes}${boundary}${carriage}`; + header += `Content-Disposition: form-data; name="${name}"`; - if (isBlob(field)) { - header += `; filename="${field.name}"${carriage}`; - header += `Content-Type: ${field.type || 'application/octet-stream'}`; - } + if (isBlob(field)) { + header += `; filename="${field.name}"${carriage}`; + header += `Content-Type: ${field.type || 'application/octet-stream'}`; + } - return `${header}${carriage.repeat(2)}`; + return `${header}${carriage.repeat(2)}`; } /** * @param {FormData} form * @param {string} boundary */ -export default async function* formDataIterator(form, boundary) { +export default async function * formDataIterator(form, boundary) { for (const [name, value] of form) { - yield getHeader(boundary, name, value); + yield getHeader(boundary, name, value); - if (isBlob(value)) { - yield * value.stream(); - } else { - yield value; - } - - yield carriage; + if (isBlob(value)) { + yield * value.stream(); + } else { + yield value; } + yield carriage; + } + yield `${dashes}${boundary}${dashes}${carriage.repeat(2)}`; } From a2ac3947da43bd733d9fdf7b34d3ae065226247f Mon Sep 17 00:00:00 2001 From: octet-stream Date: Tue, 26 May 2020 23:35:35 +0300 Subject: [PATCH 139/157] Bump minimal required Node version to 10.17 (was 10.16) --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index a4104726a..fba53a01f 100644 --- a/package.json +++ b/package.json @@ -17,7 +17,7 @@ ], "types": "./@types/index.d.ts", "engines": { - "node": ">=10.16" + "node": ">=10.17" }, "scripts": { "build": "rollup -c", From 4ef91d28d1692710fc085db4b84e955e06c9fcb4 Mon Sep 17 00:00:00 2001 From: octet-stream Date: Fri, 5 Jun 2020 02:07:25 +0300 Subject: [PATCH 140/157] Move form-data utilities into one file and rewrite boundary helper. --- src/body.js | 2 +- src/utils/boundary.js | 4 +- .../{form-data-iterator.js => form-data.js} | 37 ++++++++++++++++++- test/main.js | 2 +- 4 files changed, 41 insertions(+), 4 deletions(-) rename src/utils/{form-data-iterator.js => form-data.js} (53%) diff --git a/src/body.js b/src/body.js index d2422fbe1..935202d0e 100644 --- a/src/body.js +++ b/src/body.js @@ -12,7 +12,7 @@ import Blob from 'fetch-blob'; import getBoundary from './utils/boundary.js'; import FetchError from './errors/fetch-error.js'; -import formDataIterator from './utils/form-data-iterator.js'; +import {formDataIterator} from './utils/form-data.js'; import {isBlob, isURLSearchParameters, isAbortError, isFormData} from './utils/is.js'; const INTERNALS = Symbol('Body internals'); diff --git a/src/utils/boundary.js b/src/utils/boundary.js index 32b50e9ca..7fff805d8 100644 --- a/src/utils/boundary.js +++ b/src/utils/boundary.js @@ -1,6 +1,8 @@ +import {randomBytes} from 'crypto' + /** * @api private */ -const boundary = () => Math.random().toString(32).slice(2); +const boundary = () => randomBytes(8).toString('hex'); export default boundary; diff --git a/src/utils/form-data-iterator.js b/src/utils/form-data.js similarity index 53% rename from src/utils/form-data-iterator.js rename to src/utils/form-data.js index ed6e89db0..81da7c880 100644 --- a/src/utils/form-data-iterator.js +++ b/src/utils/form-data.js @@ -2,6 +2,12 @@ import {isBlob} from './is.js'; const carriage = '\r\n'; const dashes = '-'.repeat(2); +const carriageLength = Buffer.byteLength(carriage); + +/** + * @param {string} boundary + */ +const getFooter = boundary => `${dashes}${boundary}${dashes}${carriage.repeat(2)}`; /** * @param {string} boundary @@ -28,7 +34,7 @@ function getHeader(boundary, name, field) { * @param {FormData} form * @param {string} boundary */ -export default async function * formDataIterator(form, boundary) { +export async function * formDataIterator(form, boundary) { for (const [name, value] of form) { yield getHeader(boundary, name, value); @@ -43,3 +49,32 @@ export default async function * formDataIterator(form, boundary) { yield `${dashes}${boundary}${dashes}${carriage.repeat(2)}`; } + +/** + * @param {FormData} form + * @param {string} boundary + */ +export function getFormDataLength(form, boundary) { + let length = 0; + + for (const [name, value] of form) { + length += Buffer.byteLength(getHeader(boundary, name, value)); + + if (isBlob(value)) { + length = value.size; + } else { + length += Buffer.byteLength(value); + } + + length += carriageLength; + } + + // The FormData is empty? + if (length === 0) { + return length; + } + + length += Buffer.byteLength(getFooter(boundary)); + + return length; +} diff --git a/test/main.js b/test/main.js index 68f95a5f8..25dcc5a2b 100644 --- a/test/main.js +++ b/test/main.js @@ -1334,7 +1334,7 @@ describe('node-fetch', () => { }); }); - it('should support formdata-node as POST body', () => { + it('should support spec-compliant form-data as POST body', () => { const form = new FormDataNode(); const filename = path.join('test', 'utils', 'dummy.txt'); From f2745db8113f820710bc803feec014d12da8537e Mon Sep 17 00:00:00 2001 From: octet-stream Date: Fri, 5 Jun 2020 02:10:15 +0300 Subject: [PATCH 141/157] Move boundary helper to form-data.js file --- src/body.js | 3 +-- src/utils/form-data.js | 7 +++++++ 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/src/body.js b/src/body.js index 935202d0e..102ab3f84 100644 --- a/src/body.js +++ b/src/body.js @@ -10,9 +10,8 @@ import {types} from 'util'; import Blob from 'fetch-blob'; -import getBoundary from './utils/boundary.js'; import FetchError from './errors/fetch-error.js'; -import {formDataIterator} from './utils/form-data.js'; +import {formDataIterator, getBoundary} from './utils/form-data.js'; import {isBlob, isURLSearchParameters, isAbortError, isFormData} from './utils/is.js'; const INTERNALS = Symbol('Body internals'); diff --git a/src/utils/form-data.js b/src/utils/form-data.js index 81da7c880..e749ef61b 100644 --- a/src/utils/form-data.js +++ b/src/utils/form-data.js @@ -1,3 +1,5 @@ +import {randomBytes} from 'crypto'; + import {isBlob} from './is.js'; const carriage = '\r\n'; @@ -30,6 +32,11 @@ function getHeader(boundary, name, field) { return `${header}${carriage.repeat(2)}`; } +/** + * @return {string} + */ +export const getBoundary = () => randomBytes(8).toString('hex'); + /** * @param {FormData} form * @param {string} boundary From f4b902ad25032a2d95cd174896c5106dc2894203 Mon Sep 17 00:00:00 2001 From: octet-stream Date: Fri, 5 Jun 2020 02:11:50 +0300 Subject: [PATCH 142/157] Remove boundary.js --- src/utils/boundary.js | 8 -------- 1 file changed, 8 deletions(-) delete mode 100644 src/utils/boundary.js diff --git a/src/utils/boundary.js b/src/utils/boundary.js deleted file mode 100644 index 7fff805d8..000000000 --- a/src/utils/boundary.js +++ /dev/null @@ -1,8 +0,0 @@ -import {randomBytes} from 'crypto' - -/** - * @api private - */ -const boundary = () => randomBytes(8).toString('hex'); - -export default boundary; From 25cba2d6eca54e3db6c420ee4a85ca07f1565ca5 Mon Sep 17 00:00:00 2001 From: octet-stream Date: Fri, 5 Jun 2020 02:21:07 +0300 Subject: [PATCH 143/157] Add getFormDataLength call in getTotalBytes function. --- src/body.js | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/body.js b/src/body.js index 102ab3f84..3fab9e0c8 100644 --- a/src/body.js +++ b/src/body.js @@ -11,7 +11,7 @@ import {types} from 'util'; import Blob from 'fetch-blob'; import FetchError from './errors/fetch-error.js'; -import {formDataIterator, getBoundary} from './utils/form-data.js'; +import {formDataIterator, getBoundary, getFormDataLength} from './utils/form-data.js'; import {isBlob, isURLSearchParameters, isAbortError, isFormData} from './utils/is.js'; const INTERNALS = Symbol('Body internals'); @@ -322,7 +322,9 @@ export const extractContentType = (body, request) => { * @param {any} obj.body Body object from the Body instance. * @returns {number | null} */ -export const getTotalBytes = ({body}) => { +export const getTotalBytes = (request) => { + const {body} = request; + // Body is null or undefined if (body === null) { return 0; @@ -343,6 +345,11 @@ export const getTotalBytes = ({body}) => { return body.hasKnownLength && body.hasKnownLength() ? body.getLengthSync() : null; } + // Body is a spec-compliant form-data + if (isFormData(body)) { + return getFormDataLength(request[INTERNALS].boundary); + } + // Body is stream return null; }; From 06fa125f2b0b6f6c6d08452764f02cc77fd0ed7c Mon Sep 17 00:00:00 2001 From: octet-stream Date: Fri, 5 Jun 2020 14:20:03 +0300 Subject: [PATCH 144/157] Fix for ESLint problem --- src/body.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/body.js b/src/body.js index 3fab9e0c8..880606b58 100644 --- a/src/body.js +++ b/src/body.js @@ -322,7 +322,7 @@ export const extractContentType = (body, request) => { * @param {any} obj.body Body object from the Body instance. * @returns {number | null} */ -export const getTotalBytes = (request) => { +export const getTotalBytes = request => { const {body} = request; // Body is null or undefined From ca764f56f2d80de2a424273ecf683eab6d9a60bc Mon Sep 17 00:00:00 2001 From: octet-stream Date: Fri, 5 Jun 2020 18:17:06 +0300 Subject: [PATCH 145/157] Fix for getFormDataLength. --- src/utils/form-data.js | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/utils/form-data.js b/src/utils/form-data.js index e749ef61b..830be593d 100644 --- a/src/utils/form-data.js +++ b/src/utils/form-data.js @@ -76,11 +76,6 @@ export function getFormDataLength(form, boundary) { length += carriageLength; } - // The FormData is empty? - if (length === 0) { - return length; - } - length += Buffer.byteLength(getFooter(boundary)); return length; From 886f396bbd5b6c09a9ad1b90939a3730eec43886 Mon Sep 17 00:00:00 2001 From: octet-stream Date: Sat, 6 Jun 2020 01:54:40 +0300 Subject: [PATCH 146/157] Add a few tests for form-data helpers. --- src/utils/form-data.js | 6 ++--- test/form-data.js | 57 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 60 insertions(+), 3 deletions(-) create mode 100644 test/form-data.js diff --git a/src/utils/form-data.js b/src/utils/form-data.js index 830be593d..1fd23b0ad 100644 --- a/src/utils/form-data.js +++ b/src/utils/form-data.js @@ -54,7 +54,7 @@ export async function * formDataIterator(form, boundary) { yield carriage; } - yield `${dashes}${boundary}${dashes}${carriage.repeat(2)}`; + yield getFooter(boundary); } /** @@ -68,9 +68,9 @@ export function getFormDataLength(form, boundary) { length += Buffer.byteLength(getHeader(boundary, name, value)); if (isBlob(value)) { - length = value.size; + length += value.size; } else { - length += Buffer.byteLength(value); + length += Buffer.byteLength(String(value)); } length += carriageLength; diff --git a/test/form-data.js b/test/form-data.js new file mode 100644 index 000000000..999b13af1 --- /dev/null +++ b/test/form-data.js @@ -0,0 +1,57 @@ +import stream from 'stream'; + +import FormData from 'formdata-node'; +import Blob from 'fetch-blob'; + +import chai from 'chai'; + +import {getFormDataLength, getBoundary, formDataIterator} from '../src/utils/form-data.js'; + +const {expect} = chai; + +const carriage = '\r\n'; +const getFooter = boundary => `--${boundary}--${carriage.repeat(2)}`; + +describe('FormData', () => { + it('should return a length for empty form-data', () => { + const form = new FormData(); + const boundary = getBoundary(); + + expect(getFormDataLength(form, boundary)).to.be.equal(Buffer.byteLength(getFooter(boundary))); + }); + + it('should add a Blob field\'s size to the FormData length', async () => { + const form = new FormData(); + const boundary = getBoundary(); + + const string = 'Hello, world!'; + const expected = Buffer.byteLength( + `--${boundary}${carriage}` + + `Content-Disposition: form-data; name="field"${carriage.repeat(2)}` + + string + + `${carriage}${getFooter(boundary)}` + ); + + form.set('field', string); + + expect(getFormDataLength(form, boundary)).to.be.equal(expected); + }); + + it('should return a length for a Blog field', () => { + const form = new FormData(); + const boundary = getBoundary(); + + const blob = new Blob(['Hello, world!'], {type: 'text/plain'}); + + form.set('blob', blob); + + const expected = blob.size + Buffer.byteLength( + `--${boundary}${carriage}` + + 'Content-Disposition: form-data; name="blob"; ' + + `filename="blob"${carriage}Content-Type: text/plain` + + `${carriage.repeat(3)}${getFooter(boundary)}` + ); + + expect(getFormDataLength(form, boundary)).to.be.equal(expected); + }); +}); From 62ba68322ca10a7072ac1ef2e9e68ff5cfd19d8f Mon Sep 17 00:00:00 2001 From: octet-stream Date: Sat, 6 Jun 2020 02:05:43 +0300 Subject: [PATCH 147/157] Add formDataIterator test for empty form-data. --- test/form-data.js | 11 ++++++++++- test/utils/read-stream.js | 9 +++++++++ 2 files changed, 19 insertions(+), 1 deletion(-) create mode 100644 test/utils/read-stream.js diff --git a/test/form-data.js b/test/form-data.js index 999b13af1..c21129c67 100644 --- a/test/form-data.js +++ b/test/form-data.js @@ -5,6 +5,8 @@ import Blob from 'fetch-blob'; import chai from 'chai'; +import read from './utils/read-stream.js'; + import {getFormDataLength, getBoundary, formDataIterator} from '../src/utils/form-data.js'; const {expect} = chai; @@ -20,7 +22,7 @@ describe('FormData', () => { expect(getFormDataLength(form, boundary)).to.be.equal(Buffer.byteLength(getFooter(boundary))); }); - it('should add a Blob field\'s size to the FormData length', async () => { + it('should add a Blob field\'s size to the FormData length', () => { const form = new FormData(); const boundary = getBoundary(); @@ -54,4 +56,11 @@ describe('FormData', () => { expect(getFormDataLength(form, boundary)).to.be.equal(expected); }); + + it('should create a body from empty form-data', async () => { + const form = new FormData(); + const boundary = getBoundary(); + + expect(String(await read(formDataIterator(form, boundary)))).to.be.equal(getFooter(boundary)); + }) }); diff --git a/test/utils/read-stream.js b/test/utils/read-stream.js new file mode 100644 index 000000000..90dcf6e59 --- /dev/null +++ b/test/utils/read-stream.js @@ -0,0 +1,9 @@ +export default async function readStream(stream) { + const chunks = []; + + for await (const chunk of stream) { + chunks.push(chunk instanceof Buffer ? chunk : Buffer.from(chunk)); + } + + return Buffer.concat(chunks); +} From c15974a784a76ce3631d82fc14f474729122f26e Mon Sep 17 00:00:00 2001 From: octet-stream Date: Sat, 6 Jun 2020 02:18:25 +0300 Subject: [PATCH 148/157] Add a test for default content-type in form-data --- test/form-data.js | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/test/form-data.js b/test/form-data.js index c21129c67..7af47f8e8 100644 --- a/test/form-data.js +++ b/test/form-data.js @@ -62,5 +62,14 @@ describe('FormData', () => { const boundary = getBoundary(); expect(String(await read(formDataIterator(form, boundary)))).to.be.equal(getFooter(boundary)); - }) + }); + + it('should set default content-type', async () => { + const form = new FormData(); + const boundary = getBoundary(); + + form.set('blob', new Blob([])); + + expect(String(await read(formDataIterator(form, boundary)))).to.contain('Content-Type: application/octet-stream'); + }); }); From 60b41d350b3f9d4924f51e87730b535b6613521b Mon Sep 17 00:00:00 2001 From: octet-stream Date: Sun, 7 Jun 2020 16:20:48 +0300 Subject: [PATCH 149/157] Add tests form formDataIterator. --- test/form-data.js | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/test/form-data.js b/test/form-data.js index 7af47f8e8..b9ce6765c 100644 --- a/test/form-data.js +++ b/test/form-data.js @@ -72,4 +72,34 @@ describe('FormData', () => { expect(String(await read(formDataIterator(form, boundary)))).to.contain('Content-Type: application/octet-stream'); }); + + it('should create a body with a FormData field', async () => { + const form = new FormData(); + const boundary = getBoundary(); + const string = 'Hello, World!'; + + form.set('field', string); + + const expected = `--${boundary}${carriage}` + + `Content-Disposition: form-data; name="field"${carriage.repeat(2)}` + + string + + `${carriage}${getFooter(boundary)}`; + + expect(String(await read(formDataIterator(form, boundary)))).to.be.eqls(expected); + }); + + it('should create a body with a FormData Blob field', async () => { + const form = new FormData(); + const boundary = getBoundary(); + + const expected = `--${boundary}${carriage}` + + 'Content-Disposition: form-data; name="blob"; ' + + `filename="blob"${carriage}Content-Type: text/plain${carriage.repeat(2)}` + + 'Hello, World!' + + `${carriage}${getFooter(boundary)}`; + + form.set('blob', new Blob(['Hello, World!'], {type: 'text/plain'})); + + expect(String(await read(formDataIterator(form, boundary)))).to.be.equal(expected); + }); }); From 87658a3a6e5728dfe661d65bcd8a8f0facc4cb53 Mon Sep 17 00:00:00 2001 From: octet-stream Date: Sun, 7 Jun 2020 16:54:25 +0300 Subject: [PATCH 150/157] Replace constructor name checking with Symbol.toStringTag in isFormData helper. --- src/utils/is.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/utils/is.js b/src/utils/is.js index 4a05efd43..b4f1dd58c 100644 --- a/src/utils/is.js +++ b/src/utils/is.js @@ -62,7 +62,7 @@ export function isFormData(object) { typeof object.values === 'function' && typeof object.entries === 'function' && typeof object.constructor === 'function' && - object.constructor.name === 'FormData' + object[NAME] === 'FormData' ); } From e25b20b76f748d5800d3379a6df5f788371b8724 Mon Sep 17 00:00:00 2001 From: octet-stream Date: Sun, 7 Jun 2020 20:25:36 +0300 Subject: [PATCH 151/157] Code style fixes --- test/form-data.js | 36 +++++++++++++++++------------------- 1 file changed, 17 insertions(+), 19 deletions(-) diff --git a/test/form-data.js b/test/form-data.js index b9ce6765c..36bd2f10f 100644 --- a/test/form-data.js +++ b/test/form-data.js @@ -1,5 +1,3 @@ -import stream from 'stream'; - import FormData from 'formdata-node'; import Blob from 'fetch-blob'; @@ -28,10 +26,10 @@ describe('FormData', () => { const string = 'Hello, world!'; const expected = Buffer.byteLength( - `--${boundary}${carriage}` - + `Content-Disposition: form-data; name="field"${carriage.repeat(2)}` - + string - + `${carriage}${getFooter(boundary)}` + `--${boundary}${carriage}` + + `Content-Disposition: form-data; name="field"${carriage.repeat(2)}` + + string + + `${carriage}${getFooter(boundary)}` ); form.set('field', string); @@ -48,10 +46,10 @@ describe('FormData', () => { form.set('blob', blob); const expected = blob.size + Buffer.byteLength( - `--${boundary}${carriage}` - + 'Content-Disposition: form-data; name="blob"; ' - + `filename="blob"${carriage}Content-Type: text/plain` - + `${carriage.repeat(3)}${getFooter(boundary)}` + `--${boundary}${carriage}` + + 'Content-Disposition: form-data; name="blob"; ' + + `filename="blob"${carriage}Content-Type: text/plain` + + `${carriage.repeat(3)}${getFooter(boundary)}` ); expect(getFormDataLength(form, boundary)).to.be.equal(expected); @@ -80,10 +78,10 @@ describe('FormData', () => { form.set('field', string); - const expected = `--${boundary}${carriage}` - + `Content-Disposition: form-data; name="field"${carriage.repeat(2)}` - + string - + `${carriage}${getFooter(boundary)}`; + const expected = `--${boundary}${carriage}` + + `Content-Disposition: form-data; name="field"${carriage.repeat(2)}` + + string + + `${carriage}${getFooter(boundary)}`; expect(String(await read(formDataIterator(form, boundary)))).to.be.eqls(expected); }); @@ -92,11 +90,11 @@ describe('FormData', () => { const form = new FormData(); const boundary = getBoundary(); - const expected = `--${boundary}${carriage}` - + 'Content-Disposition: form-data; name="blob"; ' - + `filename="blob"${carriage}Content-Type: text/plain${carriage.repeat(2)}` - + 'Hello, World!' - + `${carriage}${getFooter(boundary)}`; + const expected = `--${boundary}${carriage}` + + 'Content-Disposition: form-data; name="blob"; ' + + `filename="blob"${carriage}Content-Type: text/plain${carriage.repeat(2)}` + + 'Hello, World!' + + `${carriage}${getFooter(boundary)}`; form.set('blob', new Blob(['Hello, World!'], {type: 'text/plain'})); From d298318f4870d8d8417ec179eed8b1619ce10ab8 Mon Sep 17 00:00:00 2001 From: octet-stream Date: Mon, 8 Jun 2020 01:48:52 +0300 Subject: [PATCH 152/157] Fix a typo --- test/form-data.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/test/form-data.js b/test/form-data.js index 36bd2f10f..573126ed2 100644 --- a/test/form-data.js +++ b/test/form-data.js @@ -10,6 +10,7 @@ import {getFormDataLength, getBoundary, formDataIterator} from '../src/utils/for const {expect} = chai; const carriage = '\r\n'; + const getFooter = boundary => `--${boundary}--${carriage.repeat(2)}`; describe('FormData', () => { @@ -37,7 +38,7 @@ describe('FormData', () => { expect(getFormDataLength(form, boundary)).to.be.equal(expected); }); - it('should return a length for a Blog field', () => { + it('should return a length for a Blob field', () => { const form = new FormData(); const boundary = getBoundary(); From e2948968066077fc72df8c91e10dbe405c80995a Mon Sep 17 00:00:00 2001 From: octet-stream Date: Mon, 8 Jun 2020 01:50:26 +0300 Subject: [PATCH 153/157] Fix a method call in form-data tests. --- test/form-data.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/form-data.js b/test/form-data.js index 573126ed2..fe08fe4c6 100644 --- a/test/form-data.js +++ b/test/form-data.js @@ -84,7 +84,7 @@ describe('FormData', () => { string + `${carriage}${getFooter(boundary)}`; - expect(String(await read(formDataIterator(form, boundary)))).to.be.eqls(expected); + expect(String(await read(formDataIterator(form, boundary)))).to.be.equal(expected); }); it('should create a body with a FormData Blob field', async () => { From d2476cd713f2bd5e5df13f92ce796f697a720ce5 Mon Sep 17 00:00:00 2001 From: Konstantin Vyatkin Date: Wed, 10 Jun 2020 10:33:43 -0400 Subject: [PATCH 154/157] replace parted with Busboy --- package.json | 11 ++++++++++- test/main.js | 1 - test/request.js | 1 - test/response.js | 1 - test/utils/server.js | 20 +++++++++++++------- 5 files changed, 23 insertions(+), 11 deletions(-) diff --git a/package.json b/package.json index a13d69a2f..84ea8b3cc 100644 --- a/package.json +++ b/package.json @@ -49,6 +49,7 @@ "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", @@ -95,7 +96,15 @@ "import/extensions": 0, "import/no-useless-path-segments": 0, "unicorn/import-index": 0, - "capitalized-comments": 0 + "capitalized-comments": 0, + "node/no-unsupported-features/node-builtins": [ + "error", + { + "ignores": [ + "stream.Readable.from" + ] + } + ] }, "ignores": [ "dist", diff --git a/test/main.js b/test/main.js index 0006e902c..300562e51 100644 --- a/test/main.js +++ b/test/main.js @@ -1,5 +1,4 @@ // Test tools -/* eslint-disable node/no-unsupported-features/node-builtins */ import zlib from 'zlib'; import crypto from 'crypto'; import http from 'http'; diff --git a/test/request.js b/test/request.js index 502b86a9b..5a7acc0f9 100644 --- a/test/request.js +++ b/test/request.js @@ -1,4 +1,3 @@ -/* eslint-disable node/no-unsupported-features/node-builtins */ import stream from 'stream'; import http from 'http'; diff --git a/test/response.js b/test/response.js index 7126eb95c..7ccef7102 100644 --- a/test/response.js +++ b/test/response.js @@ -1,4 +1,3 @@ -/* eslint-disable node/no-unsupported-features/node-builtins */ import * as stream from 'stream'; import chai from 'chai'; diff --git a/test/utils/server.js b/test/utils/server.js index e01027276..3086d3cbc 100644 --- a/test/utils/server.js +++ b/test/utils/server.js @@ -1,7 +1,6 @@ import http from 'http'; import zlib from 'zlib'; -import parted from 'parted'; -const {multipart: Multipart} = parted; +import Busboy from 'busboy'; export default class TestServer { constructor() { @@ -364,12 +363,19 @@ export default class TestServer { if (p === '/multipart') { res.statusCode = 200; res.setHeader('Content-Type', 'application/json'); - const parser = new Multipart(request.headers['content-type']); + const busboy = new Busboy({headers: request.headers}); let body = ''; - parser.on('part', (field, part) => { - body += field + '=' + part; + busboy.on('file', async (fieldName, file, fileName) => { + body += `file=${fileName}`; + // consume file data + // eslint-disable-next-line no-empty, no-unused-vars + for await (const c of file) { } }); - parser.on('end', () => { + + busboy.on('field', (fieldName, value) => { + body += `${fieldName}=${value}`; + }); + busboy.on('finish', () => { res.end(JSON.stringify({ method: request.method, url: request.url, @@ -377,7 +383,7 @@ export default class TestServer { body })); }); - request.pipe(parser); + request.pipe(busboy); } if (p === '/m%C3%B6bius') { From c1d45d3b6012eb8dfb1cfa66fa2cd8806bb69941 Mon Sep 17 00:00:00 2001 From: Konstantin Vyatkin Date: Wed, 10 Jun 2020 10:43:23 -0400 Subject: [PATCH 155/157] don't ignore tests on Windows --- test/main.js | 101 +++++++++++++++++++++++++-------------------------- 1 file changed, 50 insertions(+), 51 deletions(-) diff --git a/test/main.js b/test/main.js index 300562e51..49f4726b7 100644 --- a/test/main.js +++ b/test/main.js @@ -5,7 +5,7 @@ import http from 'http'; import fs from 'fs'; import stream from 'stream'; import path from 'path'; -import {lookup} from 'dns'; +import { lookup } from 'dns'; import vm from 'vm'; import chai from 'chai'; import chaiPromised from 'chai-as-promised'; @@ -18,7 +18,7 @@ import delay from 'delay'; import AbortControllerPolyfill from 'abortcontroller-polyfill/dist/abortcontroller.js'; import AbortController2 from 'abort-controller'; -const {AbortController} = AbortControllerPolyfill; +const { AbortController } = AbortControllerPolyfill; // Test subjects import Blob from 'fetch-blob'; @@ -29,11 +29,11 @@ import fetch, { Request, Response } from '../src/index.js'; -import {FetchError as FetchErrorOrig} from '../src/errors/fetch-error.js'; -import HeadersOrig, {fromRawHeaders} from '../src/headers.js'; +import { FetchError as FetchErrorOrig } from '../src/errors/fetch-error.js'; +import HeadersOrig, { fromRawHeaders } from '../src/headers.js'; import RequestOrig from '../src/request.js'; import ResponseOrig from '../src/response.js'; -import Body, {getTotalBytes, extractContentType} from '../src/body.js'; +import Body, { getTotalBytes, extractContentType } from '../src/body.js'; import TestServer from './utils/server.js'; const { @@ -46,7 +46,7 @@ chai.use(chaiPromised); chai.use(chaiIterator); chai.use(chaiString); chai.use(chaiTimeout); -const {expect} = chai; +const { expect } = chai; const local = new TestServer(); const base = `http://${local.hostname}:${local.port}/`; @@ -59,8 +59,6 @@ after(done => { local.stop(done); }); -const itIf = value => value ? it : it.skip; - function streamToPromise(stream, dataHandler) { return new Promise((resolve, reject) => { stream.on('data', (...args) => { @@ -109,11 +107,12 @@ describe('node-fetch', () => { return expect(fetch(url)).to.eventually.be.rejectedWith(TypeError, /URL scheme "ftp" is not supported/); }); - itIf(process.platform !== 'win32')('should reject with error on network failure', () => { + it('should reject with error on network failure', function () { + this.timeout(5000); const url = 'http://localhost:50000/'; return expect(fetch(url)).to.eventually.be.rejected .and.be.an.instanceOf(FetchError) - .and.include({type: 'system', code: 'ECONNREFUSED', errno: 'ECONNREFUSED'}); + .and.include({ type: 'system', code: 'ECONNREFUSED', errno: 'ECONNREFUSED' }); }); it('error should contain system error if one occurred', () => { @@ -126,7 +125,7 @@ describe('node-fetch', () => { return expect(err).to.not.have.property('erroredSysCall'); }); - itIf(process.platform !== 'win32')('system error is extracted from failed requests', () => { + if ('system error is extracted from failed requests', () => { const url = 'http://localhost:50000/'; return expect(fetch(url)).to.eventually.be.rejected .and.be.an.instanceOf(FetchError) @@ -199,7 +198,7 @@ describe('node-fetch', () => { return res.json().then(result => { expect(res.bodyUsed).to.be.true; expect(result).to.be.an('object'); - expect(result).to.deep.equal({name: 'value'}); + expect(result).to.deep.equal({ name: 'value' }); }); }); }); @@ -207,7 +206,7 @@ describe('node-fetch', () => { it('should send request with custom headers', () => { const url = `${base}inspect`; const options = { - headers: {'x-custom-header': 'abc'} + headers: { 'x-custom-header': 'abc' } }; return fetch(url, options).then(res => { return res.json(); @@ -219,7 +218,7 @@ describe('node-fetch', () => { it('should accept headers instance', () => { const url = `${base}inspect`; const options = { - headers: new Headers({'x-custom-header': 'abc'}) + headers: new Headers({ 'x-custom-header': 'abc' }) }; return fetch(url, options).then(res => { return res.json(); @@ -480,7 +479,7 @@ describe('node-fetch', () => { it('should follow redirect code 301 and keep existing headers', () => { const url = `${base}redirect/301`; const options = { - headers: new Headers({'x-custom-header': 'abc'}) + headers: new Headers({ 'x-custom-header': 'abc' }) }; return fetch(url, options).then(res => { expect(res.url).to.equal(`${base}inspect`); @@ -833,8 +832,8 @@ describe('node-fetch', () => { const controller2 = new AbortController2(); const fetches = [ - fetch(`${base}timeout`, {signal: controller.signal}), - fetch(`${base}timeout`, {signal: controller2.signal}), + fetch(`${base}timeout`, { signal: controller.signal }), + fetch(`${base}timeout`, { signal: controller2.signal }), fetch( `${base}timeout`, { @@ -842,7 +841,7 @@ describe('node-fetch', () => { signal: controller.signal, headers: { 'Content-Type': 'application/json', - body: JSON.stringify({hello: 'world'}) + body: JSON.stringify({ hello: 'world' }) } } ) @@ -880,10 +879,10 @@ describe('node-fetch', () => { it('should remove internal AbortSignal event listener after request is aborted', () => { const controller = new AbortController(); - const {signal} = controller; + const { signal } = controller; const promise = fetch( `${base}timeout`, - {signal} + { signal } ); const result = expect(promise).to.eventually.be.rejected .and.be.an.instanceof(Error) @@ -925,11 +924,11 @@ describe('node-fetch', () => { it('should remove internal AbortSignal event listener after request and response complete without aborting', () => { const controller = new AbortController(); - const {signal} = controller; - const fetchHtml = fetch(`${base}html`, {signal}) + const { signal } = controller; + const fetchHtml = fetch(`${base}html`, { signal }) .then(res => res.text()); - const fetchResponseError = fetch(`${base}error/reset`, {signal}); - const fetchRedirect = fetch(`${base}redirect/301`, {signal}).then(res => res.json()); + const fetchResponseError = fetch(`${base}error/reset`, { signal }); + const fetchRedirect = fetch(`${base}redirect/301`, { signal }).then(res => res.json()); return Promise.all([ expect(fetchHtml).to.eventually.be.fulfilled.and.equal(''), expect(fetchResponseError).to.be.eventually.rejected, @@ -943,7 +942,7 @@ describe('node-fetch', () => { const controller = new AbortController(); return expect(fetch( `${base}slow`, - {signal: controller.signal} + { signal: controller.signal } )) .to.eventually.be.fulfilled .then(res => { @@ -960,7 +959,7 @@ describe('node-fetch', () => { const controller = new AbortController(); return expect(fetch( `${base}slow`, - {signal: controller.signal} + { signal: controller.signal } )) .to.eventually.be.fulfilled .then(res => { @@ -976,7 +975,7 @@ describe('node-fetch', () => { const controller = new AbortController(); expect(fetch( `${base}slow`, - {signal: controller.signal} + { signal: controller.signal } )) .to.eventually.be.fulfilled .then(res => { @@ -992,11 +991,11 @@ describe('node-fetch', () => { it('should cancel request body of type Stream with AbortError when aborted', () => { const controller = new AbortController(); - const body = new stream.Readable({objectMode: true}); + const body = new stream.Readable({ objectMode: true }); body._read = () => { }; const promise = fetch( `${base}slow`, - {signal: controller.signal, body, method: 'POST'} + { signal: controller.signal, body, method: 'POST' } ); const result = Promise.all([ @@ -1022,15 +1021,15 @@ describe('node-fetch', () => { it('should throw a TypeError if a signal is not of type AbortSignal', () => { return Promise.all([ - expect(fetch(`${base}inspect`, {signal: {}})) + expect(fetch(`${base}inspect`, { signal: {} })) .to.be.eventually.rejected .and.be.an.instanceof(TypeError) .and.have.property('message').includes('AbortSignal'), - expect(fetch(`${base}inspect`, {signal: ''})) + expect(fetch(`${base}inspect`, { signal: '' })) .to.be.eventually.rejected .and.be.an.instanceof(TypeError) .and.have.property('message').includes('AbortSignal'), - expect(fetch(`${base}inspect`, {signal: Object.create(null)})) + expect(fetch(`${base}inspect`, { signal: Object.create(null) })) .to.be.eventually.rejected .and.be.an.instanceof(TypeError) .and.have.property('message').includes('AbortSignal') @@ -1286,7 +1285,7 @@ describe('node-fetch', () => { }); }); - itIf(process.platform !== 'win32')('should allow POST request with form-data using stream as body', () => { + if ('should allow POST request with form-data using stream as body', () => { const form = new FormData(); form.append('my_field', fs.createReadStream('test/utils/dummy.txt')); @@ -1359,7 +1358,7 @@ describe('node-fetch', () => { // Note that fetch simply calls tostring on an object const options = { method: 'POST', - body: {a: 1} + body: { a: 1 } }; return fetch(url, options).then(res => { return res.json(); @@ -1380,7 +1379,7 @@ describe('node-fetch', () => { it('constructing a Request with URLSearchParams as body should have a Content-Type', () => { const parameters = new URLSearchParams(); - const request = new Request(base, {method: 'POST', body: parameters}); + const request = new Request(base, { method: 'POST', body: parameters }); expect(request.headers.get('Content-Type')).to.equal('application/x-www-form-urlencoded;charset=UTF-8'); }); @@ -1395,7 +1394,7 @@ describe('node-fetch', () => { // Body should been cloned... it('constructing a Request/Response with URLSearchParams and mutating it should not affected body', () => { const parameters = new URLSearchParams(); - const request = new Request(`${base}inspect`, {method: 'POST', body: parameters}); + const request = new Request(`${base}inspect`, { method: 'POST', body: parameters }); parameters.append('a', '1'); return request.text().then(text => { expect(text).to.equal(''); @@ -1662,7 +1661,7 @@ describe('node-fetch', () => { return fetch(url).then(res => { const r1 = res.clone(); return Promise.all([res.json(), r1.text()]).then(results => { - expect(results[0]).to.deep.equal({name: 'value'}); + expect(results[0]).to.deep.equal({ name: 'value' }); expect(results[1]).to.equal('{"name":"value"}'); }); }); @@ -1673,7 +1672,7 @@ describe('node-fetch', () => { return fetch(url).then(res => { const r1 = res.clone(); return res.json().then(result => { - expect(result).to.deep.equal({name: 'value'}); + expect(result).to.deep.equal({ name: 'value' }); return r1.text().then(result => { expect(result).to.equal('{"name":"value"}'); }); @@ -1688,7 +1687,7 @@ describe('node-fetch', () => { return r1.text().then(result => { expect(result).to.equal('{"name":"value"}'); return res.json().then(result => { - expect(result).to.deep.equal({name: 'value'}); + expect(result).to.deep.equal({ name: 'value' }); }); }); }); @@ -1737,7 +1736,7 @@ describe('node-fetch', () => { res.end(crypto.randomBytes(firstPacketMaxSize + secondPacketSize)); }); return expect( - fetch(url, {highWaterMark: 10}).then(res => res.clone().buffer()) + fetch(url, { highWaterMark: 10 }).then(res => res.clone().buffer()) ).to.timeout; }); @@ -1761,7 +1760,7 @@ describe('node-fetch', () => { res.end(crypto.randomBytes(firstPacketMaxSize + secondPacketSize - 1)); }); return expect( - fetch(url, {highWaterMark: 10}).then(res => res.clone().buffer()) + fetch(url, { highWaterMark: 10 }).then(res => res.clone().buffer()) ).not.to.timeout; }); @@ -1771,7 +1770,7 @@ describe('node-fetch', () => { res.end(crypto.randomBytes((2 * 512 * 1024) - 1)); }); return expect( - fetch(url, {highWaterMark: 512 * 1024}).then(res => res.clone().buffer()) + fetch(url, { highWaterMark: 512 * 1024 }).then(res => res.clone().buffer()) ).not.to.timeout; }); @@ -1925,7 +1924,7 @@ describe('node-fetch', () => { method: 'POST', body: blob }); - }).then(res => res.json()).then(({body, headers}) => { + }).then(res => res.json()).then(({ body, headers }) => { expect(body).to.equal('world'); expect(headers['content-type']).to.equal(type); expect(headers['content-length']).to.equal(String(length)); @@ -1993,15 +1992,15 @@ describe('node-fetch', () => { // Issue #414 it('should reject if attempt to accumulate body stream throws', () => { - const res = new Response(stream.Readable.from((async function * () { + const res = new Response(stream.Readable.from((async function* () { yield Buffer.from('tada'); await new Promise(resolve => setTimeout(resolve, 200)); - yield {tada: 'yes'}; + yield { tada: 'yes' }; })())); return expect(res.text()).to.eventually.be.rejected .and.be.an.instanceOf(FetchError) - .and.include({type: 'system'}) + .and.include({ type: 'system' }) .and.have.property('message').that.include('Could not create Buffer'); }); @@ -2013,8 +2012,8 @@ describe('node-fetch', () => { return lookup(hostname, options, callback); } - const agent = http.Agent({lookup: lookupSpy}); - return fetch(url, {agent}).then(() => { + const agent = http.Agent({ lookup: lookupSpy }); + return fetch(url, { agent }).then(() => { expect(called).to.equal(2); }); }); @@ -2028,8 +2027,8 @@ describe('node-fetch', () => { return lookup(hostname, {}, callback); } - const agent = http.Agent({lookup: lookupSpy, family}); - return fetch(url, {agent}).then(() => { + const agent = http.Agent({ lookup: lookupSpy, family }); + return fetch(url, { agent }).then(() => { expect(families).to.have.length(2); expect(families[0]).to.equal(family); expect(families[1]).to.equal(family); @@ -2071,7 +2070,7 @@ describe('node-fetch', () => { size: 1024 }); - const blobBody = new Blob([bodyContent], {type: 'text/plain'}); + const blobBody = new Blob([bodyContent], { type: 'text/plain' }); const blobRequest = new Request(url, { method: 'POST', body: blobBody, From 56c19eac326ccc0534477239a7fabd730372cc1c Mon Sep 17 00:00:00 2001 From: Konstantin Vyatkin Date: Wed, 10 Jun 2020 10:47:38 -0400 Subject: [PATCH 156/157] use fieldName --- test/utils/server.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/utils/server.js b/test/utils/server.js index 3086d3cbc..cc9a9ab24 100644 --- a/test/utils/server.js +++ b/test/utils/server.js @@ -366,7 +366,7 @@ export default class TestServer { const busboy = new Busboy({headers: request.headers}); let body = ''; busboy.on('file', async (fieldName, file, fileName) => { - body += `file=${fileName}`; + body += `${fieldName}=${fileName}`; // consume file data // eslint-disable-next-line no-empty, no-unused-vars for await (const c of file) { } From d4f0fef43b4b2afa1eea388d8be75d864807dc0b Mon Sep 17 00:00:00 2001 From: Konstantin Vyatkin Date: Wed, 10 Jun 2020 10:47:51 -0400 Subject: [PATCH 157/157] don't ignore any tests on Windows --- test/main.js | 97 ++++++++++++++++++++++++++-------------------------- 1 file changed, 49 insertions(+), 48 deletions(-) diff --git a/test/main.js b/test/main.js index 49f4726b7..fd159a675 100644 --- a/test/main.js +++ b/test/main.js @@ -5,7 +5,7 @@ import http from 'http'; import fs from 'fs'; import stream from 'stream'; import path from 'path'; -import { lookup } from 'dns'; +import {lookup} from 'dns'; import vm from 'vm'; import chai from 'chai'; import chaiPromised from 'chai-as-promised'; @@ -18,7 +18,7 @@ import delay from 'delay'; import AbortControllerPolyfill from 'abortcontroller-polyfill/dist/abortcontroller.js'; import AbortController2 from 'abort-controller'; -const { AbortController } = AbortControllerPolyfill; +const {AbortController} = AbortControllerPolyfill; // Test subjects import Blob from 'fetch-blob'; @@ -29,11 +29,11 @@ import fetch, { Request, Response } from '../src/index.js'; -import { FetchError as FetchErrorOrig } from '../src/errors/fetch-error.js'; -import HeadersOrig, { fromRawHeaders } from '../src/headers.js'; +import {FetchError as FetchErrorOrig} from '../src/errors/fetch-error.js'; +import HeadersOrig, {fromRawHeaders} from '../src/headers.js'; import RequestOrig from '../src/request.js'; import ResponseOrig from '../src/response.js'; -import Body, { getTotalBytes, extractContentType } from '../src/body.js'; +import Body, {getTotalBytes, extractContentType} from '../src/body.js'; import TestServer from './utils/server.js'; const { @@ -46,7 +46,7 @@ chai.use(chaiPromised); chai.use(chaiIterator); chai.use(chaiString); chai.use(chaiTimeout); -const { expect } = chai; +const {expect} = chai; const local = new TestServer(); const base = `http://${local.hostname}:${local.port}/`; @@ -112,7 +112,7 @@ describe('node-fetch', () => { const url = 'http://localhost:50000/'; return expect(fetch(url)).to.eventually.be.rejected .and.be.an.instanceOf(FetchError) - .and.include({ type: 'system', code: 'ECONNREFUSED', errno: 'ECONNREFUSED' }); + .and.include({type: 'system', code: 'ECONNREFUSED', errno: 'ECONNREFUSED'}); }); it('error should contain system error if one occurred', () => { @@ -125,7 +125,8 @@ describe('node-fetch', () => { return expect(err).to.not.have.property('erroredSysCall'); }); - if ('system error is extracted from failed requests', () => { + it('system error is extracted from failed requests', function () { + this.timeout(5000); const url = 'http://localhost:50000/'; return expect(fetch(url)).to.eventually.be.rejected .and.be.an.instanceOf(FetchError) @@ -198,7 +199,7 @@ describe('node-fetch', () => { return res.json().then(result => { expect(res.bodyUsed).to.be.true; expect(result).to.be.an('object'); - expect(result).to.deep.equal({ name: 'value' }); + expect(result).to.deep.equal({name: 'value'}); }); }); }); @@ -206,7 +207,7 @@ describe('node-fetch', () => { it('should send request with custom headers', () => { const url = `${base}inspect`; const options = { - headers: { 'x-custom-header': 'abc' } + headers: {'x-custom-header': 'abc'} }; return fetch(url, options).then(res => { return res.json(); @@ -218,7 +219,7 @@ describe('node-fetch', () => { it('should accept headers instance', () => { const url = `${base}inspect`; const options = { - headers: new Headers({ 'x-custom-header': 'abc' }) + headers: new Headers({'x-custom-header': 'abc'}) }; return fetch(url, options).then(res => { return res.json(); @@ -479,7 +480,7 @@ describe('node-fetch', () => { it('should follow redirect code 301 and keep existing headers', () => { const url = `${base}redirect/301`; const options = { - headers: new Headers({ 'x-custom-header': 'abc' }) + headers: new Headers({'x-custom-header': 'abc'}) }; return fetch(url, options).then(res => { expect(res.url).to.equal(`${base}inspect`); @@ -832,8 +833,8 @@ describe('node-fetch', () => { const controller2 = new AbortController2(); const fetches = [ - fetch(`${base}timeout`, { signal: controller.signal }), - fetch(`${base}timeout`, { signal: controller2.signal }), + fetch(`${base}timeout`, {signal: controller.signal}), + fetch(`${base}timeout`, {signal: controller2.signal}), fetch( `${base}timeout`, { @@ -841,7 +842,7 @@ describe('node-fetch', () => { signal: controller.signal, headers: { 'Content-Type': 'application/json', - body: JSON.stringify({ hello: 'world' }) + body: JSON.stringify({hello: 'world'}) } } ) @@ -879,10 +880,10 @@ describe('node-fetch', () => { it('should remove internal AbortSignal event listener after request is aborted', () => { const controller = new AbortController(); - const { signal } = controller; + const {signal} = controller; const promise = fetch( `${base}timeout`, - { signal } + {signal} ); const result = expect(promise).to.eventually.be.rejected .and.be.an.instanceof(Error) @@ -924,11 +925,11 @@ describe('node-fetch', () => { it('should remove internal AbortSignal event listener after request and response complete without aborting', () => { const controller = new AbortController(); - const { signal } = controller; - const fetchHtml = fetch(`${base}html`, { signal }) + const {signal} = controller; + const fetchHtml = fetch(`${base}html`, {signal}) .then(res => res.text()); - const fetchResponseError = fetch(`${base}error/reset`, { signal }); - const fetchRedirect = fetch(`${base}redirect/301`, { signal }).then(res => res.json()); + const fetchResponseError = fetch(`${base}error/reset`, {signal}); + const fetchRedirect = fetch(`${base}redirect/301`, {signal}).then(res => res.json()); return Promise.all([ expect(fetchHtml).to.eventually.be.fulfilled.and.equal(''), expect(fetchResponseError).to.be.eventually.rejected, @@ -942,7 +943,7 @@ describe('node-fetch', () => { const controller = new AbortController(); return expect(fetch( `${base}slow`, - { signal: controller.signal } + {signal: controller.signal} )) .to.eventually.be.fulfilled .then(res => { @@ -959,7 +960,7 @@ describe('node-fetch', () => { const controller = new AbortController(); return expect(fetch( `${base}slow`, - { signal: controller.signal } + {signal: controller.signal} )) .to.eventually.be.fulfilled .then(res => { @@ -975,7 +976,7 @@ describe('node-fetch', () => { const controller = new AbortController(); expect(fetch( `${base}slow`, - { signal: controller.signal } + {signal: controller.signal} )) .to.eventually.be.fulfilled .then(res => { @@ -991,11 +992,11 @@ describe('node-fetch', () => { it('should cancel request body of type Stream with AbortError when aborted', () => { const controller = new AbortController(); - const body = new stream.Readable({ objectMode: true }); + const body = new stream.Readable({objectMode: true}); body._read = () => { }; const promise = fetch( `${base}slow`, - { signal: controller.signal, body, method: 'POST' } + {signal: controller.signal, body, method: 'POST'} ); const result = Promise.all([ @@ -1021,15 +1022,15 @@ describe('node-fetch', () => { it('should throw a TypeError if a signal is not of type AbortSignal', () => { return Promise.all([ - expect(fetch(`${base}inspect`, { signal: {} })) + expect(fetch(`${base}inspect`, {signal: {}})) .to.be.eventually.rejected .and.be.an.instanceof(TypeError) .and.have.property('message').includes('AbortSignal'), - expect(fetch(`${base}inspect`, { signal: '' })) + expect(fetch(`${base}inspect`, {signal: ''})) .to.be.eventually.rejected .and.be.an.instanceof(TypeError) .and.have.property('message').includes('AbortSignal'), - expect(fetch(`${base}inspect`, { signal: Object.create(null) })) + expect(fetch(`${base}inspect`, {signal: Object.create(null)})) .to.be.eventually.rejected .and.be.an.instanceof(TypeError) .and.have.property('message').includes('AbortSignal') @@ -1285,7 +1286,7 @@ describe('node-fetch', () => { }); }); - if ('should allow POST request with form-data using stream as body', () => { + it('should allow POST request with form-data using stream as body', () => { const form = new FormData(); form.append('my_field', fs.createReadStream('test/utils/dummy.txt')); @@ -1358,7 +1359,7 @@ describe('node-fetch', () => { // Note that fetch simply calls tostring on an object const options = { method: 'POST', - body: { a: 1 } + body: {a: 1} }; return fetch(url, options).then(res => { return res.json(); @@ -1379,7 +1380,7 @@ describe('node-fetch', () => { it('constructing a Request with URLSearchParams as body should have a Content-Type', () => { const parameters = new URLSearchParams(); - const request = new Request(base, { method: 'POST', body: parameters }); + const request = new Request(base, {method: 'POST', body: parameters}); expect(request.headers.get('Content-Type')).to.equal('application/x-www-form-urlencoded;charset=UTF-8'); }); @@ -1394,7 +1395,7 @@ describe('node-fetch', () => { // Body should been cloned... it('constructing a Request/Response with URLSearchParams and mutating it should not affected body', () => { const parameters = new URLSearchParams(); - const request = new Request(`${base}inspect`, { method: 'POST', body: parameters }); + const request = new Request(`${base}inspect`, {method: 'POST', body: parameters}); parameters.append('a', '1'); return request.text().then(text => { expect(text).to.equal(''); @@ -1661,7 +1662,7 @@ describe('node-fetch', () => { return fetch(url).then(res => { const r1 = res.clone(); return Promise.all([res.json(), r1.text()]).then(results => { - expect(results[0]).to.deep.equal({ name: 'value' }); + expect(results[0]).to.deep.equal({name: 'value'}); expect(results[1]).to.equal('{"name":"value"}'); }); }); @@ -1672,7 +1673,7 @@ describe('node-fetch', () => { return fetch(url).then(res => { const r1 = res.clone(); return res.json().then(result => { - expect(result).to.deep.equal({ name: 'value' }); + expect(result).to.deep.equal({name: 'value'}); return r1.text().then(result => { expect(result).to.equal('{"name":"value"}'); }); @@ -1687,7 +1688,7 @@ describe('node-fetch', () => { return r1.text().then(result => { expect(result).to.equal('{"name":"value"}'); return res.json().then(result => { - expect(result).to.deep.equal({ name: 'value' }); + expect(result).to.deep.equal({name: 'value'}); }); }); }); @@ -1736,7 +1737,7 @@ describe('node-fetch', () => { res.end(crypto.randomBytes(firstPacketMaxSize + secondPacketSize)); }); return expect( - fetch(url, { highWaterMark: 10 }).then(res => res.clone().buffer()) + fetch(url, {highWaterMark: 10}).then(res => res.clone().buffer()) ).to.timeout; }); @@ -1760,7 +1761,7 @@ describe('node-fetch', () => { res.end(crypto.randomBytes(firstPacketMaxSize + secondPacketSize - 1)); }); return expect( - fetch(url, { highWaterMark: 10 }).then(res => res.clone().buffer()) + fetch(url, {highWaterMark: 10}).then(res => res.clone().buffer()) ).not.to.timeout; }); @@ -1770,7 +1771,7 @@ describe('node-fetch', () => { res.end(crypto.randomBytes((2 * 512 * 1024) - 1)); }); return expect( - fetch(url, { highWaterMark: 512 * 1024 }).then(res => res.clone().buffer()) + fetch(url, {highWaterMark: 512 * 1024}).then(res => res.clone().buffer()) ).not.to.timeout; }); @@ -1924,7 +1925,7 @@ describe('node-fetch', () => { method: 'POST', body: blob }); - }).then(res => res.json()).then(({ body, headers }) => { + }).then(res => res.json()).then(({body, headers}) => { expect(body).to.equal('world'); expect(headers['content-type']).to.equal(type); expect(headers['content-length']).to.equal(String(length)); @@ -1992,15 +1993,15 @@ describe('node-fetch', () => { // Issue #414 it('should reject if attempt to accumulate body stream throws', () => { - const res = new Response(stream.Readable.from((async function* () { + const res = new Response(stream.Readable.from((async function * () { yield Buffer.from('tada'); await new Promise(resolve => setTimeout(resolve, 200)); - yield { tada: 'yes' }; + yield {tada: 'yes'}; })())); return expect(res.text()).to.eventually.be.rejected .and.be.an.instanceOf(FetchError) - .and.include({ type: 'system' }) + .and.include({type: 'system'}) .and.have.property('message').that.include('Could not create Buffer'); }); @@ -2012,8 +2013,8 @@ describe('node-fetch', () => { return lookup(hostname, options, callback); } - const agent = http.Agent({ lookup: lookupSpy }); - return fetch(url, { agent }).then(() => { + const agent = http.Agent({lookup: lookupSpy}); + return fetch(url, {agent}).then(() => { expect(called).to.equal(2); }); }); @@ -2027,8 +2028,8 @@ describe('node-fetch', () => { return lookup(hostname, {}, callback); } - const agent = http.Agent({ lookup: lookupSpy, family }); - return fetch(url, { agent }).then(() => { + const agent = http.Agent({lookup: lookupSpy, family}); + return fetch(url, {agent}).then(() => { expect(families).to.have.length(2); expect(families[0]).to.equal(family); expect(families[1]).to.equal(family); @@ -2070,7 +2071,7 @@ describe('node-fetch', () => { size: 1024 }); - const blobBody = new Blob([bodyContent], { type: 'text/plain' }); + const blobBody = new Blob([bodyContent], {type: 'text/plain'}); const blobRequest = new Request(url, { method: 'POST', body: blobBody,