node-fetch
Advanced tools
Comparing version 2.2.1 to 2.3.0
"use strict"; | ||
module.exports = exports = self.fetch; | ||
// ref: https://github.com/tc39/proposal-global | ||
var 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 = self.fetch.bind(self); | ||
exports.default = global.fetch.bind(global); | ||
exports.Headers = self.Headers; | ||
exports.Request = self.Request; | ||
exports.Response = self.Response; | ||
exports.Headers = global.Headers; | ||
exports.Request = global.Request; | ||
exports.Response = global.Response; |
@@ -8,2 +8,8 @@ | ||
## v2.3.0 | ||
- New: `AbortSignal` support, with README example. | ||
- Enhance: handle invalid `Location` header during redirect by rejecting them explicitly with `FetchError`. | ||
- Fix: update `browser.js` to support react-native environment, where `self` isn't available globally. | ||
## v2.2.1 | ||
@@ -10,0 +16,0 @@ |
@@ -181,3 +181,4 @@ process.emitWarning("The .es.js file is deprecated. Use .mjs instead."); | ||
body.on('error', function (err) { | ||
_this[INTERNALS].error = 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; | ||
}); | ||
@@ -370,5 +371,12 @@ } | ||
// handle stream error, such as incorrect content-encoding | ||
// handle stream errors | ||
_this4.body.on('error', function (err) { | ||
reject(new FetchError(`Invalid response body while trying to fetch ${_this4.url}: ${err.message}`, 'system', err)); | ||
if (err.name === 'AbortError') { | ||
// if the request was aborted, reject with this Error | ||
abort = true; | ||
reject(err); | ||
} else { | ||
// other errors, such as incorrect content-encoding | ||
reject(new FetchError(`Invalid response body while trying to fetch ${_this4.url}: ${err.message}`, 'system', err)); | ||
} | ||
}); | ||
@@ -1127,2 +1135,4 @@ | ||
const streamDestructionSupported = 'destroy' in Stream.Readable.prototype; | ||
/** | ||
@@ -1138,2 +1148,7 @@ * Check if a value is an instance of Request. | ||
function isAbortSignal(signal) { | ||
const proto = signal && typeof signal === 'object' && Object.getPrototypeOf(signal); | ||
return !!(proto && proto.constructor.name === 'AbortSignal'); | ||
} | ||
/** | ||
@@ -1191,2 +1206,9 @@ * Request class | ||
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'); | ||
} | ||
this[INTERNALS$2] = { | ||
@@ -1196,3 +1218,4 @@ method, | ||
headers, | ||
parsedURL | ||
parsedURL, | ||
signal | ||
}; | ||
@@ -1223,2 +1246,6 @@ | ||
get signal() { | ||
return this[INTERNALS$2].signal; | ||
} | ||
/** | ||
@@ -1248,3 +1275,4 @@ * Clone this request | ||
redirect: { enumerable: true }, | ||
clone: { enumerable: true } | ||
clone: { enumerable: true }, | ||
signal: { enumerable: true } | ||
}); | ||
@@ -1276,2 +1304,6 @@ | ||
if (request.signal && request.body instanceof Stream.Readable && !streamDestructionSupported) { | ||
throw new Error('Cancellation of streamed requests with AbortSignal is not supported in node < 8'); | ||
} | ||
// HTTP-network-or-cache fetch steps 2.4-2.7 | ||
@@ -1316,2 +1348,28 @@ let contentLengthValue = null; | ||
/** | ||
* abort-error.js | ||
* | ||
* AbortError interface for cancelled requests | ||
*/ | ||
/** | ||
* Create AbortError instance | ||
* | ||
* @param String message Error message for human | ||
* @return AbortError | ||
*/ | ||
function AbortError(message) { | ||
Error.call(this, message); | ||
this.type = 'aborted'; | ||
this.message = message; | ||
// 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'; | ||
// fix an issue where "PassThrough", "resolve" aren't a named export for node <10 | ||
@@ -1344,3 +1402,26 @@ const PassThrough$1 = Stream.PassThrough; | ||
const send = (options.protocol === 'https:' ? https : http).request; | ||
const signal = request.signal; | ||
let response = null; | ||
const abort = function abort() { | ||
let 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; | ||
response.body.emit('error', error); | ||
}; | ||
if (signal && signal.aborted) { | ||
abort(); | ||
return; | ||
} | ||
const abortAndFinalize = function abortAndFinalize() { | ||
abort(); | ||
finalize(); | ||
}; | ||
// send request | ||
@@ -1350,4 +1431,9 @@ const req = send(options); | ||
if (signal) { | ||
signal.addEventListener('abort', abortAndFinalize); | ||
} | ||
function finalize() { | ||
req.abort(); | ||
if (signal) signal.removeEventListener('abort', abortAndFinalize); | ||
clearTimeout(reqTimeout); | ||
@@ -1392,3 +1478,9 @@ } | ||
if (locationURL !== null) { | ||
headers.set('Location', locationURL); | ||
// handle corrupted header | ||
try { | ||
headers.set('Location', locationURL); | ||
} catch (err) { | ||
// istanbul ignore next: nodejs server prevent invalid response headers, we can't test this through normal request | ||
reject(err); | ||
} | ||
} | ||
@@ -1418,3 +1510,4 @@ break; | ||
method: request.method, | ||
body: request.body | ||
body: request.body, | ||
signal: request.signal | ||
}; | ||
@@ -1444,3 +1537,7 @@ | ||
// prepare response | ||
res.once('end', function () { | ||
if (signal) signal.removeEventListener('abort', abortAndFinalize); | ||
}); | ||
let body = res.pipe(new PassThrough$1()); | ||
const response_options = { | ||
@@ -1467,3 +1564,4 @@ url: request.url, | ||
if (!request.compress || request.method === 'HEAD' || codings === null || res.statusCode === 204 || res.statusCode === 304) { | ||
resolve(new Response(body, response_options)); | ||
response = new Response(body, response_options); | ||
resolve(response); | ||
return; | ||
@@ -1485,3 +1583,4 @@ } | ||
body = body.pipe(zlib.createGunzip(zlibOptions)); | ||
resolve(new Response(body, response_options)); | ||
response = new Response(body, response_options); | ||
resolve(response); | ||
return; | ||
@@ -1502,3 +1601,4 @@ } | ||
} | ||
resolve(new Response(body, response_options)); | ||
response = new Response(body, response_options); | ||
resolve(response); | ||
}); | ||
@@ -1509,3 +1609,4 @@ return; | ||
// otherwise, use response as-is | ||
resolve(new Response(body, response_options)); | ||
response = new Response(body, response_options); | ||
resolve(response); | ||
}); | ||
@@ -1512,0 +1613,0 @@ |
123
lib/index.js
@@ -185,3 +185,4 @@ 'use strict'; | ||
body.on('error', function (err) { | ||
_this[INTERNALS].error = 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; | ||
}); | ||
@@ -374,5 +375,12 @@ } | ||
// handle stream error, such as incorrect content-encoding | ||
// handle stream errors | ||
_this4.body.on('error', function (err) { | ||
reject(new FetchError(`Invalid response body while trying to fetch ${_this4.url}: ${err.message}`, 'system', err)); | ||
if (err.name === 'AbortError') { | ||
// if the request was aborted, reject with this Error | ||
abort = true; | ||
reject(err); | ||
} else { | ||
// other errors, such as incorrect content-encoding | ||
reject(new FetchError(`Invalid response body while trying to fetch ${_this4.url}: ${err.message}`, 'system', err)); | ||
} | ||
}); | ||
@@ -1131,2 +1139,4 @@ | ||
const streamDestructionSupported = 'destroy' in Stream.Readable.prototype; | ||
/** | ||
@@ -1142,2 +1152,7 @@ * Check if a value is an instance of Request. | ||
function isAbortSignal(signal) { | ||
const proto = signal && typeof signal === 'object' && Object.getPrototypeOf(signal); | ||
return !!(proto && proto.constructor.name === 'AbortSignal'); | ||
} | ||
/** | ||
@@ -1195,2 +1210,9 @@ * Request class | ||
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'); | ||
} | ||
this[INTERNALS$2] = { | ||
@@ -1200,3 +1222,4 @@ method, | ||
headers, | ||
parsedURL | ||
parsedURL, | ||
signal | ||
}; | ||
@@ -1227,2 +1250,6 @@ | ||
get signal() { | ||
return this[INTERNALS$2].signal; | ||
} | ||
/** | ||
@@ -1252,3 +1279,4 @@ * Clone this request | ||
redirect: { enumerable: true }, | ||
clone: { enumerable: true } | ||
clone: { enumerable: true }, | ||
signal: { enumerable: true } | ||
}); | ||
@@ -1280,2 +1308,6 @@ | ||
if (request.signal && request.body instanceof Stream.Readable && !streamDestructionSupported) { | ||
throw new Error('Cancellation of streamed requests with AbortSignal is not supported in node < 8'); | ||
} | ||
// HTTP-network-or-cache fetch steps 2.4-2.7 | ||
@@ -1320,2 +1352,28 @@ let contentLengthValue = null; | ||
/** | ||
* abort-error.js | ||
* | ||
* AbortError interface for cancelled requests | ||
*/ | ||
/** | ||
* Create AbortError instance | ||
* | ||
* @param String message Error message for human | ||
* @return AbortError | ||
*/ | ||
function AbortError(message) { | ||
Error.call(this, message); | ||
this.type = 'aborted'; | ||
this.message = message; | ||
// 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'; | ||
// fix an issue where "PassThrough", "resolve" aren't a named export for node <10 | ||
@@ -1348,3 +1406,26 @@ const PassThrough$1 = Stream.PassThrough; | ||
const send = (options.protocol === 'https:' ? https : http).request; | ||
const signal = request.signal; | ||
let response = null; | ||
const abort = function abort() { | ||
let 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; | ||
response.body.emit('error', error); | ||
}; | ||
if (signal && signal.aborted) { | ||
abort(); | ||
return; | ||
} | ||
const abortAndFinalize = function abortAndFinalize() { | ||
abort(); | ||
finalize(); | ||
}; | ||
// send request | ||
@@ -1354,4 +1435,9 @@ const req = send(options); | ||
if (signal) { | ||
signal.addEventListener('abort', abortAndFinalize); | ||
} | ||
function finalize() { | ||
req.abort(); | ||
if (signal) signal.removeEventListener('abort', abortAndFinalize); | ||
clearTimeout(reqTimeout); | ||
@@ -1396,3 +1482,9 @@ } | ||
if (locationURL !== null) { | ||
headers.set('Location', locationURL); | ||
// handle corrupted header | ||
try { | ||
headers.set('Location', locationURL); | ||
} catch (err) { | ||
// istanbul ignore next: nodejs server prevent invalid response headers, we can't test this through normal request | ||
reject(err); | ||
} | ||
} | ||
@@ -1422,3 +1514,4 @@ break; | ||
method: request.method, | ||
body: request.body | ||
body: request.body, | ||
signal: request.signal | ||
}; | ||
@@ -1448,3 +1541,7 @@ | ||
// prepare response | ||
res.once('end', function () { | ||
if (signal) signal.removeEventListener('abort', abortAndFinalize); | ||
}); | ||
let body = res.pipe(new PassThrough$1()); | ||
const response_options = { | ||
@@ -1471,3 +1568,4 @@ url: request.url, | ||
if (!request.compress || request.method === 'HEAD' || codings === null || res.statusCode === 204 || res.statusCode === 304) { | ||
resolve(new Response(body, response_options)); | ||
response = new Response(body, response_options); | ||
resolve(response); | ||
return; | ||
@@ -1489,3 +1587,4 @@ } | ||
body = body.pipe(zlib.createGunzip(zlibOptions)); | ||
resolve(new Response(body, response_options)); | ||
response = new Response(body, response_options); | ||
resolve(response); | ||
return; | ||
@@ -1506,3 +1605,4 @@ } | ||
} | ||
resolve(new Response(body, response_options)); | ||
response = new Response(body, response_options); | ||
resolve(response); | ||
}); | ||
@@ -1513,3 +1613,4 @@ return; | ||
// otherwise, use response as-is | ||
resolve(new Response(body, response_options)); | ||
response = new Response(body, response_options); | ||
resolve(response); | ||
}); | ||
@@ -1516,0 +1617,0 @@ |
{ | ||
"name": "node-fetch", | ||
"version": "2.2.1", | ||
"version": "2.3.0", | ||
"description": "A light-weight module that brings window.fetch to node.js", | ||
@@ -40,2 +40,4 @@ "main": "lib/index", | ||
"devDependencies": { | ||
"abort-controller": "^1.0.2", | ||
"abortcontroller-polyfill": "^1.1.9", | ||
"babel-core": "^6.26.0", | ||
@@ -42,0 +44,0 @@ "babel-plugin-istanbul": "^4.1.5", |
@@ -34,2 +34,3 @@ node-fetch | ||
- [Post with form-data (detect multipart)](#post-with-form-data-detect-multipart) | ||
- [Request cancellation with AbortSignal](#request-cancellation-with-abortsignal) | ||
- [API](#api) | ||
@@ -122,3 +123,3 @@ - [fetch(url[, options])](#fetchurl-options) | ||
fetch('https://httpbin.org/post', { | ||
fetch('https://httpbin.org/post', { | ||
method: 'post', | ||
@@ -253,2 +254,36 @@ body: JSON.stringify(body), | ||
#### 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 AbortContoller from 'abort-controller'; | ||
const controller = new AbortController(); | ||
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); | ||
}); | ||
``` | ||
See [test cases](https://github.com/bitinn/node-fetch/blob/master/test/test.js) for more examples. | ||
@@ -281,6 +316,7 @@ | ||
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) | ||
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 | ||
@@ -470,2 +506,9 @@ size: 0, // maximum response body size in bytes. 0 to disable | ||
<a id="class-aborterror"></a> | ||
### Class: AbortError | ||
<small>*(node-fetch extension)*</small> | ||
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. | ||
## Acknowledgement | ||
@@ -495,2 +538,2 @@ | ||
[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 | ||
[UPGRADE-GUIDE.md]: https://github.com/bitinn/node-fetch/blob/master/UPGRADE-GUIDE.md |
Sorry, the diff of this file is not supported yet
152599
4184
534
24