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-10-10 11:50:04 -07:00
|
|
|
import Request from './request';
|
|
|
|
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;
|
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-10-10 11:50:04 -07:00
|
|
|
const 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');
|
|
|
|
}
|
|
|
|
|
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-01-27 05:11:26 -08:00
|
|
|
// normalize headers
|
2016-10-10 11:50:04 -07:00
|
|
|
const headers = new Headers(options.headers);
|
2015-01-27 05:11:26 -08:00
|
|
|
|
|
|
|
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') {
|
2016-10-10 11:50:04 -07:00
|
|
|
headers.set('content-type', `multipart/form-data; boundary=${options.body.getBoundary()}`);
|
2015-07-21 19:05:06 -07:00
|
|
|
}
|
|
|
|
|
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-09-25 21:58:04 -07:00
|
|
|
} 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());
|
|
|
|
}
|
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
|
2016-10-10 11:50:04 -07:00
|
|
|
const req = send(options);
|
|
|
|
let reqTimeout;
|
2015-01-27 05:11:26 -08:00
|
|
|
|
2015-02-01 04:36:17 -08:00
|
|
|
if (options.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-10-10 11:50:04 -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
|
|
|
|
2016-10-10 11:50:04 -07:00
|
|
|
req.on('error', err => {
|
2015-04-16 09:01:29 -07:00
|
|
|
clearTimeout(reqTimeout);
|
2016-10-10 11:50:04 -07:00
|
|
|
reject(new FetchError(`request to ${options.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-10-10 11:50:04 -07:00
|
|
|
if (fetch.isRedirect(res.statusCode) && options.redirect !== 'manual') {
|
2016-04-05 11:47:23 -07:00
|
|
|
if (options.redirect === 'error') {
|
2016-10-10 11:50:04 -07:00
|
|
|
reject(new FetchError(`redirect mode is set to error: ${options.url}`, 'no-redirect'));
|
2016-04-05 11:47:23 -07:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2015-01-26 09:46:32 -08:00
|
|
|
if (options.counter >= options.follow) {
|
2016-10-10 11:50:04 -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-10-10 11:50:04 -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++;
|
|
|
|
|
2016-10-10 11:50:04 -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-10-10 11:50:04 -07:00
|
|
|
const 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
|
2016-10-10 11:50:04 -07:00
|
|
|
let body = res.pipe(new PassThrough());
|
|
|
|
const 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
|
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)
|
|
|
|
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
|
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-08-02 22:07:47 -07:00
|
|
|
// accept string, buffer or readable stream as body
|
2016-09-11 07:33:22 -07:00
|
|
|
// per spec we will call tostring on non-stream objects
|
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);
|
2016-09-11 07:33:22 -07:00
|
|
|
} else if (typeof options.body === 'object') {
|
|
|
|
req.write(options.body.toString());
|
|
|
|
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;
|
|
|
|
fetch.Response = Response;
|
|
|
|
fetch.Headers = Headers;
|
|
|
|
fetch.Request = Request;
|