Skip to content

Commit 769f75d

Browse files
authored
Drop custom Promises and refactor to async functions (node-fetch#845)
* refactor to async * no custsom promises anymore * restore server premature handler * simplify * fixing break * lint * remove promise dependency * fix docs
1 parent 966a4c3 commit 769f75d

File tree

5 files changed

+40
-93
lines changed

5 files changed

+40
-93
lines changed

README.md

Lines changed: 2 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -85,9 +85,9 @@ See Jason Miller's [isomorphic-unfetch](https://www.npmjs.com/package/isomorphic
8585

8686
- Stay consistent with `window.fetch` API.
8787
- Make conscious trade-off when following [WHATWG fetch spec][whatwg-fetch] and [stream spec](https://streams.spec.whatwg.org/) implementation details, document known differences.
88-
- Use native promise, but allow substituting it with [insert your favorite promise library].
88+
- Use native promise and async functions.
8989
- Use native Node streams for body, on both request and response.
90-
- Decode content encoding (gzip/deflate) properly, and convert string output (such as `res.text()` and `res.json()`) to UTF-8 automatically.
90+
- Decode content encoding (gzip/deflate/brotli) properly, and convert string output (such as `res.text()` and `res.json()`) to UTF-8 automatically.
9191
- Useful extensions such as redirect limit, response size limit, [explicit errors][error-handling.md] for troubleshooting.
9292

9393
## Difference from client-side fetch
@@ -116,15 +116,6 @@ const fetch = require('node-fetch');
116116
import fetch from 'node-fetch';
117117
```
118118

119-
If you are using a Promise library other than native, set it through `fetch.Promise`:
120-
121-
```js
122-
const fetch = require('node-fetch');
123-
const Bluebird = require('bluebird');
124-
125-
fetch.Promise = Bluebird;
126-
```
127-
128119
If you want to patch the global object in node:
129120

130121
```js

package.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,6 @@
6060
"mocha": "^7.1.2",
6161
"p-timeout": "^3.2.0",
6262
"parted": "^0.1.1",
63-
"promise": "^8.1.0",
6463
"resumer": "0.0.0",
6564
"rollup": "^2.10.8",
6665
"string-to-arraybuffer": "^1.0.2",

src/body.js

Lines changed: 32 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
* Body interface provides common methods for Request and Response
66
*/
77

8-
import Stream, {finished, PassThrough} from 'stream';
8+
import Stream, {PassThrough} from 'stream';
99
import {types} from 'util';
1010

1111
import Blob from 'fetch-blob';
@@ -148,22 +148,22 @@ Object.defineProperties(Body.prototype, {
148148
*
149149
* @return Promise
150150
*/
151-
const consumeBody = data => {
151+
async function consumeBody(data) {
152152
if (data[INTERNALS].disturbed) {
153-
return Body.Promise.reject(new TypeError(`body used already for: ${data.url}`));
153+
throw new TypeError(`body used already for: ${data.url}`);
154154
}
155155

156156
data[INTERNALS].disturbed = true;
157157

158158
if (data[INTERNALS].error) {
159-
return Body.Promise.reject(data[INTERNALS].error);
159+
throw data[INTERNALS].error;
160160
}
161161

162162
let {body} = data;
163163

164164
// Body is null
165165
if (body === null) {
166-
return Body.Promise.resolve(Buffer.alloc(0));
166+
return Buffer.alloc(0);
167167
}
168168

169169
// Body is blob
@@ -173,61 +173,49 @@ const consumeBody = data => {
173173

174174
// Body is buffer
175175
if (Buffer.isBuffer(body)) {
176-
return Body.Promise.resolve(body);
176+
return body;
177177
}
178178

179179
/* c8 ignore next 3 */
180180
if (!(body instanceof Stream)) {
181-
return Body.Promise.resolve(Buffer.alloc(0));
181+
return Buffer.alloc(0);
182182
}
183183

184184
// Body is stream
185185
// get ready to actually consume the body
186186
const accum = [];
187187
let accumBytes = 0;
188-
let abort = false;
189188

190-
return new Body.Promise((resolve, reject) => {
191-
body.on('data', chunk => {
192-
if (abort || chunk === null) {
193-
return;
194-
}
195-
196-
if (data.size && accumBytes + chunk.length > data.size) {
197-
abort = true;
198-
reject(new FetchError(`content size at ${data.url} over limit: ${data.size}`, 'max-size'));
199-
return;
189+
try {
190+
for await (const chunk of body) {
191+
if (data.size > 0 && accumBytes + chunk.length > data.size) {
192+
const err = new FetchError(`content size at ${data.url} over limit: ${data.size}`, 'max-size');
193+
body.destroy(err);
194+
throw err;
200195
}
201196

202197
accumBytes += chunk.length;
203198
accum.push(chunk);
204-
});
199+
}
200+
} catch (error) {
201+
if (isAbortError(error) || error instanceof FetchError) {
202+
throw error;
203+
} else {
204+
// Other errors, such as incorrect content-encoding
205+
throw new FetchError(`Invalid response body while trying to fetch ${data.url}: ${error.message}`, 'system', error);
206+
}
207+
}
205208

206-
finished(body, {writable: false}, err => {
207-
if (err) {
208-
if (isAbortError(err)) {
209-
// If the request was aborted, reject with this Error
210-
abort = true;
211-
reject(err);
212-
} else {
213-
// Other errors, such as incorrect content-encoding
214-
reject(new FetchError(`Invalid response body while trying to fetch ${data.url}: ${err.message}`, 'system', err));
215-
}
216-
} else {
217-
if (abort) {
218-
return;
219-
}
220-
221-
try {
222-
resolve(Buffer.concat(accum, accumBytes));
223-
} catch (error) {
224-
// Handle streams that have accumulated too much data (issue #414)
225-
reject(new FetchError(`Could not create Buffer from response body for ${data.url}: ${error.message}`, 'system', error));
226-
}
227-
}
228-
});
229-
});
230-
};
209+
if (body.readableEnded === true || body._readableState.ended === true) {
210+
try {
211+
return Buffer.concat(accum, accumBytes);
212+
} catch (error) {
213+
throw new FetchError(`Could not create Buffer from response body for ${data.url}: ${error.message}`, 'system', error);
214+
}
215+
} else {
216+
throw new FetchError(`Premature close of server response while trying to fetch ${data.url}`);
217+
}
218+
}
231219

232220
/**
233221
* Clone body given Res/Req instance
@@ -370,5 +358,3 @@ export const writeToStream = (dest, {body}) => {
370358
}
371359
};
372360

373-
// Expose Promise
374-
Body.Promise = global.Promise;

src/index.js

Lines changed: 5 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import zlib from 'zlib';
1212
import Stream, {PassThrough, pipeline as pump} from 'stream';
1313
import dataURIToBuffer from 'data-uri-to-buffer';
1414

15-
import Body, {writeToStream, getTotalBytes} from './body.js';
15+
import {writeToStream, getTotalBytes} from './body.js';
1616
import Response from './response.js';
1717
import Headers, {fromRawHeaders} from './headers.js';
1818
import Request, {getNodeRequestOptions} from './request.js';
@@ -29,32 +29,25 @@ export {Headers, Request, Response, FetchError, AbortError, isRedirect};
2929
* @param Object opts Fetch options
3030
* @return Promise
3131
*/
32-
export default function fetch(url, options_) {
33-
// Allow custom promise
34-
if (!fetch.Promise) {
35-
throw new Error('native promise missing, set fetch.Promise to your favorite alternative');
36-
}
37-
32+
export default async function fetch(url, options_) {
3833
// Regex for data uri
3934
const dataUriRegex = /^\s*data:([a-z]+\/[a-z]+(;[a-z-]+=[a-z-]+)?)?(;base64)?,[\w!$&',()*+;=\-.~:@/?%\s]*\s*$/i;
4035

4136
// If valid data uri
4237
if (dataUriRegex.test(url)) {
4338
const data = dataURIToBuffer(url);
4439
const response = new Response(data, {headers: {'Content-Type': data.type}});
45-
return fetch.Promise.resolve(response);
40+
return response;
4641
}
4742

4843
// If invalid data uri
4944
if (url.toString().startsWith('data:')) {
5045
const request = new Request(url, options_);
51-
return fetch.Promise.reject(new FetchError(`[${request.method}] ${request.url} invalid URL`, 'system'));
46+
throw new FetchError(`[${request.method}] ${request.url} invalid URL`, 'system');
5247
}
5348

54-
Body.Promise = fetch.Promise;
55-
5649
// Wrap http.request into fetch
57-
return new fetch.Promise((resolve, reject) => {
50+
return new Promise((resolve, reject) => {
5851
// Build request object
5952
const request = new Request(url, options_);
6053
const options = getNodeRequestOptions(request);
@@ -290,5 +283,3 @@ export default function fetch(url, options_) {
290283
});
291284
}
292285

293-
// Expose Promise
294-
fetch.Promise = global.Promise;

test/main.js

Lines changed: 1 addition & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@ import chai from 'chai';
1010
import chaiPromised from 'chai-as-promised';
1111
import chaiIterator from 'chai-iterator';
1212
import chaiString from 'chai-string';
13-
import then from 'promise';
1413
import resumer from 'resumer';
1514
import FormData from 'form-data';
1615
import stringToArrayBuffer from 'string-to-arraybuffer';
@@ -77,29 +76,10 @@ describe('node-fetch', () => {
7776
it('should return a promise', () => {
7877
const url = `${base}hello`;
7978
const p = fetch(url);
80-
expect(p).to.be.an.instanceof(fetch.Promise);
79+
expect(p).to.be.an.instanceof(Promise);
8180
expect(p).to.have.property('then');
8281
});
8382

84-
it('should allow custom promise', () => {
85-
const url = `${base}hello`;
86-
const old = fetch.Promise;
87-
fetch.Promise = then;
88-
expect(fetch(url)).to.be.an.instanceof(then);
89-
expect(fetch(url)).to.not.be.an.instanceof(old);
90-
fetch.Promise = old;
91-
});
92-
93-
it('should throw error when no promise implementation are found', () => {
94-
const url = `${base}hello`;
95-
const old = fetch.Promise;
96-
fetch.Promise = undefined;
97-
expect(() => {
98-
fetch(url);
99-
}).to.throw(Error);
100-
fetch.Promise = old;
101-
});
102-
10383
it('should expose Headers, Response and Request constructors', () => {
10484
expect(FetchError).to.equal(FetchErrorOrig);
10585
expect(Headers).to.equal(HeadersOrig);

0 commit comments

Comments
 (0)