function Headers(headers) { var self = this; this._headers = {}; // Headers if (headers instanceof Headers) { headers = headers.raw(); } // plain object for (var prop in headers) { if (!headers.hasOwnProperty(prop)) { continue; } if (typeof headers[prop] === 'string') { this.set(prop, headers[prop]); } else if (typeof headers[prop] === 'number' && !isNaN(headers[prop])) { this.set(prop, headers[prop].toString()); } else if (headers[prop] instanceof Array) { headers[prop].forEach(function(item) { self.append(prop, item.toString()); }); } } }
n/a
function Promise() { [native code] }
...
}
Body.Promise = Fetch.Promise;
var self = this;
// wrap http.request into fetch
return new Fetch.Promise(function(resolve, reject) {
// build request object
var options = new Request(url, opts);
if (!options.protocol || !options.hostname) {
throw new Error('only absolute urls are supported');
}
...
function Request(input, init) { var url, url_parsed; // normalize input if (!(input instanceof Request)) { url = input; url_parsed = parse_url(url); input = {}; } else { url = input.url; url_parsed = parse_url(url); } // normalize init init = init || {}; // fetch spec options this.method = init.method || input.method || 'GET'; this.redirect = init.redirect || input.redirect || 'follow'; this.headers = new Headers(init.headers || input.headers || {}); this.url = url; // server only options this.follow = init.follow !== undefined ? init.follow : input.follow !== undefined ? input.follow : 20; this.compress = init.compress !== undefined ? init.compress : input.compress !== undefined ? input.compress : true; this.counter = init.counter || input.counter || 0; this.agent = init.agent || input.agent; Body.call(this, init.body || this._clone(input), { timeout: init.timeout || input.timeout || 0, size: init.size || input.size || 0 }); // server request options this.protocol = url_parsed.protocol; this.hostname = url_parsed.hostname; this.port = url_parsed.port; this.path = url_parsed.path; this.auth = url_parsed.auth; }
n/a
function Response(body, opts) { opts = opts || {}; this.url = opts.url; this.status = opts.status || 200; this.statusText = opts.statusText || http.STATUS_CODES[this.status]; this.headers = new Headers(opts.headers); this.ok = this.status >= 200 && this.status < 300; Body.call(this, body, opts); }
n/a
function Body(body, opts) { opts = opts || {}; this.body = body; this.bodyUsed = false; this.size = opts.size || 0; this.timeout = opts.timeout || 0; this._raw = []; this._abort = false; }
n/a
function Fetch(url, opts) { // allow call as function if (!(this instanceof Fetch)) return new Fetch(url, opts); // allow custom promise if (!Fetch.Promise) { throw new Error('native promise missing, set Fetch.Promise to your favorite alternative'); } Body.Promise = Fetch.Promise; var self = this; // wrap http.request into fetch return new Fetch.Promise(function(resolve, reject) { // build request object var options = new Request(url, opts); 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'); } var send; if (options.protocol === 'https:') { send = https.request; } else { send = http.request; } // 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') && !options.agent) { headers.set('connection', 'close'); } if (!headers.has('accept')) { headers.set('accept', '*/*'); } // 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') { headers.set('content-type', 'multipart/form-data; boundary=' + options.body.getBoundary()); } // 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'); } } 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]; } // send request var req = send(options); var reqTimeout; if (options.timeout) { req.once('socket', function(socket) { reqTimeout = setTimeout(function() { req.abort(); reject(new FetchError('network timeout at: ' + options.url, 'request-timeout')); }, options.timeout); }); } req.on('error', function(err) { clearTimeout(reqTimeout); reject(new FetchError('request to ' + options.url + ' failed, reason: ' + err.message, 'system', err)); }); req.on('response', function(res) { clearTimeout(reqTimeout); // handle redirect 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; } if (options.counter >= options.follow) { reject(new FetchError('maximum redirect reached at: ' + options.url, 'max-redirect')); return; } if (!res.headers.location) { reject(new FetchError('redirect location header missing at: ' + options.url, 'invalid-redirect')); return; } // per fetch spec, for POST request with 301/302 response, or any request with 303 response, use GET wh ...
n/a
function FetchError(message, type, systemError) { // hide custom error implementation details from end-users Error.captureStackTrace(this, this.constructor); this.name = this.constructor.name; this.message = message; this.type = type; // when err.type is `system`, err.code contains system error code if (systemError) { this.code = this.errno = systemError.code; } }
n/a
function Headers(headers) { var self = this; this._headers = {}; // Headers if (headers instanceof Headers) { headers = headers.raw(); } // plain object for (var prop in headers) { if (!headers.hasOwnProperty(prop)) { continue; } if (typeof headers[prop] === 'string') { this.set(prop, headers[prop]); } else if (typeof headers[prop] === 'number' && !isNaN(headers[prop])) { this.set(prop, headers[prop].toString()); } else if (headers[prop] instanceof Array) { headers[prop].forEach(function(item) { self.append(prop, item.toString()); }); } } }
n/a
append = function (name, value) { if (!this.has(name)) { this.set(name, value); return; } this._headers[name.toLowerCase()].push(value); }
...
console.log(json);
});
// post with form-data (detect multipart)
var FormData = require('form-data');
var form = new FormData();
form.append('a', 1);
fetch('http://httpbin.org/post', { method: 'POST', body: form })
.then(function(res) {
return res.json();
}).then(function(json) {
console.log(json);
});
...
delete = function (name) { delete this._headers[name.toLowerCase()]; }
n/a
forEach = function (callback, thisArg) { Object.getOwnPropertyNames(this._headers).forEach(function(name) { this._headers[name].forEach(function(value) { callback.call(thisArg, value, name, this) }, this) }, this) }
n/a
get = function (name) { var list = this._headers[name.toLowerCase()]; return list ? list[0] : null; }
...
fetch('https://github.com/')
.then(function(res) {
console.log(res.ok);
console.log(res.status);
console.log(res.statusText);
console.log(res.headers.raw());
console.log(res.headers.get('content-type'));
});
// post
fetch('http://httpbin.org/post', { method: 'POST', body: 'a=1' })
.then(function(res) {
return res.json();
...
getAll = function (name) { if (!this.has(name)) { return []; } return this._headers[name.toLowerCase()]; }
n/a
has = function (name) { return this._headers.hasOwnProperty(name.toLowerCase()); }
...
// 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') && !options.agent) {
headers.set('connection', 'close');
}
...
raw = function () { return this._headers; }
...
// meta
fetch('https://github.com/')
.then(function(res) {
console.log(res.ok);
console.log(res.status);
console.log(res.statusText);
console.log(res.headers.raw());
console.log(res.headers.get('content-type'));
});
// post
fetch('http://httpbin.org/post', { method: 'POST', body: 'a=1' })
.then(function(res) {
...
set = function (name, value) { this._headers[name.toLowerCase()] = [value]; }
...
send = http.request;
}
// 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') && !options.agent) {
...
function Request(input, init) { var url, url_parsed; // normalize input if (!(input instanceof Request)) { url = input; url_parsed = parse_url(url); input = {}; } else { url = input.url; url_parsed = parse_url(url); } // normalize init init = init || {}; // fetch spec options this.method = init.method || input.method || 'GET'; this.redirect = init.redirect || input.redirect || 'follow'; this.headers = new Headers(init.headers || input.headers || {}); this.url = url; // server only options this.follow = init.follow !== undefined ? init.follow : input.follow !== undefined ? input.follow : 20; this.compress = init.compress !== undefined ? init.compress : input.compress !== undefined ? input.compress : true; this.counter = init.counter || input.counter || 0; this.agent = init.agent || input.agent; Body.call(this, init.body || this._clone(input), { timeout: init.timeout || input.timeout || 0, size: init.size || input.size || 0 }); // server request options this.protocol = url_parsed.protocol; this.hostname = url_parsed.hostname; this.port = url_parsed.port; this.path = url_parsed.path; this.auth = url_parsed.auth; }
n/a
clone = function () { return new Request(this); }
n/a
function Response(body, opts) { opts = opts || {}; this.url = opts.url; this.status = opts.status || 200; this.statusText = opts.statusText || http.STATUS_CODES[this.status]; this.headers = new Headers(opts.headers); this.ok = this.status >= 200 && this.status < 300; Body.call(this, body, opts); }
n/a
clone = function () { return new Response(this._clone(this), { url: this.url , status: this.status , statusText: this.statusText , headers: this.headers , ok: this.ok }); }
n/a
function Body(body, opts) { opts = opts || {}; this.body = body; this.bodyUsed = false; this.size = opts.size || 0; this.timeout = opts.timeout || 0; this._raw = []; this._abort = false; }
n/a
function Promise() { [native code] }
...
}
Body.Promise = Fetch.Promise;
var self = this;
// wrap http.request into fetch
return new Fetch.Promise(function(resolve, reject) {
// build request object
var options = new Request(url, opts);
if (!options.protocol || !options.hostname) {
throw new Error('only absolute urls are supported');
}
...
_clone = function (instance) { var p1, p2; var body = instance.body; // don't allow cloning a used body if (instance.bodyUsed) { throw new Error('cannot clone body after it is used'); } // check that body is a stream and not form-data object // note: we can't clone the form-data object without having it as a dependency if (bodyStream(body) && typeof body.getBoundary !== 'function') { // tee instance body p1 = new PassThrough(); p2 = new PassThrough(); body.pipe(p1); body.pipe(p2); // set instance body to teed body and return the other teed body instance.body = p1; body = p2; } return body; }
n/a
_convert = function (encoding) { encoding = encoding || 'utf-8'; var ct = this.headers.get('content-type'); var charset = 'utf-8'; var res, str; // header if (ct) { // skip encoding detection altogether if not html/xml/plain text if (!/text\/html|text\/plain|\+xml|\/xml/i.test(ct)) { return Buffer.concat(this._raw); } res = /charset=([^;]*)/i.exec(ct); } // no charset in content type, peek at response body for at most 1024 bytes if (!res && this._raw.length > 0) { for (var i = 0; i < this._raw.length; i++) { str += this._raw[i].toString() if (str.length > 1024) { break; } } str = str.substr(0, 1024); } // html5 if (!res && str) { res = /<meta.+?charset=(['"])(.+?)\1/i.exec(str); } // html4 if (!res && str) { res = /<meta[\s]+?http-equiv=(['"])content-type\1[\s]+?content=(['"])(.+?)\2/i.exec(str); if (res) { res = /charset=(.*)/i.exec(res.pop()); } } // xml if (!res && str) { res = /<\?xml.+?encoding=(['"])(.+?)\1/i.exec(str); } // found charset if (res) { charset = res.pop(); // prevent decode issues when sites use incorrect encoding // ref: https://hsivonen.fi/encoding-menu/ if (charset === 'gb2312' || charset === 'gbk') { charset = 'gb18030'; } } // turn raw buffers into a single utf-8 buffer return convert( Buffer.concat(this._raw) , encoding , charset ); }
...
return new Body.Promise(function(resolve, reject) {
var resTimeout;
// body is string
if (typeof self.body === 'string') {
self._bytes = self.body.length;
self._raw = [new Buffer(self.body)];
return resolve(self._convert());
}
// body is buffer
if (self.body instanceof Buffer) {
self._bytes = self.body.length;
self._raw = [self.body];
return resolve(self._convert());
...
_decode = function () { var self = this; if (this.bodyUsed) { return Body.Promise.reject(new Error('body used already for: ' + this.url)); } this.bodyUsed = true; this._bytes = 0; this._abort = false; this._raw = []; return new Body.Promise(function(resolve, reject) { var resTimeout; // body is string if (typeof self.body === 'string') { self._bytes = self.body.length; self._raw = [new Buffer(self.body)]; return resolve(self._convert()); } // body is buffer if (self.body instanceof Buffer) { self._bytes = self.body.length; self._raw = [self.body]; return resolve(self._convert()); } // allow timeout on slow response body if (self.timeout) { resTimeout = setTimeout(function() { self._abort = true; reject(new FetchError('response timeout at ' + self.url + ' over limit: ' + self.timeout, 'body-timeout')); }, self.timeout); } // handle stream error, such as incorrect content-encoding self.body.on('error', function(err) { reject(new FetchError('invalid response body at: ' + self.url + ' reason: ' + err.message, 'system', err)); }); // body is stream self.body.on('data', function(chunk) { if (self._abort || chunk === null) { return; } if (self.size && self._bytes + chunk.length > self.size) { self._abort = true; reject(new FetchError('content size at ' + self.url + ' over limit: ' + self.size, 'max-size')); return; } self._bytes += chunk.length; self._raw.push(chunk); }); self.body.on('end', function() { if (self._abort) { return; } clearTimeout(resTimeout); resolve(self._convert()); }); }); }
...
Body.prototype.json = function() {
// for 204 No Content response, buffer will be empty, parsing it will throw error
if (this.status === 204) {
return Body.Promise.resolve({});
}
return this._decode().then(function(buffer) {
return JSON.parse(buffer.toString());
});
};
/**
* Decode response as text
...
buffer = function () { return this._decode(); }
...
// buffer
// if you prefer to cache binary data in full, use buffer()
// note that buffer() is a node-fetch only API
var fileType = require('file-type');
fetch('https://assets-cdn.github.com/images/modules/logos_page/Octocat.png')
.then(function(res) {
return res.buffer();
}).then(function(buffer) {
fileType(buffer);
});
// meta
fetch('https://github.com/')
...
json = function () { // for 204 No Content response, buffer will be empty, parsing it will throw error if (this.status === 204) { return Body.Promise.resolve({}); } return this._decode().then(function(buffer) { return JSON.parse(buffer.toString()); }); }
...
# Features
- Stay consistent with `window.fetch` API.
- Make conscious trade-off when following [whatwg fetch spec](https://fetch.spec.whatwg.org/) and [stream spec](https://streams.
spec.whatwg.org/) implementation details, document known difference.
- Use native promise, but allow substituting it with [insert your favorite promise library].
- Use native stream for body, on both request and response.
- Decode content encoding (gzip/deflate) properly, and convert string output (such as `res.text()` and `res.json()`) to UTF-8 automatically.
- Useful extensions such as timeout, redirect limit, response size limit, [explicit errors](https://github.com/bitinn/node-fetch
/blob/master/ERROR-HANDLING.md) for troubleshooting.
# Difference from client-side fetch
- See [Known Differences](https://github.com/bitinn/node-fetch/blob/master/LIMITS.md) for details.
- If you happen to use a missing feature that `window.fetch` offers, feel free to open an issue.
...
text = function () { return this._decode().then(function(buffer) { return buffer.toString(); }); }
...
# Features
- Stay consistent with `window.fetch` API.
- Make conscious trade-off when following [whatwg fetch spec](https://fetch.spec.whatwg.org/) and [stream spec](https://streams.
spec.whatwg.org/) implementation details, document known difference.
- Use native promise, but allow substituting it with [insert your favorite promise library].
- Use native stream for body, on both request and response.
- Decode content encoding (gzip/deflate) properly, and convert string output (such as `res.text
()` and `res.json()`) to UTF-8 automatically.
- Useful extensions such as timeout, redirect limit, response size limit, [explicit errors](https://github.com/bitinn/node-fetch
/blob/master/ERROR-HANDLING.md) for troubleshooting.
# Difference from client-side fetch
- See [Known Differences](https://github.com/bitinn/node-fetch/blob/master/LIMITS.md) for details.
- If you happen to use a missing feature that `window.fetch` offers, feel free to open an issue.
...
function FetchError(message, type, systemError) { // hide custom error implementation details from end-users Error.captureStackTrace(this, this.constructor); this.name = this.constructor.name; this.message = message; this.type = type; // when err.type is `system`, err.code contains system error code if (systemError) { this.code = this.errno = systemError.code; } }
n/a
function Error() { [native code] }
n/a