2015-01-26 01:02:34 -08:00
|
|
|
|
|
|
|
/**
|
|
|
|
* index.js
|
|
|
|
*
|
2015-01-27 05:11:26 -08:00
|
|
|
* a request API compatible with window.fetch
|
2018-03-04 19:38:57 -08:00
|
|
|
*
|
|
|
|
* All spec algorithm step numbers are based on https://fetch.spec.whatwg.org/commit-snapshots/ae716822cb3a61843226cd090eefc6589446c1d2/.
|
2015-01-26 01:02:34 -08:00
|
|
|
*/
|
|
|
|
|
2018-11-05 01:42:51 -08:00
|
|
|
import Url from 'url';
|
2018-07-22 09:58:03 -07:00
|
|
|
import http from 'http';
|
|
|
|
import https from 'https';
|
|
|
|
import zlib from 'zlib';
|
2018-11-05 01:42:51 -08:00
|
|
|
import Stream from 'stream';
|
2018-07-22 09:58:03 -07:00
|
|
|
|
2018-03-04 19:29:59 -08:00
|
|
|
import Body, { writeToStream, getTotalBytes } from './body';
|
2016-10-10 11:50:04 -07:00
|
|
|
import Response from './response';
|
2018-03-04 13:12:36 -08:00
|
|
|
import Headers, { createHeadersLenient } from './headers';
|
2016-11-23 13:39:35 -08:00
|
|
|
import Request, { getNodeRequestOptions } from './request';
|
2016-10-10 11:50:04 -07:00
|
|
|
import FetchError from './fetch-error';
|
2018-11-12 20:40:11 -08:00
|
|
|
import AbortError from './abort-error';
|
2015-01-26 01:02:34 -08:00
|
|
|
|
2018-11-05 01:42:51 -08:00
|
|
|
// fix an issue where "PassThrough", "resolve" aren't a named export for node <10
|
|
|
|
const PassThrough = Stream.PassThrough;
|
|
|
|
const resolve_url = Url.resolve;
|
|
|
|
|
2015-01-26 01:02:34 -08:00
|
|
|
/**
|
2016-10-10 11:50:04 -07:00
|
|
|
* Fetch function
|
2015-01-26 01:02:34 -08:00
|
|
|
*
|
2015-06-03 21:05:01 -07:00
|
|
|
* @param Mixed url Absolute url or Request instance
|
2015-01-26 01:02:34 -08:00
|
|
|
* @param Object opts Fetch options
|
|
|
|
* @return Promise
|
|
|
|
*/
|
2017-01-14 20:50:10 -08:00
|
|
|
export default function fetch(url, opts) {
|
2015-01-26 01:02:34 -08:00
|
|
|
|
2015-01-27 05:11:26 -08:00
|
|
|
// allow custom promise
|
2016-10-10 11:50:04 -07:00
|
|
|
if (!fetch.Promise) {
|
|
|
|
throw new Error('native promise missing, set fetch.Promise to your favorite alternative');
|
2015-01-26 02:15:07 -08:00
|
|
|
}
|
2015-01-26 01:02:34 -08:00
|
|
|
|
2016-10-10 11:50:04 -07:00
|
|
|
Body.Promise = fetch.Promise;
|
2015-01-26 09:46:32 -08:00
|
|
|
|
2015-01-27 05:11:26 -08:00
|
|
|
// wrap http.request into fetch
|
2016-10-10 11:50:04 -07:00
|
|
|
return new fetch.Promise((resolve, reject) => {
|
2015-06-03 21:05:01 -07:00
|
|
|
// build request object
|
2016-11-23 13:39:35 -08:00
|
|
|
const request = new Request(url, opts);
|
|
|
|
const options = getNodeRequestOptions(request);
|
2015-01-26 02:15:07 -08:00
|
|
|
|
2017-04-08 18:33:46 -07:00
|
|
|
const send = (options.protocol === 'https:' ? https : http).request;
|
2018-11-12 20:40:11 -08:00
|
|
|
const { signal } = request;
|
|
|
|
let response = null;
|
|
|
|
|
|
|
|
const 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 = () => {
|
|
|
|
abort();
|
|
|
|
finalize();
|
|
|
|
}
|
2015-01-26 02:15:07 -08:00
|
|
|
|
2015-01-27 05:11:26 -08:00
|
|
|
// send request
|
2016-10-10 11:50:04 -07:00
|
|
|
const req = send(options);
|
|
|
|
let reqTimeout;
|
2015-01-27 05:11:26 -08:00
|
|
|
|
2018-11-12 20:40:11 -08:00
|
|
|
if (signal) {
|
|
|
|
signal.addEventListener('abort', abortAndFinalize);
|
|
|
|
}
|
|
|
|
|
2018-03-24 20:48:33 -07:00
|
|
|
function finalize() {
|
|
|
|
req.abort();
|
2018-11-12 20:40:11 -08:00
|
|
|
if (signal) signal.removeEventListener('abort', abortAndFinalize);
|
2018-03-24 20:48:33 -07:00
|
|
|
clearTimeout(reqTimeout);
|
|
|
|
}
|
|
|
|
|
2016-11-23 13:39:35 -08:00
|
|
|
if (request.timeout) {
|
2016-10-10 11:50:04 -07:00
|
|
|
req.once('socket', socket => {
|
|
|
|
reqTimeout = setTimeout(() => {
|
2016-11-23 13:39:35 -08:00
|
|
|
reject(new FetchError(`network timeout at: ${request.url}`, 'request-timeout'));
|
2018-03-24 20:48:33 -07:00
|
|
|
finalize();
|
2016-11-23 13:39:35 -08:00
|
|
|
}, request.timeout);
|
2015-02-01 04:36:17 -08:00
|
|
|
});
|
|
|
|
}
|
2015-01-26 02:15:07 -08:00
|
|
|
|
2016-10-10 11:50:04 -07:00
|
|
|
req.on('error', err => {
|
2016-11-23 13:39:35 -08:00
|
|
|
reject(new FetchError(`request to ${request.url} failed, reason: ${err.message}`, 'system', err));
|
2018-03-24 20:48:33 -07:00
|
|
|
finalize();
|
2015-01-26 05:28:23 -08:00
|
|
|
});
|
|
|
|
|
2016-10-10 11:50:04 -07:00
|
|
|
req.on('response', res => {
|
2015-04-16 09:01:29 -07:00
|
|
|
clearTimeout(reqTimeout);
|
|
|
|
|
2018-03-04 19:29:59 -08:00
|
|
|
const headers = createHeadersLenient(res.headers);
|
2015-01-26 09:46:32 -08:00
|
|
|
|
2018-03-04 19:29:59 -08:00
|
|
|
// HTTP fetch step 5
|
|
|
|
if (fetch.isRedirect(res.statusCode)) {
|
|
|
|
// HTTP fetch step 5.2
|
|
|
|
const location = headers.get('Location');
|
|
|
|
|
|
|
|
// HTTP fetch step 5.3
|
|
|
|
const locationURL = location === null ? null : resolve_url(request.url, location);
|
|
|
|
|
|
|
|
// HTTP fetch step 5.5
|
|
|
|
switch (request.redirect) {
|
|
|
|
case 'error':
|
|
|
|
reject(new FetchError(`redirect mode is set to error: ${request.url}`, 'no-redirect'));
|
2018-03-24 20:48:33 -07:00
|
|
|
finalize();
|
2018-03-04 19:29:59 -08:00
|
|
|
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.
|
|
|
|
if (locationURL !== null) {
|
2018-11-12 22:35:09 -08:00
|
|
|
// 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);
|
|
|
|
}
|
2018-03-04 19:29:59 -08:00
|
|
|
}
|
|
|
|
break;
|
|
|
|
case 'follow':
|
|
|
|
// HTTP-redirect fetch step 2
|
|
|
|
if (locationURL === null) {
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
|
|
|
|
// HTTP-redirect fetch step 5
|
|
|
|
if (request.counter >= request.follow) {
|
|
|
|
reject(new FetchError(`maximum redirect reached at: ${request.url}`, 'max-redirect'));
|
2018-03-24 20:48:33 -07:00
|
|
|
finalize();
|
2018-03-04 19:29:59 -08:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
// HTTP-redirect fetch step 6 (counter increment)
|
|
|
|
// Create a new Request object.
|
|
|
|
const requestOpts = {
|
|
|
|
headers: new Headers(request.headers),
|
|
|
|
follow: request.follow,
|
|
|
|
counter: request.counter + 1,
|
|
|
|
agent: request.agent,
|
|
|
|
compress: request.compress,
|
|
|
|
method: request.method,
|
2018-11-12 20:40:11 -08:00
|
|
|
body: request.body,
|
|
|
|
signal: request.signal,
|
2019-04-26 09:46:53 -07:00
|
|
|
timeout: request.timeout
|
2018-03-04 19:29:59 -08:00
|
|
|
};
|
|
|
|
|
|
|
|
// HTTP-redirect fetch step 9
|
|
|
|
if (res.statusCode !== 303 && request.body && getTotalBytes(request) === null) {
|
2018-03-24 20:48:33 -07:00
|
|
|
reject(new FetchError('Cannot follow redirect with body being a readable stream', 'unsupported-redirect'));
|
|
|
|
finalize();
|
|
|
|
return;
|
2018-03-04 19:29:59 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
// HTTP-redirect fetch step 11
|
|
|
|
if (res.statusCode === 303 || ((res.statusCode === 301 || res.statusCode === 302) && request.method === 'POST')) {
|
|
|
|
requestOpts.method = 'GET';
|
|
|
|
requestOpts.body = undefined;
|
|
|
|
requestOpts.headers.delete('content-length');
|
|
|
|
}
|
|
|
|
|
|
|
|
// HTTP-redirect fetch step 15
|
|
|
|
resolve(fetch(new Request(locationURL, requestOpts)));
|
2018-03-24 20:48:33 -07:00
|
|
|
finalize();
|
2018-03-04 19:29:59 -08:00
|
|
|
return;
|
2015-09-28 07:30:41 -07:00
|
|
|
}
|
2016-04-05 11:47:23 -07:00
|
|
|
}
|
|
|
|
|
2016-08-03 02:31:46 -07:00
|
|
|
// prepare response
|
2018-11-12 20:40:11 -08:00
|
|
|
res.once('end', () => {
|
|
|
|
if (signal) signal.removeEventListener('abort', abortAndFinalize);
|
|
|
|
});
|
2016-10-10 11:50:04 -07:00
|
|
|
let body = res.pipe(new PassThrough());
|
2018-11-12 20:40:11 -08:00
|
|
|
|
2016-10-10 11:50:04 -07:00
|
|
|
const response_options = {
|
2018-03-04 20:29:12 -08:00
|
|
|
url: request.url,
|
|
|
|
status: res.statusCode,
|
|
|
|
statusText: res.statusMessage,
|
|
|
|
headers: headers,
|
|
|
|
size: request.size,
|
2019-04-30 22:05:32 -07:00
|
|
|
timeout: request.timeout,
|
|
|
|
counter: request.counter
|
2016-08-03 02:31:46 -07:00
|
|
|
};
|
|
|
|
|
2018-03-04 19:38:57 -08:00
|
|
|
// HTTP-network fetch step 12.1.1.3
|
2017-02-26 21:27:27 -08:00
|
|
|
const codings = headers.get('Content-Encoding');
|
|
|
|
|
2018-03-04 19:38:57 -08:00
|
|
|
// HTTP-network fetch step 12.1.1.4: handle content codings
|
2016-08-03 02:31:46 -07:00
|
|
|
|
|
|
|
// in following scenarios we ignore compression support
|
|
|
|
// 1. compression support is disabled
|
|
|
|
// 2. HEAD request
|
2017-02-26 21:27:27 -08:00
|
|
|
// 3. no Content-Encoding header
|
2016-08-03 02:31:46 -07:00
|
|
|
// 4. no content response (204)
|
|
|
|
// 5. content not modified response (304)
|
2017-02-26 21:27:27 -08:00
|
|
|
if (!request.compress || request.method === 'HEAD' || codings === null || res.statusCode === 204 || res.statusCode === 304) {
|
2018-11-12 20:40:11 -08:00
|
|
|
response = new Response(body, response_options);
|
|
|
|
resolve(response);
|
2016-08-03 02:31:46 -07:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2017-05-15 04:45:04 -07:00
|
|
|
// For Node v6+
|
2017-02-22 00:05:55 -08:00
|
|
|
// Be less strict when decoding compressed responses, since sometimes
|
|
|
|
// servers send slightly invalid responses that are still accepted
|
|
|
|
// by common browsers.
|
|
|
|
// Always using Z_SYNC_FLUSH is what cURL does.
|
|
|
|
const zlibOptions = {
|
|
|
|
flush: zlib.Z_SYNC_FLUSH,
|
|
|
|
finishFlush: zlib.Z_SYNC_FLUSH
|
|
|
|
};
|
|
|
|
|
2016-08-03 02:31:46 -07:00
|
|
|
// for gzip
|
2017-02-26 21:27:27 -08:00
|
|
|
if (codings == 'gzip' || codings == 'x-gzip') {
|
2017-02-22 00:05:55 -08:00
|
|
|
body = body.pipe(zlib.createGunzip(zlibOptions));
|
2018-11-12 20:40:11 -08:00
|
|
|
response = new Response(body, response_options);
|
|
|
|
resolve(response);
|
2016-08-03 02:31:46 -07:00
|
|
|
return;
|
2017-02-26 21:27:27 -08:00
|
|
|
}
|
2016-08-03 02:31:46 -07:00
|
|
|
|
|
|
|
// for deflate
|
2017-02-26 21:27:27 -08:00
|
|
|
if (codings == 'deflate' || codings == 'x-deflate') {
|
2016-08-03 02:31:46 -07:00
|
|
|
// handle the infamous raw deflate response from old servers
|
|
|
|
// a hack for old IIS and Apache servers
|
2016-10-10 11:50:04 -07:00
|
|
|
const raw = res.pipe(new PassThrough());
|
|
|
|
raw.once('data', chunk => {
|
2016-08-03 02:31:46 -07:00
|
|
|
// see http://stackoverflow.com/questions/37519828
|
|
|
|
if ((chunk[0] & 0x0F) === 0x08) {
|
2017-05-15 04:45:04 -07:00
|
|
|
body = body.pipe(zlib.createInflate());
|
2016-08-03 02:31:46 -07:00
|
|
|
} else {
|
2017-05-15 04:45:04 -07:00
|
|
|
body = body.pipe(zlib.createInflateRaw());
|
2016-08-03 02:31:46 -07:00
|
|
|
}
|
2018-11-12 20:40:11 -08:00
|
|
|
response = new Response(body, response_options);
|
|
|
|
resolve(response);
|
2016-08-03 02:31:46 -07:00
|
|
|
});
|
|
|
|
return;
|
|
|
|
}
|
2015-01-26 02:15:07 -08:00
|
|
|
|
2019-04-26 09:20:15 -07:00
|
|
|
// for br
|
|
|
|
if (codings == 'br' && typeof zlib.createBrotliDecompress === 'function') {
|
|
|
|
body = body.pipe(zlib.createBrotliDecompress());
|
|
|
|
response = new Response(body, response_options);
|
|
|
|
resolve(response);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2016-08-03 02:31:46 -07:00
|
|
|
// otherwise, use response as-is
|
2018-11-12 20:40:11 -08:00
|
|
|
response = new Response(body, response_options);
|
|
|
|
resolve(response);
|
2015-01-26 02:15:07 -08:00
|
|
|
});
|
|
|
|
|
2016-12-05 20:25:13 -08:00
|
|
|
writeToStream(req, request);
|
2015-01-26 02:15:07 -08:00
|
|
|
});
|
2015-01-26 01:02:34 -08:00
|
|
|
|
|
|
|
};
|
|
|
|
|
2015-01-26 09:46:32 -08:00
|
|
|
/**
|
2015-01-27 05:11:26 -08:00
|
|
|
* Redirect code matching
|
2015-01-26 09:46:32 -08:00
|
|
|
*
|
|
|
|
* @param Number code Status code
|
|
|
|
* @return Boolean
|
|
|
|
*/
|
2016-10-10 11:50:04 -07:00
|
|
|
fetch.isRedirect = code => code === 301 || code === 302 || code === 303 || code === 307 || code === 308;
|
2015-01-26 09:46:32 -08:00
|
|
|
|
2015-01-26 02:15:07 -08:00
|
|
|
// expose Promise
|
2016-10-10 11:50:04 -07:00
|
|
|
fetch.Promise = global.Promise;
|
2017-01-14 20:50:10 -08:00
|
|
|
export {
|
|
|
|
Headers,
|
|
|
|
Request,
|
2017-02-26 16:45:11 -08:00
|
|
|
Response,
|
|
|
|
FetchError
|
2017-01-14 20:50:10 -08:00
|
|
|
};
|