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');
|
2015-01-27 05:11:26 -08:00
|
|
|
|
2015-01-26 01:02:34 -08:00
|
|
|
module.exports = Fetch;
|
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
|
|
|
|
var options;
|
|
|
|
try {
|
|
|
|
options = new Request(url, opts);
|
|
|
|
} catch (err) {
|
|
|
|
reject(err);
|
2015-01-26 02:15:07 -08:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
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)');
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!headers.has('connection')) {
|
|
|
|
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());
|
|
|
|
}
|
|
|
|
|
2015-09-28 05:46:39 -07:00
|
|
|
// bring node-fetch closer to browser behavior by setting content-length automatically for POST, PUT, PATCH requests when body is empty or string
|
|
|
|
if (!headers.has('content-length') && options.method.substr(0, 1).toUpperCase() === 'P') {
|
|
|
|
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
|
|
|
|
} else if (options.body && typeof options.body.getLengthSync === 'function') {
|
|
|
|
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();
|
2015-06-03 21:05:01 -07:00
|
|
|
reject(new Error('network timeout at: ' + options.url));
|
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);
|
2015-06-03 21:05:01 -07:00
|
|
|
reject(new Error('request to ' + options.url + ' failed, reason: ' + err.message));
|
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
|
2015-01-26 09:46:32 -08:00
|
|
|
if (self.isRedirect(res.statusCode)) {
|
|
|
|
if (options.counter >= options.follow) {
|
2015-06-03 21:05:01 -07:00
|
|
|
reject(new Error('maximum redirect reached at: ' + options.url));
|
2015-01-27 07:33:06 -08:00
|
|
|
return;
|
2015-01-26 09:46:32 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
if (!res.headers.location) {
|
2015-06-03 21:05:01 -07:00
|
|
|
reject(new Error('redirect location header missing at: ' + options.url));
|
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
|
|
|
}
|
|
|
|
|
2015-01-27 05:11:26 -08:00
|
|
|
// handle compression
|
|
|
|
var body = res.pipe(new stream.PassThrough());
|
|
|
|
var headers = new Headers(res.headers);
|
|
|
|
|
2015-01-27 07:33:06 -08:00
|
|
|
if (options.compress && headers.has('content-encoding')) {
|
2015-01-27 05:11:26 -08:00
|
|
|
var name = headers.get('content-encoding');
|
|
|
|
|
|
|
|
if (name == 'gzip' || name == 'x-gzip') {
|
|
|
|
body = body.pipe(zlib.createGunzip());
|
|
|
|
} else if (name == 'deflate' || name == 'x-deflate') {
|
|
|
|
body = body.pipe(zlib.createInflate());
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// response object
|
|
|
|
var output = new Response(body, {
|
2015-06-03 21:05:01 -07:00
|
|
|
url: options.url
|
2015-01-27 05:11:26 -08:00
|
|
|
, status: res.statusCode
|
|
|
|
, headers: headers
|
2015-01-27 09:00:53 -08:00
|
|
|
, size: options.size
|
2015-04-16 22:26:55 -07:00
|
|
|
, timeout: options.timeout
|
2015-01-27 05:11:26 -08:00
|
|
|
});
|
2015-01-26 02:15:07 -08:00
|
|
|
|
2015-01-26 05:28:23 -08:00
|
|
|
resolve(output);
|
2015-01-26 02:15:07 -08:00
|
|
|
});
|
|
|
|
|
2015-01-27 05:11:26 -08:00
|
|
|
// accept string or readable stream as body
|
2015-01-26 09:46:32 -08:00
|
|
|
if (typeof options.body === 'string') {
|
|
|
|
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;
|