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