Drop custom Promises and refactor to `async` functions (#845)

* refactor to async

* no custsom promises anymore

* restore server premature handler

* simplify

* fixing break

* lint

* remove promise dependency

* fix docs
This commit is contained in:
Konstantin Vyatkin 2020-05-28 17:57:57 -04:00 committed by GitHub
parent 966a4c3c78
commit 769f75d054
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 40 additions and 93 deletions

View File

@ -85,9 +85,9 @@ See Jason Miller's [isomorphic-unfetch](https://www.npmjs.com/package/isomorphic
- Stay consistent with `window.fetch` API. - Stay consistent with `window.fetch` API.
- Make conscious trade-off when following [WHATWG fetch spec][whatwg-fetch] and [stream spec](https://streams.spec.whatwg.org/) implementation details, document known differences. - Make conscious trade-off when following [WHATWG fetch spec][whatwg-fetch] and [stream spec](https://streams.spec.whatwg.org/) implementation details, document known differences.
- Use native promise, but allow substituting it with [insert your favorite promise library]. - Use native promise and async functions.
- Use native Node streams for body, on both request and response. - Use native Node streams for body, on both request and response.
- Decode content encoding (gzip/deflate) properly, and convert string output (such as `res.text()` and `res.json()`) to UTF-8 automatically. - Decode content encoding (gzip/deflate/brotli) properly, and convert string output (such as `res.text()` and `res.json()`) to UTF-8 automatically.
- Useful extensions such as redirect limit, response size limit, [explicit errors][error-handling.md] for troubleshooting. - Useful extensions such as redirect limit, response size limit, [explicit errors][error-handling.md] for troubleshooting.
## Difference from client-side fetch ## Difference from client-side fetch
@ -116,15 +116,6 @@ const fetch = require('node-fetch');
import fetch from 'node-fetch'; import fetch from '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;
```
If you want to patch the global object in node: If you want to patch the global object in node:
```js ```js

View File

@ -60,7 +60,6 @@
"mocha": "^7.1.2", "mocha": "^7.1.2",
"p-timeout": "^3.2.0", "p-timeout": "^3.2.0",
"parted": "^0.1.1", "parted": "^0.1.1",
"promise": "^8.1.0",
"resumer": "0.0.0", "resumer": "0.0.0",
"rollup": "^2.10.8", "rollup": "^2.10.8",
"string-to-arraybuffer": "^1.0.2", "string-to-arraybuffer": "^1.0.2",

View File

@ -5,7 +5,7 @@
* Body interface provides common methods for Request and Response * Body interface provides common methods for Request and Response
*/ */
import Stream, {finished, PassThrough} from 'stream'; import Stream, {PassThrough} from 'stream';
import {types} from 'util'; import {types} from 'util';
import Blob from 'fetch-blob'; import Blob from 'fetch-blob';
@ -148,22 +148,22 @@ Object.defineProperties(Body.prototype, {
* *
* @return Promise * @return Promise
*/ */
const consumeBody = data => { async function consumeBody(data) {
if (data[INTERNALS].disturbed) { if (data[INTERNALS].disturbed) {
return Body.Promise.reject(new TypeError(`body used already for: ${data.url}`)); throw new TypeError(`body used already for: ${data.url}`);
} }
data[INTERNALS].disturbed = true; data[INTERNALS].disturbed = true;
if (data[INTERNALS].error) { if (data[INTERNALS].error) {
return Body.Promise.reject(data[INTERNALS].error); throw data[INTERNALS].error;
} }
let {body} = data; let {body} = data;
// Body is null // Body is null
if (body === null) { if (body === null) {
return Body.Promise.resolve(Buffer.alloc(0)); return Buffer.alloc(0);
} }
// Body is blob // Body is blob
@ -173,61 +173,49 @@ const consumeBody = data => {
// Body is buffer // Body is buffer
if (Buffer.isBuffer(body)) { if (Buffer.isBuffer(body)) {
return Body.Promise.resolve(body); return body;
} }
/* c8 ignore next 3 */ /* c8 ignore next 3 */
if (!(body instanceof Stream)) { if (!(body instanceof Stream)) {
return Body.Promise.resolve(Buffer.alloc(0)); return Buffer.alloc(0);
} }
// Body is stream // Body is stream
// get ready to actually consume the body // get ready to actually consume the body
const accum = []; const accum = [];
let accumBytes = 0; let accumBytes = 0;
let abort = false;
return new Body.Promise((resolve, reject) => { try {
body.on('data', chunk => { for await (const chunk of body) {
if (abort || chunk === null) { if (data.size > 0 && accumBytes + chunk.length > data.size) {
return; const err = new FetchError(`content size at ${data.url} over limit: ${data.size}`, 'max-size');
} body.destroy(err);
throw err;
if (data.size && accumBytes + chunk.length > data.size) {
abort = true;
reject(new FetchError(`content size at ${data.url} over limit: ${data.size}`, 'max-size'));
return;
} }
accumBytes += chunk.length; accumBytes += chunk.length;
accum.push(chunk); accum.push(chunk);
}); }
} catch (error) {
if (isAbortError(error) || error instanceof FetchError) {
throw error;
} else {
// Other errors, such as incorrect content-encoding
throw new FetchError(`Invalid response body while trying to fetch ${data.url}: ${error.message}`, 'system', error);
}
}
finished(body, {writable: false}, err => { if (body.readableEnded === true || body._readableState.ended === true) {
if (err) { try {
if (isAbortError(err)) { return Buffer.concat(accum, accumBytes);
// If the request was aborted, reject with this Error } catch (error) {
abort = true; throw new FetchError(`Could not create Buffer from response body for ${data.url}: ${error.message}`, 'system', error);
reject(err); }
} else { } else {
// Other errors, such as incorrect content-encoding throw new FetchError(`Premature close of server response while trying to fetch ${data.url}`);
reject(new FetchError(`Invalid response body while trying to fetch ${data.url}: ${err.message}`, 'system', err)); }
} }
} else {
if (abort) {
return;
}
try {
resolve(Buffer.concat(accum, accumBytes));
} catch (error) {
// Handle streams that have accumulated too much data (issue #414)
reject(new FetchError(`Could not create Buffer from response body for ${data.url}: ${error.message}`, 'system', error));
}
}
});
});
};
/** /**
* Clone body given Res/Req instance * Clone body given Res/Req instance
@ -370,5 +358,3 @@ export const writeToStream = (dest, {body}) => {
} }
}; };
// Expose Promise
Body.Promise = global.Promise;

View File

@ -12,7 +12,7 @@ import zlib from 'zlib';
import Stream, {PassThrough, pipeline as pump} from 'stream'; import Stream, {PassThrough, pipeline as pump} from 'stream';
import dataURIToBuffer from 'data-uri-to-buffer'; import dataURIToBuffer from 'data-uri-to-buffer';
import Body, {writeToStream, getTotalBytes} from './body.js'; import {writeToStream, getTotalBytes} from './body.js';
import Response from './response.js'; import Response from './response.js';
import Headers, {fromRawHeaders} from './headers.js'; import Headers, {fromRawHeaders} from './headers.js';
import Request, {getNodeRequestOptions} from './request.js'; import Request, {getNodeRequestOptions} from './request.js';
@ -29,12 +29,7 @@ export {Headers, Request, Response, FetchError, AbortError, isRedirect};
* @param Object opts Fetch options * @param Object opts Fetch options
* @return Promise * @return Promise
*/ */
export default function fetch(url, options_) { export default async function fetch(url, options_) {
// Allow custom promise
if (!fetch.Promise) {
throw new Error('native promise missing, set fetch.Promise to your favorite alternative');
}
// Regex for data uri // Regex for data uri
const dataUriRegex = /^\s*data:([a-z]+\/[a-z]+(;[a-z-]+=[a-z-]+)?)?(;base64)?,[\w!$&',()*+;=\-.~:@/?%\s]*\s*$/i; const dataUriRegex = /^\s*data:([a-z]+\/[a-z]+(;[a-z-]+=[a-z-]+)?)?(;base64)?,[\w!$&',()*+;=\-.~:@/?%\s]*\s*$/i;
@ -42,19 +37,17 @@ export default function fetch(url, options_) {
if (dataUriRegex.test(url)) { if (dataUriRegex.test(url)) {
const data = dataURIToBuffer(url); const data = dataURIToBuffer(url);
const response = new Response(data, {headers: {'Content-Type': data.type}}); const response = new Response(data, {headers: {'Content-Type': data.type}});
return fetch.Promise.resolve(response); return response;
} }
// If invalid data uri // If invalid data uri
if (url.toString().startsWith('data:')) { if (url.toString().startsWith('data:')) {
const request = new Request(url, options_); const request = new Request(url, options_);
return fetch.Promise.reject(new FetchError(`[${request.method}] ${request.url} invalid URL`, 'system')); throw new FetchError(`[${request.method}] ${request.url} invalid URL`, 'system');
} }
Body.Promise = fetch.Promise;
// Wrap http.request into fetch // Wrap http.request into fetch
return new fetch.Promise((resolve, reject) => { return new Promise((resolve, reject) => {
// Build request object // Build request object
const request = new Request(url, options_); const request = new Request(url, options_);
const options = getNodeRequestOptions(request); const options = getNodeRequestOptions(request);
@ -290,5 +283,3 @@ export default function fetch(url, options_) {
}); });
} }
// Expose Promise
fetch.Promise = global.Promise;

View File

@ -10,7 +10,6 @@ import chai from 'chai';
import chaiPromised from 'chai-as-promised'; import chaiPromised from 'chai-as-promised';
import chaiIterator from 'chai-iterator'; import chaiIterator from 'chai-iterator';
import chaiString from 'chai-string'; import chaiString from 'chai-string';
import then from 'promise';
import resumer from 'resumer'; import resumer from 'resumer';
import FormData from 'form-data'; import FormData from 'form-data';
import stringToArrayBuffer from 'string-to-arraybuffer'; import stringToArrayBuffer from 'string-to-arraybuffer';
@ -77,29 +76,10 @@ describe('node-fetch', () => {
it('should return a promise', () => { it('should return a promise', () => {
const url = `${base}hello`; const url = `${base}hello`;
const p = fetch(url); const p = fetch(url);
expect(p).to.be.an.instanceof(fetch.Promise); expect(p).to.be.an.instanceof(Promise);
expect(p).to.have.property('then'); expect(p).to.have.property('then');
}); });
it('should allow custom promise', () => {
const url = `${base}hello`;
const old = fetch.Promise;
fetch.Promise = then;
expect(fetch(url)).to.be.an.instanceof(then);
expect(fetch(url)).to.not.be.an.instanceof(old);
fetch.Promise = old;
});
it('should throw error when no promise implementation are found', () => {
const url = `${base}hello`;
const old = fetch.Promise;
fetch.Promise = undefined;
expect(() => {
fetch(url);
}).to.throw(Error);
fetch.Promise = old;
});
it('should expose Headers, Response and Request constructors', () => { it('should expose Headers, Response and Request constructors', () => {
expect(FetchError).to.equal(FetchErrorOrig); expect(FetchError).to.equal(FetchErrorOrig);
expect(Headers).to.equal(HeadersOrig); expect(Headers).to.equal(HeadersOrig);