node-fetch/src/index.js

224 lines
6.4 KiB
JavaScript
Raw Normal View History

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
*/
import Body, { writeToStream, getTotalBytes } from './body';
import Response from './response';
import Headers, { createHeadersLenient } from './headers';
2016-11-23 13:39:35 -08:00
import Request, { getNodeRequestOptions } from './request';
import FetchError from './fetch-error';
2015-01-26 01:02:34 -08:00
const http = require('http');
const https = require('https');
const { PassThrough } = require('stream');
const { resolve: resolve_url } = require('url');
const zlib = require('zlib');
2015-01-26 01:02:34 -08: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
*/
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
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
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
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
const send = (options.protocol === 'https:' ? https : http).request;
2015-01-26 02:15:07 -08:00
// http.request only support string as host header, this hack make custom host header possible
if (options.headers.host) {
options.headers.host = options.headers.host[0];
}
2015-01-27 05:11:26 -08:00
// send request
const req = send(options);
let reqTimeout;
2015-01-27 05:11:26 -08:00
2016-11-23 13:39:35 -08:00
if (request.timeout) {
req.once('socket', socket => {
reqTimeout = setTimeout(() => {
2015-01-27 05:11:26 -08:00
req.abort();
2016-11-23 13:39:35 -08:00
reject(new FetchError(`network timeout at: ${request.url}`, 'request-timeout'));
}, request.timeout);
2015-02-01 04:36:17 -08:00
});
}
2015-01-26 02:15:07 -08:00
req.on('error', err => {
2015-04-16 09:01:29 -07:00
clearTimeout(reqTimeout);
2016-11-23 13:39:35 -08:00
reject(new FetchError(`request to ${request.url} failed, reason: ${err.message}`, 'system', err));
2015-01-26 05:28:23 -08:00
});
req.on('response', res => {
2015-04-16 09:01:29 -07:00
clearTimeout(reqTimeout);
const headers = createHeadersLenient(res.headers);
2015-01-26 09:46:32 -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'));
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) {
headers.set('Location', locationURL);
}
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'));
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,
body: request.body
};
// HTTP-redirect fetch step 9
if (res.statusCode !== 303 && request.body && getTotalBytes(request) === null) {
reject(new FetchError('Cannot follow redirect with body being a readable stream', 'unsupported-redirect'))
}
// 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)));
return;
}
2016-04-05 11:47:23 -07:00
}
// prepare response
let body = res.pipe(new PassThrough());
const response_options = {
2016-11-23 13:39:35 -08:00
url: request.url
2015-01-27 05:11:26 -08:00
, status: res.statusCode
2016-03-19 03:06:33 -07:00
, statusText: res.statusMessage
2015-01-27 05:11:26 -08:00
, headers: headers
2016-11-23 13:39:35 -08:00
, size: request.size
, timeout: request.timeout
};
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
// 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
// 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) {
resolve(new Response(body, response_options));
return;
}
// For Node v6+
// 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
};
// for gzip
2017-02-26 21:27:27 -08:00
if (codings == 'gzip' || codings == 'x-gzip') {
body = body.pipe(zlib.createGunzip(zlibOptions));
2017-02-26 21:27:27 -08:00
resolve(new Response(body, response_options));
return;
2017-02-26 21:27:27 -08:00
}
// for deflate
2017-02-26 21:27:27 -08:00
if (codings == 'deflate' || codings == 'x-deflate') {
// handle the infamous raw deflate response from old servers
// a hack for old IIS and Apache servers
const raw = res.pipe(new PassThrough());
raw.once('data', chunk => {
// see http://stackoverflow.com/questions/37519828
if ((chunk[0] & 0x0F) === 0x08) {
body = body.pipe(zlib.createInflate());
} else {
body = body.pipe(zlib.createInflateRaw());
}
2017-02-26 21:27:27 -08:00
resolve(new Response(body, response_options));
});
return;
}
2015-01-26 02:15:07 -08:00
// otherwise, use response as-is
2017-02-26 21:27:27 -08:00
resolve(new Response(body, response_options));
2015-01-26 02:15:07 -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
*/
fetch.isRedirect = code => code === 301 || code === 302 || code === 303 || code === 307 || code === 308;
2015-01-26 09:46:32 -08:00
// Needed for TypeScript.
fetch.default = fetch;
2015-01-26 02:15:07 -08:00
// expose Promise
fetch.Promise = global.Promise;
export {
Headers,
Request,
Response,
FetchError
};