node-fetch/src/index.js

269 lines
8.0 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
2015-01-26 01:02:34 -08:00
*/
import {resolve as resolve_url} from 'url';
import * as http from 'http';
import * as https from 'https';
import * as zlib from 'zlib';
import {PassThrough} from 'stream';
import Body from './body';
import Response from './response';
import Headers from './headers';
import Request from './request';
import FetchError from './fetch-error';
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
*/
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;
2016-10-10 18:31:53 -07:00
Headers.FOLLOW_SPEC = fetch.FOLLOW_SPEC;
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
const options = new Request(url, opts);
2015-01-26 02:15:07 -08:00
if (!options.protocol || !options.hostname) {
throw new Error('only absolute urls are supported');
}
if (options.protocol !== 'http:' && options.protocol !== 'https:') {
throw new Error('only http(s) protocols are supported');
}
const send = (options.protocol === 'https:' ? https : http).request;
2015-01-26 02:15:07 -08:00
2015-01-27 05:11:26 -08:00
// normalize headers
const headers = new Headers(options.headers);
2015-01-27 05:11:26 -08:00
if (options.compress) {
headers.set('accept-encoding', 'gzip,deflate');
}
if (!headers.has('user-agent')) {
headers.set('user-agent', 'node-fetch/1.0 (+https://github.com/bitinn/node-fetch)');
}
if (!headers.has('connection') && !options.agent) {
2015-01-27 05:11:26 -08:00
headers.set('connection', 'close');
}
if (!headers.has('accept')) {
headers.set('accept', '*/*');
}
2016-05-25 11:00:25 -07:00
// bring node-fetch closer to browser behavior by setting content-length automatically
if (!headers.has('content-length') && /post|put|patch|delete/i.test(options.method)) {
if (typeof options.body === 'string') {
headers.set('content-length', Buffer.byteLength(options.body));
// detect form data input from form-data module, this hack avoid the need to add content-length header manually
} else if (options.body && typeof options.body.getLengthSync === 'function') {
// for form-data 1.x
if (options.body._lengthRetrievers && options.body._lengthRetrievers.length == 0) {
headers.set('content-length', options.body.getLengthSync().toString());
// for form-data 2.x
} else if (options.body.hasKnownLength && options.body.hasKnownLength()) {
headers.set('content-length', options.body.getLengthSync().toString());
}
// this is only necessary for older nodejs releases (before iojs merge)
} else if (options.body === undefined || options.body === null) {
headers.set('content-length', '0');
}
}
2015-01-27 05:11:26 -08:00
options.headers = headers.raw();
// 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
2015-02-01 04:36:17 -08:00
if (options.timeout) {
req.once('socket', socket => {
reqTimeout = setTimeout(() => {
2015-01-27 05:11:26 -08:00
req.abort();
reject(new FetchError(`network timeout at: ${options.url}`, 'request-timeout'));
2015-01-27 05:11:26 -08:00
}, options.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);
reject(new FetchError(`request to ${options.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);
2015-01-27 05:11:26 -08:00
// handle redirect
if (fetch.isRedirect(res.statusCode) && options.redirect !== 'manual') {
2016-04-05 11:47:23 -07:00
if (options.redirect === 'error') {
reject(new FetchError(`redirect mode is set to error: ${options.url}`, 'no-redirect'));
2016-04-05 11:47:23 -07:00
return;
}
2015-01-26 09:46:32 -08:00
if (options.counter >= options.follow) {
reject(new FetchError(`maximum redirect reached at: ${options.url}`, 'max-redirect'));
2015-01-27 07:33:06 -08:00
return;
2015-01-26 09:46:32 -08:00
}
if (!res.headers.location) {
reject(new FetchError(`redirect location header missing at: ${options.url}`, 'invalid-redirect'));
2015-01-27 07:33:06 -08:00
return;
2015-01-26 09:46:32 -08:00
}
// per fetch spec, for POST request with 301/302 response, or any request with 303 response, use GET when following redirect
if (res.statusCode === 303
|| ((res.statusCode === 301 || res.statusCode === 302) && options.method === 'POST'))
{
options.method = 'GET';
delete options.body;
delete options.headers['content-length'];
}
2015-01-27 07:33:06 -08:00
options.counter++;
resolve(fetch(resolve_url(options.url, res.headers.location), options));
2015-01-27 05:11:26 -08:00
return;
2015-01-26 09:46:32 -08:00
}
2016-04-05 11:47:23 -07:00
// normalize location header for manual redirect mode
const headers = new Headers();
for (const name of Object.keys(res.headers)) {
if (Array.isArray(res.headers[name])) {
for (const val of res.headers[name]) {
headers.append(name, val);
}
} else {
headers.append(name, res.headers[name]);
}
}
2016-04-12 11:58:04 -07:00
if (options.redirect === 'manual' && headers.has('location')) {
2016-04-05 11:47:23 -07:00
headers.set('location', resolve_url(options.url, headers.get('location')));
}
// prepare response
let body = res.pipe(new PassThrough());
const response_options = {
2015-06-03 21:05:01 -07:00
url: options.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
2015-01-27 09:00:53 -08:00
, size: options.size
, timeout: options.timeout
};
// response object
let output;
// in following scenarios we ignore compression support
// 1. compression support is disabled
// 2. HEAD request
// 3. no content-encoding header
// 4. no content response (204)
// 5. content not modified response (304)
if (!options.compress || options.method === 'HEAD' || !headers.has('content-encoding') || res.statusCode === 204 || res.statusCode === 304) {
output = new Response(body, response_options);
resolve(output);
return;
}
// otherwise, check for gzip or deflate
let name = headers.get('content-encoding');
// for gzip
if (name == 'gzip' || name == 'x-gzip') {
body = body.pipe(zlib.createGunzip());
output = new Response(body, response_options);
resolve(output);
return;
// for deflate
} else if (name == 'deflate' || name == '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());
}
output = new Response(body, response_options);
resolve(output);
});
return;
}
2015-01-26 02:15:07 -08:00
// otherwise, use response as-is
output = new Response(body, response_options);
2015-01-26 05:28:23 -08:00
resolve(output);
return;
2015-01-26 02:15:07 -08:00
});
2016-08-02 22:07:47 -07:00
// accept string, buffer or readable stream as body
2016-09-11 07:33:22 -07:00
// per spec we will call tostring on non-stream objects
2015-01-26 09:46:32 -08:00
if (typeof options.body === 'string') {
req.write(options.body);
req.end();
2016-08-02 22:07:47 -07:00
} else if (options.body instanceof Buffer) {
req.write(options.body);
req.end()
2015-01-27 07:33:06 -08:00
} else if (typeof options.body === 'object' && options.body.pipe) {
2015-01-26 09:46:32 -08:00
options.body.pipe(req);
2016-09-11 07:33:22 -07:00
} else if (typeof options.body === 'object') {
req.write(options.body.toString());
req.end();
2015-01-26 09:46:32 -08:00
} else {
req.end();
}
2015-01-26 02:15:07 -08:00
});
2015-01-26 01:02:34 -08:00
};
module.exports = fetch;
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
2015-01-26 02:15:07 -08:00
// expose Promise
fetch.Promise = global.Promise;
2016-10-10 18:31:53 -07:00
/**
* Option to make newly constructed Headers objects conformant to the
* **latest** version of the Fetch Standard. Note, that most other
* implementations of fetch() have not yet been updated to the latest
* version, so enabling this option almost certainly breaks any isomorphic
* attempt. Also, changing this variable will only affect new Headers
* objects; existing objects are not affected.
*/
fetch.FOLLOW_SPEC = false;
fetch.Response = Response;
fetch.Headers = Headers;
fetch.Request = Request;