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
|
|
|
*/
|
|
|
|
|
2016-10-10 11:50:04 -07: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';
|
2016-10-08 20:51:01 -07:00
|
|
|
import Headers 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';
|
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
|
|
|
|
*/
|
2016-10-10 11:50:04 -07:00
|
|
|
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;
|
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
|
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
|
|
|
|
2016-05-25 11:19:16 -07: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');
|
|
|
|
}
|
|
|
|
|
2016-10-10 11:50:04 -07:00
|
|
|
const send = (options.protocol === 'https:' ? https : http).request;
|
2015-01-26 02:15:07 -08:00
|
|
|
|
2015-07-11 04:38:26 -07:00
|
|
|
// http.request only support string as host header, this hack make custom host header possible
|
2015-07-11 02:36:58 -07:00
|
|
|
if (options.headers.host) {
|
|
|
|
options.headers.host = options.headers.host[0];
|
|
|
|
}
|
|
|
|
|
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
|
|
|
|
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(() => {
|
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
|
|
|
|
2016-10-10 11:50:04 -07: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
|
|
|
});
|
|
|
|
|
2016-10-10 11:50:04 -07: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
|
2016-11-23 13:39:35 -08:00
|
|
|
if (fetch.isRedirect(res.statusCode) && request.redirect !== 'manual') {
|
|
|
|
if (request.redirect === 'error') {
|
|
|
|
reject(new FetchError(`redirect mode is set to error: ${request.url}`, 'no-redirect'));
|
2016-04-05 11:47:23 -07:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2016-11-23 13:39:35 -08:00
|
|
|
if (request.counter >= request.follow) {
|
|
|
|
reject(new FetchError(`maximum redirect reached at: ${request.url}`, 'max-redirect'));
|
2015-01-27 07:33:06 -08:00
|
|
|
return;
|
2015-01-26 09:46:32 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
if (!res.headers.location) {
|
2016-11-23 13:39:35 -08:00
|
|
|
reject(new FetchError(`redirect location header missing at: ${request.url}`, 'invalid-redirect'));
|
2015-01-27 07:33:06 -08:00
|
|
|
return;
|
2015-01-26 09:46:32 -08:00
|
|
|
}
|
|
|
|
|
2015-09-28 07:30:41 -07: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
|
2016-11-23 13:39:35 -08:00
|
|
|
|| ((res.statusCode === 301 || res.statusCode === 302) && request.method === 'POST'))
|
2015-09-28 07:30:41 -07:00
|
|
|
{
|
2016-11-23 13:39:35 -08:00
|
|
|
request.method = 'GET';
|
|
|
|
request.body = null;
|
|
|
|
request.headers.delete('content-length');
|
2015-09-28 07:30:41 -07:00
|
|
|
}
|
|
|
|
|
2016-11-23 13:39:35 -08:00
|
|
|
request.counter++;
|
2015-01-27 07:33:06 -08:00
|
|
|
|
2016-11-23 13:39:35 -08:00
|
|
|
resolve(fetch(resolve_url(request.url, res.headers.location), request));
|
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
|
2016-10-10 14:12:57 -07:00
|
|
|
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-11-23 13:39:35 -08:00
|
|
|
if (request.redirect === 'manual' && headers.has('location')) {
|
|
|
|
headers.set('location', resolve_url(request.url, headers.get('location')));
|
2016-04-05 11:47:23 -07:00
|
|
|
}
|
|
|
|
|
2016-08-03 02:31:46 -07:00
|
|
|
// prepare response
|
2016-10-10 11:50:04 -07:00
|
|
|
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
|
2016-08-03 02:31:46 -07:00
|
|
|
};
|
|
|
|
|
|
|
|
// response object
|
2016-10-10 11:50:04 -07:00
|
|
|
let output;
|
2016-08-03 02:31:46 -07:00
|
|
|
|
|
|
|
// 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)
|
2016-11-23 13:39:35 -08:00
|
|
|
if (!request.compress || request.method === 'HEAD' || !headers.has('content-encoding') || res.statusCode === 204 || res.statusCode === 304) {
|
2016-08-03 02:31:46 -07:00
|
|
|
output = new Response(body, response_options);
|
|
|
|
resolve(output);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
// otherwise, check for gzip or deflate
|
2016-10-10 11:50:04 -07:00
|
|
|
let name = headers.get('content-encoding');
|
2016-08-03 02:31:46 -07:00
|
|
|
|
|
|
|
// 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
|
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) {
|
|
|
|
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
|
|
|
|
2016-08-03 02:31:46 -07:00
|
|
|
// otherwise, use response as-is
|
|
|
|
output = new Response(body, response_options);
|
2015-01-26 05:28:23 -08:00
|
|
|
resolve(output);
|
2016-08-03 02:31:46 -07:00
|
|
|
return;
|
2015-01-26 02:15:07 -08:00
|
|
|
});
|
|
|
|
|
2016-12-04 13:16:03 -08:00
|
|
|
// accept string, buffer, readable stream or null as body
|
2016-09-11 07:33:22 -07:00
|
|
|
// per spec we will call tostring on non-stream objects
|
2016-11-23 13:39:35 -08:00
|
|
|
if (typeof request.body === 'string') {
|
|
|
|
req.write(request.body);
|
2015-01-26 09:46:32 -08:00
|
|
|
req.end();
|
2016-11-23 13:39:35 -08:00
|
|
|
} else if (request.body instanceof Buffer) {
|
|
|
|
req.write(request.body);
|
2016-08-02 22:07:47 -07:00
|
|
|
req.end()
|
2016-12-04 13:16:03 -08:00
|
|
|
} else if (request.body && typeof request.body === 'object' && request.body.pipe) {
|
2016-11-23 13:39:35 -08:00
|
|
|
request.body.pipe(req);
|
2016-12-04 13:16:03 -08:00
|
|
|
} else if (request.body && typeof request.body === 'object') {
|
2016-11-23 13:39:35 -08:00
|
|
|
req.write(request.body.toString());
|
2016-09-11 07:33:22 -07:00
|
|
|
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
|
|
|
|
|
|
|
};
|
|
|
|
|
2016-10-10 11:50:04 -07: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
|
|
|
|
*/
|
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;
|
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;
|
2016-10-10 11:50:04 -07:00
|
|
|
fetch.Response = Response;
|
|
|
|
fetch.Headers = Headers;
|
|
|
|
fetch.Request = Request;
|