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:
parent
966a4c3c78
commit
769f75d054
13
README.md
13
README.md
|
@ -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
|
||||||
|
|
|
@ -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",
|
||||||
|
|
78
src/body.js
78
src/body.js
|
@ -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;
|
|
||||||
|
|
19
src/index.js
19
src/index.js
|
@ -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;
|
|
||||||
|
|
22
test/main.js
22
test/main.js
|
@ -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);
|
||||||
|
|
Loading…
Reference in New Issue