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
|
|
|
*/
|
|
|
|
|
2015-01-27 07:33:06 -08:00
|
|
|
var parse_url = require('url').parse;
|
|
|
|
var resolve_url = require('url').resolve;
|
2015-01-26 01:02:34 -08:00
|
|
|
var http = require('http');
|
|
|
|
var https = require('https');
|
|
|
|
var zlib = require('zlib');
|
2015-01-26 09:46:32 -08:00
|
|
|
var stream = require('stream');
|
2015-01-26 01:02:34 -08:00
|
|
|
|
2015-08-10 12:35:01 -07:00
|
|
|
var Body = require('./lib/body');
|
2015-01-27 05:11:26 -08:00
|
|
|
var Response = require('./lib/response');
|
|
|
|
var Headers = require('./lib/headers');
|
2015-06-03 21:12:27 -07:00
|
|
|
var Request = require('./lib/request');
|
2016-04-05 06:20:42 -07:00
|
|
|
var FetchError = require('./lib/fetch-error');
|
2015-01-27 05:11:26 -08:00
|
|
|
|
2016-03-19 00:55:33 -07:00
|
|
|
// commonjs
|
2015-01-26 01:02:34 -08:00
|
|
|
module.exports = Fetch;
|
2016-03-19 00:55:33 -07:00
|
|
|
// es6 default export compatibility
|
2016-03-12 00:48:00 -08:00
|
|
|
module.exports.default = module.exports;
|
2015-01-26 01:02:34 -08:00
|
|
|
|
|
|
|
/**
|
2015-01-27 05:11:26 -08:00
|
|
|
* Fetch class
|
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-27 05:11:26 -08:00
|
|
|
// allow call as function
|
2015-01-26 01:02:34 -08:00
|
|
|
if (!(this instanceof Fetch))
|
|
|
|
return new Fetch(url, opts);
|
|
|
|
|
2015-01-27 05:11:26 -08:00
|
|
|
// allow custom promise
|
2015-01-26 02:15:07 -08:00
|
|
|
if (!Fetch.Promise) {
|
2015-01-26 05:58:52 -08:00
|
|
|
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
|
|
|
|
2015-08-10 12:35:01 -07:00
|
|
|
Body.Promise = Fetch.Promise;
|
2015-01-27 05:11:26 -08:00
|
|
|
|
2015-01-26 09:46:32 -08:00
|
|
|
var self = this;
|
|
|
|
|
2015-01-27 05:11:26 -08:00
|
|
|
// wrap http.request into fetch
|
2015-01-26 02:15:07 -08:00
|
|
|
return new Fetch.Promise(function(resolve, reject) {
|
2015-06-03 21:05:01 -07:00
|
|
|
// build request object
|
2016-05-25 11:20:38 -07:00
|
|
|
var options = new Request(url, opts);
|
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');
|
|
|
|
}
|
|
|
|
|
2015-06-03 21:05:01 -07:00
|
|
|
var send;
|
|
|
|
if (options.protocol === 'https:') {
|
|
|
|
send = https.request;
|
2015-01-26 02:15:07 -08:00
|
|
|
} else {
|
2015-06-03 21:05:01 -07:00
|
|
|
send = http.request;
|
2015-01-26 02:15:07 -08:00
|
|
|
}
|
|
|
|
|
2015-01-27 05:11:26 -08:00
|
|
|
// normalize headers
|
|
|
|
var headers = new Headers(options.headers);
|
|
|
|
|
|
|
|
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)');
|
|
|
|
}
|
|
|
|
|
2016-04-29 22:19:08 -07:00
|
|
|
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', '*/*');
|
|
|
|
}
|
|
|
|
|
2015-07-22 00:40:52 -07:00
|
|
|
// detect form data input from form-data module, this hack avoid the need to pass multipart header manually
|
|
|
|
if (!headers.has('content-type') && options.body && typeof options.body.getBoundary === 'function') {
|
2015-07-21 19:05:06 -07:00
|
|
|
headers.set('content-type', 'multipart/form-data; boundary=' + options.body.getBoundary());
|
|
|
|
}
|
|
|
|
|
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)) {
|
2015-09-28 05:46:39 -07:00
|
|
|
if (typeof options.body === 'string') {
|
|
|
|
headers.set('content-length', Buffer.byteLength(options.body));
|
Add content-length header with form-data
Old source do not add content-length header automatically
with form-data. If body is instance of form-data, Users
must missing this header. It cause parse error with some
server engine like WSGI. WSGI must drop body if this was not
set, and WSGI based frameworks such as flask can not read
form data.
For example, this python code with flask..
----
from flask import Flask, request, jsonify
app = Flask(__name__)
@app.route('/', method=['POST'])
def index():
return jsonify(**request.form)
app.run()
----
Can not receive any data from this code.
----
require('es6-promise').polyfill();
var fetch = require('isomorphic-fetch'); // it use node-fetch
var FormData = require('form-data');
var data = new FormData();
data.append('name', 'item4');
data.append('message', 'it is test!');
fetch('http://localhost:5000', {method: 'POST', body: data})
.then(function (data) {
return data.json();
})
.then(function (data) {
console.log(data);
});
----
This commit just use FormData's getLengthSync method.
2015-11-25 13:54:51 -08:00
|
|
|
// detect form data input from form-data module, this hack avoid the need to add content-length header manually
|
2016-05-25 10:37:36 -07:00
|
|
|
} else if (options.body && typeof options.body.getLengthSync === 'function' && options.body._lengthRetrievers.length == 0) {
|
Add content-length header with form-data
Old source do not add content-length header automatically
with form-data. If body is instance of form-data, Users
must missing this header. It cause parse error with some
server engine like WSGI. WSGI must drop body if this was not
set, and WSGI based frameworks such as flask can not read
form data.
For example, this python code with flask..
----
from flask import Flask, request, jsonify
app = Flask(__name__)
@app.route('/', method=['POST'])
def index():
return jsonify(**request.form)
app.run()
----
Can not receive any data from this code.
----
require('es6-promise').polyfill();
var fetch = require('isomorphic-fetch'); // it use node-fetch
var FormData = require('form-data');
var data = new FormData();
data.append('name', 'item4');
data.append('message', 'it is test!');
fetch('http://localhost:5000', {method: 'POST', body: data})
.then(function (data) {
return data.json();
})
.then(function (data) {
console.log(data);
});
----
This commit just use FormData's getLengthSync method.
2015-11-25 13:54:51 -08:00
|
|
|
headers.set('content-length', options.body.getLengthSync().toString());
|
2015-09-28 05:46:39 -07:00
|
|
|
// 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-09-28 02:40:58 -07:00
|
|
|
}
|
|
|
|
|
2015-01-27 05:11:26 -08:00
|
|
|
options.headers = headers.raw();
|
|
|
|
|
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
|
2015-06-03 21:05:01 -07:00
|
|
|
var req = send(options);
|
2015-04-16 09:01:29 -07:00
|
|
|
var reqTimeout;
|
2015-01-27 05:11:26 -08:00
|
|
|
|
2015-02-01 04:36:17 -08:00
|
|
|
if (options.timeout) {
|
|
|
|
req.once('socket', function(socket) {
|
2015-04-16 09:01:29 -07:00
|
|
|
reqTimeout = setTimeout(function() {
|
2015-01-27 05:11:26 -08:00
|
|
|
req.abort();
|
2016-04-05 10:47:07 -07:00
|
|
|
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
|
|
|
|
2015-01-26 05:28:23 -08:00
|
|
|
req.on('error', function(err) {
|
2015-04-16 09:01:29 -07:00
|
|
|
clearTimeout(reqTimeout);
|
2016-04-05 06:20:42 -07:00
|
|
|
reject(new FetchError('request to ' + options.url + ' failed, reason: ' + err.message, 'system', err));
|
2015-01-26 05:28:23 -08:00
|
|
|
});
|
|
|
|
|
2015-01-26 02:15:07 -08:00
|
|
|
req.on('response', function(res) {
|
2015-04-16 09:01:29 -07:00
|
|
|
clearTimeout(reqTimeout);
|
|
|
|
|
2015-01-27 05:11:26 -08:00
|
|
|
// handle redirect
|
2016-04-05 11:47:23 -07:00
|
|
|
if (self.isRedirect(res.statusCode) && options.redirect !== 'manual') {
|
|
|
|
if (options.redirect === 'error') {
|
|
|
|
reject(new FetchError('redirect mode is set to error: ' + options.url, 'no-redirect'));
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2015-01-26 09:46:32 -08:00
|
|
|
if (options.counter >= options.follow) {
|
2016-04-05 06:20:42 -07:00
|
|
|
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) {
|
2016-04-05 06:20:42 -07:00
|
|
|
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
|
|
|
}
|
|
|
|
|
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
|
|
|
|
|| ((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++;
|
|
|
|
|
2015-06-03 21:05:01 -07:00
|
|
|
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
|
2016-08-03 02:31:46 -07:00
|
|
|
var headers = new Headers(res.headers);
|
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')));
|
|
|
|
}
|
|
|
|
|
2016-08-03 02:31:46 -07:00
|
|
|
// prepare response
|
|
|
|
var body = res.pipe(new stream.PassThrough());
|
|
|
|
var 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
|
2015-04-16 22:26:55 -07:00
|
|
|
, timeout: options.timeout
|
2016-08-03 02:31:46 -07:00
|
|
|
};
|
|
|
|
|
|
|
|
// response object
|
|
|
|
var 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
|
|
|
|
var 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
|
|
|
|
var raw = res.pipe(new stream.PassThrough());
|
|
|
|
raw.once('data', function(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
|
|
|
|
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-08-02 22:07:47 -07:00
|
|
|
// accept string, buffer or readable stream as body
|
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);
|
|
|
|
} else {
|
|
|
|
req.end();
|
|
|
|
}
|
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.prototype.isRedirect = function(code) {
|
|
|
|
return code === 301 || code === 302 || code === 303 || code === 307 || code === 308;
|
|
|
|
}
|
|
|
|
|
2015-01-26 02:15:07 -08:00
|
|
|
// expose Promise
|
2015-01-26 01:02:34 -08:00
|
|
|
Fetch.Promise = global.Promise;
|
2015-05-03 01:37:58 -07:00
|
|
|
Fetch.Response = Response;
|
|
|
|
Fetch.Headers = Headers;
|
2015-06-03 21:05:01 -07:00
|
|
|
Fetch.Request = Request;
|