Merge pull request #177 from bitinn/v2

node-fetch v2
This commit is contained in:
Timothy Gu 2017-01-14 21:33:01 -08:00 committed by GitHub
commit 8cf1541fb0
27 changed files with 2791 additions and 1602 deletions

30
.babelrc Normal file
View File

@ -0,0 +1,30 @@
{
"plugins": [
"transform-runtime"
],
"env": {
"test": {
"presets": [
[ "es2015", { "loose": true } ]
],
"plugins": [
"transform-runtime",
"./build/babel-plugin"
]
},
"coverage": {
"presets": [
[ "es2015", { "loose": true } ]
],
"plugins": [
[ "istanbul", { "exclude": [ "src/blob.js", "build", "test" ] } ],
"./build/babel-plugin"
]
},
"rollup": {
"presets": [
[ "es2015", { "loose": true, "modules": false } ]
]
}
}
}

6
.gitignore vendored
View File

@ -10,7 +10,8 @@ pids
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
# Coverage directory used by tools like nyc and istanbul
.nyc_output
coverage
# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
@ -29,3 +30,6 @@ node_modules
# OS files
.DS_Store
# Babel-compiled files
lib

7
.nycrc Normal file
View File

@ -0,0 +1,7 @@
{
"require": [
"babel-register"
],
"sourceMap": false,
"instrument": false
}

View File

@ -1,7 +1,8 @@
language: node_js
node_js:
- "0.10"
- "0.12"
- "4"
- "6"
- "node"
env:
- FORMDATA_VERSION=1.0.0

View File

@ -3,6 +3,33 @@ Changelog
=========
# 2.x release
## v2.0.0-alpha.1 (UNRELEASED)
- Major: Node.js 0.10.x support is dropped
- Major: rewrite in transpiled ES2015
- Major: internal methods are no longer exposed
- Major: throw error when a GET/HEAD Request is constructed with a non-null body (per spec)
- Major: `response.text()` no longer attempts to detect encoding, instead always opting for UTF-8 (per spec); use `response.textConverted()` for the old behavior
- Major: make `response.json()` throw error instead of returning an empty object on 204 no-content respose (per spec; reverts behavior set in v1.6.2)
- Major: arrays as parameters to `headers.append` and `headers.set` are joined as a string (per spec)
- Enhance: start testing on Node.js 4, 6, 7
- Enhance: use Rollup to produce a distributed bundle (less memory overhead and faster startup)
- Enhance: make `toString()` on Headers, Requests, and Responses return correct IDL class strings
- Enhance: add an option to conform to latest spec at the expense of reduced compatibility
- Enhance: set `Content-Length` header for Buffers as well
- Enhance: add `response.arrayBuffer()` (also applies to Requests)
- Enhance: add experimental `response.blob()` (also applies to Requests)
- Enhance: make Headers iterable
- Enhance: make Headers constructor accept an array of tuples
- Enhance: make sure header names and values are valid in HTTP
- Fix: coerce Headers prototype function parameters to strings, where applicable
- Fix: fix Request and Response with `null` body
- Fix: support WHATWG URL objects, created by `whatwg-url` package or `require('url').URL` in Node.js 7+
- Other: use Codecov for code coverage tracking
# 1.x release
## v1.6.3

View File

@ -2,7 +2,7 @@
Known differences
=================
*As of 1.x release*
*As of 2.x release*
- Topics such as Cross-Origin, Content Security Policy, Mixed Content, Service Workers are ignored, given our server-side context.
@ -16,12 +16,10 @@ Known differences
- Also, you can handle rejected fetch requests through checking `err.type` and `err.code`.
- Only support `res.text()`, `res.json()`, `res.buffer()` at the moment, until there are good use-cases for blob/arrayBuffer.
- Only support `res.text()`, `res.json()`, `res.blob()`, `res.arraybuffer()`, `res.buffer()`
- There is currently no built-in caching, as server-side caching varies by use-cases.
- Current implementation lacks server-side cookie store, you will need to extract `Set-Cookie` headers manually.
- If you are using `res.clone()` and writing an isomorphic app, note that stream on Node.js have a smaller internal buffer size (16Kb, aka `highWaterMark`) from client-side browsers (>1Mb, not consistent across browsers).
- ES6 features such as `headers.entries()` are missing at the moment, but you can use `headers.raw()` to retrieve the raw headers object.

110
README.md
View File

@ -41,44 +41,40 @@ See Matt Andrews' [isomorphic-fetch](https://github.com/matthew-andrews/isomorph
# Usage
```javascript
var fetch = require('node-fetch');
import fetch from 'node-fetch';
// or
// const fetch = require('node-fetch');
// if you are on node v0.10, set a Promise library first, eg.
// fetch.Promise = require('bluebird');
// if you are using your own Promise library, set it through fetch.Promise. Eg.
// import Bluebird from 'bluebird';
// fetch.Promise = Bluebird;
// plain text or html
fetch('https://github.com/')
.then(function(res) {
return res.text();
}).then(function(body) {
console.log(body);
});
.then(res => res.text())
.then(body => console.log(body));
// json
fetch('https://api.github.com/users/github')
.then(function(res) {
return res.json();
}).then(function(json) {
console.log(json);
});
.then(res => res.json())
.then(json => console.log(json));
// catching network error
// 3xx-5xx responses are NOT network errors, and should be handled in then()
// you only need one catch() at the end of your promise chain
fetch('http://domain.invalid/')
.catch(function(err) {
console.log(err);
});
.catch(err => console.error(err));
// stream
// the node.js way is to use stream when possible
fetch('https://assets-cdn.github.com/images/modules/logos_page/Octocat.png')
.then(function(res) {
var dest = fs.createWriteStream('./octocat.png');
.then(res => {
const dest = fs.createWriteStream('./octocat.png');
res.body.pipe(dest);
});
@ -86,18 +82,17 @@ fetch('https://assets-cdn.github.com/images/modules/logos_page/Octocat.png')
// 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');
import fileType from '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);
});
.then(res => res.buffer())
.then(buffer => fileType(buffer))
.then(type => { /* ... */ });
// meta
fetch('https://github.com/')
.then(function(res) {
.then(res => {
console.log(res.ok);
console.log(res.status);
console.log(res.statusText);
@ -108,22 +103,17 @@ fetch('https://github.com/')
// post
fetch('http://httpbin.org/post', { method: 'POST', body: 'a=1' })
.then(function(res) {
return res.json();
}).then(function(json) {
console.log(json);
});
.then(res => res.json())
.then(json => console.log(json));
// post with stream from resumer
// post with stream from file
var resumer = require('resumer');
var stream = resumer().queue('a=1').end();
import { createReadStream } from 'fs';
const stream = createReadStream('input.txt');
fetch('http://httpbin.org/post', { method: 'POST', body: stream })
.then(function(res) {
return res.json();
}).then(function(json) {
console.log(json);
});
.then(res => res.json())
.then(json => console.log(json));
// post with JSON
@ -133,45 +123,37 @@ fetch('http://httpbin.org/post', {
body: JSON.stringify(body),
headers: { 'Content-Type': 'application/json' },
})
.then(function(res) {
return res.json();
}).then(function(json) {
console.log(json);
});
.then(res => res.json())
.then(json => console.log(json));
// post with form-data (detect multipart)
var FormData = require('form-data');
var form = new FormData();
import FormData from 'form-data';
const 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);
});
.then(res => res.json())
.then(json => console.log(json));
// post with form-data (custom headers)
// note that getHeaders() is non-standard API
var FormData = require('form-data');
var form = new FormData();
import FormData from 'form-data';
const form = new FormData();
form.append('a', 1);
fetch('http://httpbin.org/post', { method: 'POST', body: form, headers: form.getHeaders() })
.then(function(res) {
return res.json();
}).then(function(json) {
console.log(json);
});
.then(res => res.json())
.then(json => console.log(json));
// node 0.12+, yield with co
// node 7+ with async function
var co = require('co');
co(function *() {
var res = yield fetch('https://api.github.com/users/github');
var json = yield res.json();
console.log(res);
});
(async function () {
const res = await fetch('https://api.github.com/users/github');
const json = await res.json();
console.log(json);
})();
```
See [test cases](https://github.com/bitinn/node-fetch/blob/master/test/test.js) for more examples.

95
UPGRADE-GUIDE.md Normal file
View File

@ -0,0 +1,95 @@
# Upgrade to node-fetch v2
node-fetch v2 brings about many changes that increase the compliance of
WHATWG's [Fetch Standard][whatwg-fetch]. However, many of these changes meant
that apps written for node-fetch v1 needs to be updated to work with node-fetch
v2 and be conformant with the Fetch Standard.
## `.text()` no longer tries to detect encoding
In v1, `response.text()` attempts to guess the text encoding of the input
material and decode it for the user. However, it runs counter to the Fetch
Standard which demands `.text()` to always use UTF-8.
In "response" to that, we have changed `.text()` to use UTF-8. A new function
**`response.textConverted()`** is created that maintains the behavior of
`.text()` last year.
## Internal methods hidden
In v1, the user can access internal methods such as `_clone()`, `_decode()`,
and `_convert()` on the `response` object. While these methods should never
have been used, node-fetch v2 makes these functions completely inaccessible.
If your app makes use of these functions, it may break when upgrading to v2.
If you have a use case that requires these methods to be available, feel free
to file an issue and we will be happy to help you solve the problem.
## Headers
The main goal we have for the `Headers` class in v2 is to make it completely
spec-compliant. However, due to changes in the Fetch Standard itself, total
spec compliance would mean incompatibility with all current major browser
implementations.
Therefore, in v2, only a limited set of changes was applied to preserve
compatibility with browsers by default. See [#181] for more information on why
a feature is enabled or disabled.
```js
//////////////////////////////////////////////////////////////////////////////
// If you are using an object as the initializer, all values will be
// stringified. For arrays, the members will be joined with a comma.
const headers = new Headers({
'Abc': 'string',
'Multi': [ 'header1', 'header2' ]
});
// before after
headers.get('Multi') => headers.get('Multi') =>
'header1'; 'header1,header2';
headers.getAll('Multi') => headers.getAll('Multi') =>
[ 'header1', 'header2' ]; [ 'header1,header2' ];
// Instead, to preserve the older behavior, you can use the header pair array
// syntax.
const headers = new Headers([
[ 'Abc', 'string' ],
[ 'Multi', 'header1' ],
[ 'Multi', 'header2' ]
]);
//////////////////////////////////////////////////////////////////////////////
// All method parameters are now stringified.
const headers = new Headers();
headers.set('null-header', null);
headers.set('undefined', undefined);
// before after
headers.get('null-header') headers.get('null-header')
=> null => 'null'
headers.get(undefined) headers.get(undefined)
=> throws => 'undefined'
//////////////////////////////////////////////////////////////////////////////
// Invalid HTTP header names and values are now rejected outright.
const headers = new Headers();
headers.set('Héy', 'ok'); // now throws
headers.get('Héy'); // now throws
new Headers({ 'Héy': 'ok' }); // now throws
```
## 0.10.x support dropped
If you are still using Node.js v0.10, upgrade ASAP. Not only has Node.js
dropped support for that release branch, it has become too much work for us to
maintain. Therefore, we have dropped official support for v0.10.
That being said, node-fetch may still work with v0.10, but as we are not
actively trying to support that version, it is in the user's best interest to
upgrade.
[whatwg-fetch]: https://fetch.spec.whatwg.org/
[#181]: https://github.com/bitinn/node-fetch/issues/181

48
build/babel-plugin.js Normal file
View File

@ -0,0 +1,48 @@
// This Babel plugin makes it possible to do CommonJS-style function exports
const walked = Symbol('walked');
module.exports = ({ types: t }) => ({
visitor: {
Program: {
exit(program) {
if (program[walked]) {
return;
}
for (let path of program.get('body')) {
if (path.isExpressionStatement()) {
const expr = path.get('expression');
if (expr.isAssignmentExpression() &&
expr.get('left').matchesPattern('exports.*')) {
const prop = expr.get('left').get('property');
if (prop.isIdentifier({ name: 'default' })) {
program.unshiftContainer('body', [
t.expressionStatement(
t.assignmentExpression('=',
t.identifier('exports'),
t.assignmentExpression('=',
t.memberExpression(
t.identifier('module'), t.identifier('exports')
),
expr.node.right
)
)
),
t.expressionStatement(
t.assignmentExpression('=',
expr.node.left, t.identifier('exports')
)
)
]);
path.remove();
}
}
}
}
program[walked] = true;
}
}
}
});

16
build/rollup-plugin.js Normal file
View File

@ -0,0 +1,16 @@
export default function tweakDefault() {
return {
transformBundle: function (source) {
var lines = source.split('\n');
for (var i = 0; i < lines.length; i++) {
var line = lines[i];
var matches = /^exports\['default'] = (.*);$/.exec(line);
if (matches) {
lines[i] = 'module.exports = exports = ' + matches[1] + ';';
break;
}
}
return lines.join('\n');
}
};
}

271
index.js
View File

@ -1,271 +0,0 @@
/**
* index.js
*
* a request API compatible with window.fetch
*/
var parse_url = require('url').parse;
var resolve_url = require('url').resolve;
var http = require('http');
var https = require('https');
var zlib = require('zlib');
var stream = require('stream');
var Body = require('./lib/body');
var Response = require('./lib/response');
var Headers = require('./lib/headers');
var Request = require('./lib/request');
var FetchError = require('./lib/fetch-error');
// commonjs
module.exports = Fetch;
// es6 default export compatibility
module.exports.default = module.exports;
/**
* Fetch class
*
* @param Mixed url Absolute url or Request instance
* @param Object opts Fetch options
* @return Promise
*/
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 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'];
}
options.counter++;
resolve(Fetch(resolve_url(options.url, res.headers.location), options));
return;
}
// normalize location header for manual redirect mode
var headers = new Headers(res.headers);
if (options.redirect === 'manual' && headers.has('location')) {
headers.set('location', resolve_url(options.url, headers.get('location')));
}
// prepare response
var body = res.pipe(new stream.PassThrough());
var response_options = {
url: options.url
, status: res.statusCode
, statusText: res.statusMessage
, headers: headers
, size: options.size
, timeout: options.timeout
};
// 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;
}
// otherwise, use response as-is
output = new Response(body, response_options);
resolve(output);
return;
});
// accept string, buffer or readable stream as body
// per spec we will call tostring on non-stream objects
if (typeof options.body === 'string') {
req.write(options.body);
req.end();
} else if (options.body instanceof Buffer) {
req.write(options.body);
req.end()
} else if (typeof options.body === 'object' && options.body.pipe) {
options.body.pipe(req);
} else if (typeof options.body === 'object') {
req.write(options.body.toString());
req.end();
} else {
req.end();
}
});
};
/**
* Redirect code matching
*
* @param Number code Status code
* @return Boolean
*/
Fetch.prototype.isRedirect = function(code) {
return code === 301 || code === 302 || code === 303 || code === 307 || code === 308;
}
// expose Promise
Fetch.Promise = global.Promise;
Fetch.Response = Response;
Fetch.Headers = Headers;
Fetch.Request = Request;

View File

@ -1,260 +0,0 @@
/**
* body.js
*
* Body interface provides common methods for Request and Response
*/
var convert = require('encoding').convert;
var bodyStream = require('is-stream');
var PassThrough = require('stream').PassThrough;
var FetchError = require('./fetch-error');
module.exports = Body;
/**
* Body class
*
* @param Stream body Readable stream
* @param Object opts Response options
* @return Void
*/
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;
}
/**
* Decode response as json
*
* @return Promise
*/
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
*
* @return Promise
*/
Body.prototype.text = function() {
return this._decode().then(function(buffer) {
return buffer.toString();
});
};
/**
* Decode response as buffer (non-spec api)
*
* @return Promise
*/
Body.prototype.buffer = function() {
return this._decode();
};
/**
* Decode buffers into utf-8 string
*
* @return Promise
*/
Body.prototype._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());
});
});
};
/**
* Detect buffer encoding and convert to target encoding
* ref: http://www.w3.org/TR/2011/WD-html5-20110113/parsing.html#determining-the-character-encoding
*
* @param String encoding Target encoding
* @return String
*/
Body.prototype._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
);
};
/**
* Clone body given Res/Req instance
*
* @param Mixed instance Response or Request instance
* @return Mixed
*/
Body.prototype._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;
}
// expose Promise
Body.Promise = global.Promise;

View File

@ -1,141 +0,0 @@
/**
* headers.js
*
* Headers class offers convenient helpers
*/
module.exports = Headers;
/**
* Headers class
*
* @param Object headers Response headers
* @return Void
*/
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());
});
}
}
}
/**
* Return first header value given name
*
* @param String name Header name
* @return Mixed
*/
Headers.prototype.get = function(name) {
var list = this._headers[name.toLowerCase()];
return list ? list[0] : null;
};
/**
* Return all header values given name
*
* @param String name Header name
* @return Array
*/
Headers.prototype.getAll = function(name) {
if (!this.has(name)) {
return [];
}
return this._headers[name.toLowerCase()];
};
/**
* Iterate over all headers
*
* @param Function callback Executed for each item with parameters (value, name, thisArg)
* @param Boolean thisArg `this` context for callback function
* @return Void
*/
Headers.prototype.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)
}
/**
* Overwrite header values given name
*
* @param String name Header name
* @param String value Header value
* @return Void
*/
Headers.prototype.set = function(name, value) {
this._headers[name.toLowerCase()] = [value];
};
/**
* Append a value onto existing header
*
* @param String name Header name
* @param String value Header value
* @return Void
*/
Headers.prototype.append = function(name, value) {
if (!this.has(name)) {
this.set(name, value);
return;
}
this._headers[name.toLowerCase()].push(value);
};
/**
* Check for header name existence
*
* @param String name Header name
* @return Boolean
*/
Headers.prototype.has = function(name) {
return this._headers.hasOwnProperty(name.toLowerCase());
};
/**
* Delete all header values given name
*
* @param String name Header name
* @return Void
*/
Headers.prototype['delete'] = function(name) {
delete this._headers[name.toLowerCase()];
};
/**
* Return raw headers (non-spec api)
*
* @return Object
*/
Headers.prototype.raw = function() {
return this._headers;
};

View File

@ -1,75 +0,0 @@
/**
* request.js
*
* Request class contains server only options
*/
var parse_url = require('url').parse;
var Headers = require('./headers');
var Body = require('./body');
module.exports = Request;
/**
* Request class
*
* @param Mixed input Url or Request instance
* @param Object init Custom options
* @return Void
*/
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;
}
Request.prototype = Object.create(Body.prototype);
/**
* Clone this request
*
* @return Request
*/
Request.prototype.clone = function() {
return new Request(this);
};

View File

@ -1,50 +0,0 @@
/**
* response.js
*
* Response class provides content decoding
*/
var http = require('http');
var Headers = require('./headers');
var Body = require('./body');
module.exports = Response;
/**
* Response class
*
* @param Stream body Readable stream
* @param Object opts Response options
* @return Void
*/
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);
}
Response.prototype = Object.create(Body.prototype);
/**
* Clone this response
*
* @return Response
*/
Response.prototype.clone = function() {
return new Response(this._clone(this), {
url: this.url
, status: this.status
, statusText: this.statusText
, headers: this.headers
, ok: this.ok
});
};

View File

@ -2,11 +2,18 @@
"name": "node-fetch",
"version": "1.6.3",
"description": "A light-weight module that brings window.fetch to node.js and io.js",
"main": "index.js",
"main": "lib/index.js",
"jsnext:main": "lib/index.es.js",
"files": [
"lib/index.js",
"lib/index.es.js"
],
"scripts": {
"test": "mocha test/test.js",
"report": "istanbul cover _mocha -- -R spec test/test.js",
"coverage": "istanbul cover _mocha --report lcovonly -- -R spec test/test.js && codecov -f coverage/coverage.json"
"build": "cross-env BABEL_ENV=rollup rollup -c",
"prepublish": "npm run build",
"test": "cross-env BABEL_ENV=test mocha --compilers js:babel-register test/test.js",
"report": "cross-env BABEL_ENV=coverage nyc --reporter lcov --reporter text mocha -R spec test/test.js",
"coverage": "cross-env BABEL_ENV=coverage nyc --reporter json --reporter text mocha -R spec test/test.js && codecov -f coverage/coverage-final.json"
},
"repository": {
"type": "git",
@ -24,18 +31,32 @@
},
"homepage": "https://github.com/bitinn/node-fetch",
"devDependencies": {
"babel-plugin-istanbul": "^3.0.0",
"babel-plugin-transform-runtime": "^6.15.0",
"babel-preset-es2015": "^6.16.0",
"babel-register": "^6.16.3",
"bluebird": "^3.3.4",
"chai": "^3.5.0",
"chai-as-promised": "^5.2.0",
"chai-as-promised": "^6.0.0",
"chai-iterator": "^1.1.1",
"chai-string": "^1.3.0",
"codecov": "^1.0.1",
"cross-env": "2.0.1",
"form-data": ">=1.0.0",
"istanbul": "^0.4.2",
"mocha": "^2.1.0",
"is-builtin-module": "^1.0.0",
"mocha": "^3.1.2",
"nyc": "^10.0.0",
"parted": "^0.1.1",
"promise": "^7.1.1",
"resumer": "0.0.0"
"resumer": "0.0.0",
"rollup": "^0.37.0",
"rollup-plugin-babel": "^2.6.1",
"rollup-plugin-node-resolve": "^2.0.0",
"whatwg-url": "^4.0.0"
},
"dependencies": {
"babel-runtime": "^6.11.6",
"buffer-to-arraybuffer": "0.0.4",
"encoding": "^0.1.11",
"is-stream": "^1.0.1"
}

27
rollup.config.js Normal file
View File

@ -0,0 +1,27 @@
import isBuiltin from 'is-builtin-module';
import babel from 'rollup-plugin-babel';
import resolve from 'rollup-plugin-node-resolve';
import tweakDefault from './build/rollup-plugin';
process.env.BABEL_ENV = 'rollup';
export default {
entry: 'src/index.js',
plugins: [
babel({
runtimeHelpers: true
}),
tweakDefault()
],
targets: [
{ dest: 'lib/index.js', format: 'cjs' },
{ dest: 'lib/index.es.js', format: 'es' }
],
external: function (id) {
if (isBuiltin(id)) {
return true;
}
id = id.split('/').slice(0, id[0] === '@' ? 2 : 1).join('/');
return !!require('./package.json').dependencies[id];
}
};

104
src/blob.js Normal file
View File

@ -0,0 +1,104 @@
// Based on https://github.com/tmpvar/jsdom/blob/aa85b2abf07766ff7bf5c1f6daafb3726f2f2db5/lib/jsdom/living/blob.js
// (MIT licensed)
export const BUFFER = Symbol('buffer');
const TYPE = Symbol('type');
const CLOSED = Symbol('closed');
export default class Blob {
constructor() {
Object.defineProperty(this, Symbol.toStringTag, {
value: 'Blob',
writable: false,
enumerable: false,
configurable: true
});
this[CLOSED] = false;
this[TYPE] = '';
const blobParts = arguments[0];
const options = arguments[1];
const buffers = [];
if (blobParts) {
const a = blobParts;
const length = Number(a.length);
for (let i = 0; i < length; i++) {
const element = a[i];
let buffer;
if (element instanceof Buffer) {
buffer = element;
} else if (ArrayBuffer.isView(element)) {
buffer = new Buffer(new Uint8Array(element.buffer, element.byteOffset, element.byteLength));
} else if (element instanceof ArrayBuffer) {
buffer = new Buffer(new Uint8Array(element));
} else if (element instanceof Blob) {
buffer = element[BUFFER];
} else {
buffer = new Buffer(typeof element === 'string' ? element : String(element));
}
buffers.push(buffer);
}
}
this[BUFFER] = Buffer.concat(buffers);
let type = options && options.type !== undefined && String(options.type).toLowerCase();
if (type && !/[^\u0020-\u007E]/.test(type)) {
this[TYPE] = type;
}
}
get size() {
return this[CLOSED] ? 0 : this[BUFFER].length;
}
get type() {
return this[TYPE];
}
get isClosed() {
return this[CLOSED];
}
slice() {
const size = this.size;
const start = arguments[0];
const end = arguments[1];
let relativeStart, relativeEnd;
if (start === undefined) {
relativeStart = 0;
} else if (start < 0) {
relativeStart = Math.max(size + start, 0);
} else {
relativeStart = Math.min(start, size);
}
if (end === undefined) {
relativeEnd = size;
} else if (end < 0) {
relativeEnd = Math.max(size + end, 0);
} else {
relativeEnd = Math.min(end, size);
}
const span = Math.max(relativeEnd - relativeStart, 0);
const buffer = this[BUFFER];
const slicedBuffer = buffer.slice(
relativeStart,
relativeStart + span
);
const blob = new Blob([], { type: arguments[2] });
blob[BUFFER] = slicedBuffer;
blob[CLOSED] = this[CLOSED];
return blob;
}
close() {
this[CLOSED] = true;
}
}
Object.defineProperty(Blob.prototype, Symbol.toStringTag, {
value: 'BlobPrototype',
writable: false,
enumerable: false,
configurable: true
});

387
src/body.js Normal file
View File

@ -0,0 +1,387 @@
/**
* body.js
*
* Body interface provides common methods for Request and Response
*/
import {convert} from 'encoding';
import bodyStream from 'is-stream';
import toArrayBuffer from 'buffer-to-arraybuffer';
import {PassThrough} from 'stream';
import Blob, {BUFFER} from './blob.js';
import FetchError from './fetch-error.js';
const DISTURBED = Symbol('disturbed');
const CONSUME_BODY = Symbol('consumeBody');
/**
* Body class
*
* @param Stream body Readable stream
* @param Object opts Response options
* @return Void
*/
export default class Body {
constructor(body, {
size = 0,
timeout = 0
} = {}) {
if (body == null) {
// body is undefined or null
body = null;
} else if (typeof body === 'string') {
// body is string
} else if (body instanceof Blob) {
// body is blob
} else if (Buffer.isBuffer(body)) {
// body is buffer
} else if (bodyStream(body)) {
// body is stream
} else {
// none of the above
// coerce to string
body = String(body);
}
this.body = body;
this[DISTURBED] = false;
this.size = size;
this.timeout = timeout;
}
get bodyUsed() {
return this[DISTURBED];
}
/**
* Decode response as ArrayBuffer
*
* @return Promise
*/
arrayBuffer() {
return this[CONSUME_BODY]().then(buf => toArrayBuffer(buf));
}
/**
* Return raw response as Blob
*
* @return Promise
*/
blob() {
let ct = this.headers && this.headers.get('content-type') || '';
return this[CONSUME_BODY]().then(buf => Object.assign(
// Prevent copying
new Blob([], {
type: ct.toLowerCase()
}),
{
[BUFFER]: buf
}
));
}
/**
* Decode response as json
*
* @return Promise
*/
json() {
return this[CONSUME_BODY]().then(buffer => JSON.parse(buffer.toString()));
}
/**
* Decode response as text
*
* @return Promise
*/
text() {
return this[CONSUME_BODY]().then(buffer => buffer.toString());
}
/**
* Decode response as buffer (non-spec api)
*
* @return Promise
*/
buffer() {
return this[CONSUME_BODY]();
}
/**
* Decode response as text, while automatically detecting the encoding and
* trying to decode to UTF-8 (non-spec api)
*
* @return Promise
*/
textConverted() {
return this[CONSUME_BODY]().then(buffer => convertBody(buffer, this.headers));
}
/**
* Decode buffers into utf-8 string
*
* @return Promise
*/
[CONSUME_BODY]() {
if (this[DISTURBED]) {
return Body.Promise.reject(new Error(`body used already for: ${this.url}`));
}
this[DISTURBED] = true;
// body is null
if (this.body === null) {
return Body.Promise.resolve(new Buffer(0));
}
// body is string
if (typeof this.body === 'string') {
return Body.Promise.resolve(new Buffer(this.body));
}
// body is blob
if (this.body instanceof Blob) {
return Body.Promise.resolve(this.body[BUFFER]);
}
// body is buffer
if (Buffer.isBuffer(this.body)) {
return Body.Promise.resolve(this.body);
}
// istanbul ignore if: should never happen
if (!bodyStream(this.body)) {
return Body.Promise.resolve(new Buffer(0));
}
// body is stream
// get ready to actually consume the body
let accum = [];
let accumBytes = 0;
let abort = false;
return new Body.Promise((resolve, reject) => {
let resTimeout;
// allow timeout on slow response body
if (this.timeout) {
resTimeout = setTimeout(() => {
abort = true;
reject(new FetchError(`Response timeout while trying to fetch ${this.url} (over ${this.timeout}ms)`, 'body-timeout'));
}, this.timeout);
}
// handle stream error, such as incorrect content-encoding
this.body.on('error', err => {
reject(new FetchError(`Invalid response body while trying to fetch ${this.url}: ${err.message}`, 'system', err));
});
this.body.on('data', chunk => {
if (abort || chunk === null) {
return;
}
if (this.size && accumBytes + chunk.length > this.size) {
abort = true;
reject(new FetchError(`content size at ${this.url} over limit: ${this.size}`, 'max-size'));
return;
}
accumBytes += chunk.length;
accum.push(chunk);
});
this.body.on('end', () => {
if (abort) {
return;
}
clearTimeout(resTimeout);
resolve(Buffer.concat(accum));
});
});
}
}
/**
* Detect buffer encoding and convert to target encoding
* ref: http://www.w3.org/TR/2011/WD-html5-20110113/parsing.html#determining-the-character-encoding
*
* @param Buffer buffer Incoming buffer
* @param String encoding Target encoding
* @return String
*/
function convertBody(buffer, headers) {
const ct = headers.get('content-type');
let charset = 'utf-8';
let res, str;
// header
if (ct) {
res = /charset=([^;]*)/i.exec(ct);
}
// no charset in content type, peek at response body for at most 1024 bytes
str = buffer.slice(0, 1024).toString();
// 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
, 'UTF-8'
, charset
).toString();
}
/**
* Clone body given Res/Req instance
*
* @param Mixed instance Response or Request instance
* @return Mixed
*/
export function clone(instance) {
let p1, p2;
let 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;
}
/**
* Performs the operation "extract a `Content-Type` value from |object|" as
* specified in the specification:
* https://fetch.spec.whatwg.org/#concept-bodyinit-extract
*
* This function assumes that instance.body is present and non-null.
*
* @param Mixed instance Response or Request instance
*/
export function extractContentType(instance) {
const {body} = instance;
// istanbul ignore if: Currently, because of a guard in Request, body
// can never be null. Included here for completeness.
if (body === null) {
// body is null
return null;
} else if (typeof body === 'string') {
// body is string
return 'text/plain;charset=UTF-8';
} else if (body instanceof Blob) {
// body is blob
return body.type || null;
} else if (Buffer.isBuffer(body)) {
// body is buffer
return null;
} else if (typeof body.getBoundary === 'function') {
// detect form data input from form-data module
return `multipart/form-data;boundary=${body.getBoundary()}`;
} else {
// body is stream
// can't really do much about this
return null;
}
}
export function getTotalBytes(instance) {
const {body} = instance;
if (body === null) {
// body is null
return 0;
} else if (typeof body === 'string') {
// body is string
return Buffer.byteLength(body);
} else if (body instanceof Blob) {
// body is blob
return body.size;
} else if (Buffer.isBuffer(body)) {
// body is buffer
return body.length;
} else if (body && typeof body.getLengthSync === 'function') {
// detect form data input from form-data module
if (body._lengthRetrievers && body._lengthRetrievers.length == 0 || // 1.x
body.hasKnownLength && body.hasKnownLength()) { // 2.x
return body.getLengthSync();
}
return null;
} else {
// body is stream
// can't really do much about this
return null;
}
}
export function writeToStream(dest, instance) {
const {body} = instance;
if (body === null) {
// body is null
dest.end();
} else if (typeof body === 'string') {
// body is string
dest.write(body);
dest.end();
} else if (body instanceof Blob) {
// body is blob
dest.write(body[BUFFER]);
dest.end();
} else if (Buffer.isBuffer(body)) {
// body is buffer
dest.write(body);
dest.end()
} else {
// body is stream
body.pipe(dest);
}
}
// expose Promise
Body.Promise = global.Promise;

111
src/common.js Normal file
View File

@ -0,0 +1,111 @@
/**
* A set of utilities borrowed from Node.js' _http_common.js
*/
/**
* Verifies that the given val is a valid HTTP token
* per the rules defined in RFC 7230
* See https://tools.ietf.org/html/rfc7230#section-3.2.6
*
* Allowed characters in an HTTP token:
* ^_`a-z 94-122
* A-Z 65-90
* - 45
* 0-9 48-57
* ! 33
* #$%&' 35-39
* *+ 42-43
* . 46
* | 124
* ~ 126
*
* This implementation of checkIsHttpToken() loops over the string instead of
* using a regular expression since the former is up to 180% faster with v8 4.9
* depending on the string length (the shorter the string, the larger the
* performance difference)
*
* Additionally, checkIsHttpToken() is currently designed to be inlinable by v8,
* so take care when making changes to the implementation so that the source
* code size does not exceed v8's default max_inlined_source_size setting.
**/
/* istanbul ignore next */
function isValidTokenChar(ch) {
if (ch >= 94 && ch <= 122)
return true;
if (ch >= 65 && ch <= 90)
return true;
if (ch === 45)
return true;
if (ch >= 48 && ch <= 57)
return true;
if (ch === 34 || ch === 40 || ch === 41 || ch === 44)
return false;
if (ch >= 33 && ch <= 46)
return true;
if (ch === 124 || ch === 126)
return true;
return false;
}
/* istanbul ignore next */
function checkIsHttpToken(val) {
if (typeof val !== 'string' || val.length === 0)
return false;
if (!isValidTokenChar(val.charCodeAt(0)))
return false;
const len = val.length;
if (len > 1) {
if (!isValidTokenChar(val.charCodeAt(1)))
return false;
if (len > 2) {
if (!isValidTokenChar(val.charCodeAt(2)))
return false;
if (len > 3) {
if (!isValidTokenChar(val.charCodeAt(3)))
return false;
for (var i = 4; i < len; i++) {
if (!isValidTokenChar(val.charCodeAt(i)))
return false;
}
}
}
}
return true;
}
export { checkIsHttpToken };
/**
* True if val contains an invalid field-vchar
* field-value = *( field-content / obs-fold )
* field-content = field-vchar [ 1*( SP / HTAB ) field-vchar ]
* field-vchar = VCHAR / obs-text
*
* checkInvalidHeaderChar() is currently designed to be inlinable by v8,
* so take care when making changes to the implementation so that the source
* code size does not exceed v8's default max_inlined_source_size setting.
**/
/* istanbul ignore next */
function checkInvalidHeaderChar(val) {
val += '';
if (val.length < 1)
return false;
var c = val.charCodeAt(0);
if ((c <= 31 && c !== 9) || c > 255 || c === 127)
return true;
if (val.length < 2)
return false;
c = val.charCodeAt(1);
if ((c <= 31 && c !== 9) || c > 255 || c === 127)
return true;
if (val.length < 3)
return false;
c = val.charCodeAt(2);
if ((c <= 31 && c !== 9) || c > 255 || c === 127)
return true;
for (var i = 3; i < val.length; ++i) {
c = val.charCodeAt(i);
if ((c <= 31 && c !== 9) || c > 255 || c === 127)
return true;
}
return false;
}
export { checkInvalidHeaderChar };

View File

@ -5,8 +5,6 @@
* FetchError interface for operational errors
*/
module.exports = FetchError;
/**
* Create FetchError instance
*
@ -15,12 +13,9 @@ module.exports = FetchError;
* @param String systemError For Node.js system error
* @return FetchError
*/
function FetchError(message, type, systemError) {
export default function FetchError(message, type, systemError) {
Error.call(this, message);
// hide custom error implementation details from end-users
Error.captureStackTrace(this, this.constructor);
this.name = this.constructor.name;
this.message = message;
this.type = type;
@ -29,6 +24,10 @@ function FetchError(message, type, systemError) {
this.code = this.errno = systemError.code;
}
// hide custom error implementation details from end-users
Error.captureStackTrace(this, this.constructor);
}
require('util').inherits(FetchError, Error);
FetchError.prototype = Object.create(Error.prototype);
FetchError.prototype.constructor = FetchError;
FetchError.prototype.name = 'FetchError';

309
src/headers.js Normal file
View File

@ -0,0 +1,309 @@
/**
* headers.js
*
* Headers class offers convenient helpers
*/
import { checkIsHttpToken, checkInvalidHeaderChar } from './common.js';
function sanitizeName(name) {
name += '';
if (!checkIsHttpToken(name)) {
throw new TypeError(`${name} is not a legal HTTP header name`);
}
return name.toLowerCase();
}
function sanitizeValue(value) {
value += '';
if (checkInvalidHeaderChar(value)) {
throw new TypeError(`${value} is not a legal HTTP header value`);
}
return value;
}
export const MAP = Symbol('map');
const FOLLOW_SPEC = Symbol('followSpec');
export default class Headers {
/**
* Headers class
*
* @param Object headers Response headers
* @return Void
*/
constructor(headers) {
this[MAP] = Object.create(null);
this[FOLLOW_SPEC] = Headers.FOLLOW_SPEC;
// Headers
if (headers instanceof Headers) {
let init = headers.raw();
for (let name of Object.keys(init)) {
for (let value of init[name]) {
this.append(name, value);
}
}
} else if (typeof headers === 'object' && headers[Symbol.iterator]) {
// array of tuples
for (let el of headers) {
if (typeof el !== 'object' || !el[Symbol.iterator]) {
throw new TypeError('Header pairs must be an iterable object');
}
el = Array.from(el);
if (el.length !== 2) {
throw new TypeError('Header pairs must contain exactly two items');
}
this.append(el[0], el[1]);
}
} else if (typeof headers === 'object') {
// plain object
for (const prop of Object.keys(headers)) {
// We don't worry about converting prop to ByteString here as append()
// will handle it.
this.append(prop, headers[prop]);
}
} else if (headers != null) {
throw new TypeError('Provided initializer must be an object');
}
Object.defineProperty(this, Symbol.toStringTag, {
value: 'Headers',
writable: false,
enumerable: false,
configurable: true
});
}
/**
* Return first header value given name
*
* @param String name Header name
* @return Mixed
*/
get(name) {
const list = this[MAP][sanitizeName(name)];
if (!list) {
return null;
}
return this[FOLLOW_SPEC] ? list.join(',') : list[0];
}
/**
* Return all header values given name
*
* @param String name Header name
* @return Array
*/
getAll(name) {
if (!this.has(name)) {
return [];
}
return this[MAP][sanitizeName(name)];
}
/**
* Iterate over all headers
*
* @param Function callback Executed for each item with parameters (value, name, thisArg)
* @param Boolean thisArg `this` context for callback function
* @return Void
*/
forEach(callback, thisArg = undefined) {
let pairs = getHeaderPairs(this);
let i = 0;
while (i < pairs.length) {
const [name, value] = pairs[i];
callback.call(thisArg, value, name, this);
pairs = getHeaderPairs(this);
i++;
}
}
/**
* Overwrite header values given name
*
* @param String name Header name
* @param String value Header value
* @return Void
*/
set(name, value) {
this[MAP][sanitizeName(name)] = [sanitizeValue(value)];
}
/**
* Append a value onto existing header
*
* @param String name Header name
* @param String value Header value
* @return Void
*/
append(name, value) {
if (!this.has(name)) {
this.set(name, value);
return;
}
this[MAP][sanitizeName(name)].push(sanitizeValue(value));
}
/**
* Check for header name existence
*
* @param String name Header name
* @return Boolean
*/
has(name) {
return !!this[MAP][sanitizeName(name)];
}
/**
* Delete all header values given name
*
* @param String name Header name
* @return Void
*/
delete(name) {
delete this[MAP][sanitizeName(name)];
};
/**
* Return raw headers (non-spec api)
*
* @return Object
*/
raw() {
return this[MAP];
}
/**
* Get an iterator on keys.
*
* @return Iterator
*/
keys() {
return createHeadersIterator(this, 'key');
}
/**
* Get an iterator on values.
*
* @return Iterator
*/
values() {
return createHeadersIterator(this, 'value');
}
/**
* Get an iterator on entries.
*
* This is the default iterator of the Headers object.
*
* @return Iterator
*/
[Symbol.iterator]() {
return createHeadersIterator(this, 'key+value');
}
}
Headers.prototype.entries = Headers.prototype[Symbol.iterator];
Object.defineProperty(Headers.prototype, Symbol.toStringTag, {
value: 'HeadersPrototype',
writable: false,
enumerable: false,
configurable: true
});
function getHeaderPairs(headers, kind) {
if (headers[FOLLOW_SPEC]) {
const keys = Object.keys(headers[MAP]).sort();
return keys.map(
kind === 'key' ?
k => [k] :
k => [k, headers.get(k)]
);
}
const values = [];
for (let name in headers[MAP]) {
for (let value of headers[MAP][name]) {
values.push([name, value]);
}
}
return values;
}
const INTERNAL = Symbol('internal');
function createHeadersIterator(target, kind) {
const iterator = Object.create(HeadersIteratorPrototype);
iterator[INTERNAL] = {
target,
kind,
index: 0
};
return iterator;
}
const HeadersIteratorPrototype = Object.setPrototypeOf({
next() {
// istanbul ignore if
if (!this ||
Object.getPrototypeOf(this) !== HeadersIteratorPrototype) {
throw new TypeError('Value of `this` is not a HeadersIterator');
}
const {
target,
kind,
index
} = this[INTERNAL];
const values = getHeaderPairs(target, kind);
const len = values.length;
if (index >= len) {
return {
value: undefined,
done: true
};
}
const pair = values[index];
this[INTERNAL].index = index + 1;
let result;
if (kind === 'key') {
result = pair[0];
} else if (kind === 'value') {
result = pair[1];
} else {
result = pair;
}
return {
value: result,
done: false
};
}
}, Object.getPrototypeOf(
Object.getPrototypeOf([][Symbol.iterator]())
));
// On Node.js v0.12 the %IteratorPrototype% object is broken
if (typeof HeadersIteratorPrototype[Symbol.iterator] !== 'function') {
HeadersIteratorPrototype[Symbol.iterator] = function () {
return this;
};
}
Object.defineProperty(HeadersIteratorPrototype, Symbol.toStringTag, {
value: 'HeadersIterator',
writable: false,
enumerable: false,
configurable: true
});
Headers.FOLLOW_SPEC = false;

215
src/index.js Normal file
View File

@ -0,0 +1,215 @@
/**
* index.js
*
* a request API compatible with window.fetch
*/
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, { writeToStream } from './body';
import Response from './response';
import Headers from './headers';
import Request, { getNodeRequestOptions } from './request';
import FetchError from './fetch-error';
/**
* Fetch function
*
* @param Mixed url Absolute url or Request instance
* @param Object opts Fetch options
* @return Promise
*/
export default function 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;
Headers.FOLLOW_SPEC = fetch.FOLLOW_SPEC;
// wrap http.request into fetch
return new fetch.Promise((resolve, reject) => {
// build request object
const request = new Request(url, opts);
const options = getNodeRequestOptions(request);
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');
}
const send = (options.protocol === 'https:' ? https : http).request;
// 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
const req = send(options);
let reqTimeout;
if (request.timeout) {
req.once('socket', socket => {
reqTimeout = setTimeout(() => {
req.abort();
reject(new FetchError(`network timeout at: ${request.url}`, 'request-timeout'));
}, request.timeout);
});
}
req.on('error', err => {
clearTimeout(reqTimeout);
reject(new FetchError(`request to ${request.url} failed, reason: ${err.message}`, 'system', err));
});
req.on('response', res => {
clearTimeout(reqTimeout);
// handle redirect
if (fetch.isRedirect(res.statusCode) && request.redirect !== 'manual') {
if (request.redirect === 'error') {
reject(new FetchError(`redirect mode is set to error: ${request.url}`, 'no-redirect'));
return;
}
if (request.counter >= request.follow) {
reject(new FetchError(`maximum redirect reached at: ${request.url}`, 'max-redirect'));
return;
}
if (!res.headers.location) {
reject(new FetchError(`redirect location header missing at: ${request.url}`, 'invalid-redirect'));
return;
}
// 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) && request.method === 'POST'))
{
request.method = 'GET';
request.body = null;
request.headers.delete('content-length');
}
request.counter++;
resolve(fetch(resolve_url(request.url, res.headers.location), request));
return;
}
// normalize location header for manual redirect mode
const headers = new Headers();
for (const name of Object.keys(res.headers)) {
if (Array.isArray(res.headers[name])) {
for (const val of res.headers[name]) {
headers.append(name, val);
}
} else {
headers.append(name, res.headers[name]);
}
}
if (request.redirect === 'manual' && headers.has('location')) {
headers.set('location', resolve_url(request.url, headers.get('location')));
}
// prepare response
let body = res.pipe(new PassThrough());
const response_options = {
url: request.url
, status: res.statusCode
, statusText: res.statusMessage
, headers: headers
, size: request.size
, timeout: request.timeout
};
// response object
let 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 (!request.compress || request.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
let 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
const raw = res.pipe(new PassThrough());
raw.once('data', 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;
}
// otherwise, use response as-is
output = new Response(body, response_options);
resolve(output);
return;
});
writeToStream(req, request);
});
};
/**
* Redirect code matching
*
* @param Number code Status code
* @return Boolean
*/
fetch.isRedirect = code => code === 301 || code === 302 || code === 303 || code === 307 || code === 308;
// expose Promise
fetch.Promise = global.Promise;
/**
* Option to make newly constructed Headers objects conformant to the
* **latest** version of the Fetch Standard. Note, that most other
* implementations of fetch() have not yet been updated to the latest
* version, so enabling this option almost certainly breaks any isomorphic
* attempt. Also, changing this variable will only affect new Headers
* objects; existing objects are not affected.
*/
fetch.FOLLOW_SPEC = false;
export {
Headers,
Request,
Response
};

147
src/request.js Normal file
View File

@ -0,0 +1,147 @@
/**
* request.js
*
* Request class contains server only options
*/
import { format as format_url, parse as parse_url } from 'url';
import Headers from './headers.js';
import Body, { clone, extractContentType, getTotalBytes } from './body';
const PARSED_URL = Symbol('url');
/**
* Request class
*
* @param Mixed input Url or Request instance
* @param Object init Custom options
* @return Void
*/
export default class Request extends Body {
constructor(input, init = {}) {
let parsedURL;
// normalize input
if (!(input instanceof Request)) {
if (input && input.href) {
// in order to support Node.js' Url objects; though WHATWG's URL objects
// will fall into this branch also (since their `toString()` will return
// `href` property anyway)
parsedURL = parse_url(input.href);
} else {
// coerce input to a string before attempting to parse
parsedURL = parse_url(`${input}`);
}
input = {};
} else {
parsedURL = parse_url(input.url);
}
let method = init.method || input.method || 'GET';
if ((init.body != null || input instanceof Request && input.body !== null) &&
(method === 'GET' || method === 'HEAD')) {
throw new TypeError('Request with GET/HEAD method cannot have body');
}
let inputBody = init.body != null ?
init.body :
input instanceof Request && input.body !== null ?
clone(input) :
null;
super(inputBody, {
timeout: init.timeout || input.timeout || 0,
size: init.size || input.size || 0
});
// fetch spec options
this.method = method;
this.redirect = init.redirect || input.redirect || 'follow';
this.headers = new Headers(init.headers || input.headers || {});
if (init.body != null) {
const contentType = extractContentType(this);
if (contentType !== null && !this.headers.has('Content-Type')) {
this.headers.append('Content-Type', contentType);
}
}
// 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;
this[PARSED_URL] = parsedURL;
Object.defineProperty(this, Symbol.toStringTag, {
value: 'Request',
writable: false,
enumerable: false,
configurable: true
});
}
get url() {
return format_url(this[PARSED_URL]);
}
/**
* Clone this request
*
* @return Request
*/
clone() {
return new Request(this);
}
}
Object.defineProperty(Request.prototype, Symbol.toStringTag, {
value: 'RequestPrototype',
writable: false,
enumerable: false,
configurable: true
});
function normalizeHeaders(request) {
const headers = new Headers(request.headers);
if (request.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') && !request.agent) {
headers.set('connection', 'close');
}
if (!headers.has('accept')) {
headers.set('accept', '*/*');
}
if (!headers.has('content-length') && /post|put|patch|delete/i.test(request.method)) {
const totalBytes = getTotalBytes(request);
if (typeof totalBytes === 'number') {
headers.set('content-length', totalBytes);
}
}
return headers;
}
export function getNodeRequestOptions(request) {
return Object.assign({}, request[PARSED_URL], {
method: request.method,
headers: normalizeHeaders(request).raw(),
agent: request.agent
});
}

66
src/response.js Normal file
View File

@ -0,0 +1,66 @@
/**
* response.js
*
* Response class provides content decoding
*/
import { STATUS_CODES } from 'http';
import Headers from './headers.js';
import Body, { clone } from './body';
/**
* Response class
*
* @param Stream body Readable stream
* @param Object opts Response options
* @return Void
*/
export default class Response extends Body {
constructor(body = null, opts = {}) {
super(body, opts);
this.url = opts.url;
this.status = opts.status || 200;
this.statusText = opts.statusText || STATUS_CODES[this.status];
this.headers = new Headers(opts.headers);
Object.defineProperty(this, Symbol.toStringTag, {
value: 'Response',
writable: false,
enumerable: false,
configurable: true
});
}
/**
* Convenience property representing if the request ended normally
*/
get ok() {
return this.status >= 200 && this.status < 300;
}
/**
* Clone this response
*
* @return Response
*/
clone() {
return new Response(clone(this), {
url: this.url
, status: this.status
, statusText: this.statusText
, headers: this.headers
, ok: this.ok
});
}
}
Object.defineProperty(Response.prototype, Symbol.toStringTag, {
value: 'ResponsePrototype',
writable: false,
enumerable: false,
configurable: true
});

View File

@ -1,337 +1,336 @@
import repeat from 'babel-runtime/core-js/string/repeat';
import * as http from 'http';
import { parse } from 'url';
import * as zlib from 'zlib';
import * as stream from 'stream';
import { convert } from 'encoding';
import { multipart as Multipart } from 'parted';
var http = require('http');
var parse = require('url').parse;
var zlib = require('zlib');
var stream = require('stream');
var convert = require('encoding').convert;
var Multipart = require('parted').multipart;
module.exports = TestServer;
function TestServer() {
this.server = http.createServer(this.router);
this.port = 30001;
this.hostname = 'localhost';
this.server.on('error', function(err) {
console.log(err.stack);
});
this.server.on('connection', function(socket) {
socket.setTimeout(1500);
});
}
TestServer.prototype.start = function(cb) {
this.server.listen(this.port, this.hostname, cb);
}
TestServer.prototype.stop = function(cb) {
this.server.close(cb);
}
TestServer.prototype.router = function(req, res) {
var p = parse(req.url).pathname;
if (p === '/hello') {
res.statusCode = 200;
res.setHeader('Content-Type', 'text/plain');
res.end('world');
}
if (p === '/plain') {
res.statusCode = 200;
res.setHeader('Content-Type', 'text/plain');
res.end('text');
}
if (p === '/options') {
res.statusCode = 200;
res.setHeader('Allow', 'GET, HEAD, OPTIONS');
res.end('hello world');
}
if (p === '/html') {
res.statusCode = 200;
res.setHeader('Content-Type', 'text/html');
res.end('<html></html>');
}
if (p === '/json') {
res.statusCode = 200;
res.setHeader('Content-Type', 'application/json');
res.end(JSON.stringify({
name: 'value'
}));
}
if (p === '/gzip') {
res.statusCode = 200;
res.setHeader('Content-Type', 'text/plain');
res.setHeader('Content-Encoding', 'gzip');
zlib.gzip('hello world', function(err, buffer) {
res.end(buffer);
export default class TestServer {
constructor() {
this.server = http.createServer(this.router);
this.port = 30001;
this.hostname = 'localhost';
this.server.on('error', function(err) {
console.log(err.stack);
});
this.server.on('connection', function(socket) {
socket.setTimeout(1500);
});
}
if (p === '/deflate') {
res.statusCode = 200;
res.setHeader('Content-Type', 'text/plain');
res.setHeader('Content-Encoding', 'deflate');
zlib.deflate('hello world', function(err, buffer) {
res.end(buffer);
});
start(cb) {
this.server.listen(this.port, this.hostname, cb);
}
if (p === '/deflate-raw') {
res.statusCode = 200;
res.setHeader('Content-Type', 'text/plain');
res.setHeader('Content-Encoding', 'deflate');
zlib.deflateRaw('hello world', function(err, buffer) {
res.end(buffer);
});
stop(cb) {
this.server.close(cb);
}
if (p === '/sdch') {
res.statusCode = 200;
res.setHeader('Content-Type', 'text/plain');
res.setHeader('Content-Encoding', 'sdch');
res.end('fake sdch string');
}
router(req, res) {
let p = parse(req.url).pathname;
if (p === '/invalid-content-encoding') {
res.statusCode = 200;
res.setHeader('Content-Type', 'text/plain');
res.setHeader('Content-Encoding', 'gzip');
res.end('fake gzip string');
}
if (p === '/hello') {
res.statusCode = 200;
res.setHeader('Content-Type', 'text/plain');
res.end('world');
}
if (p === '/timeout') {
setTimeout(function() {
if (p === '/plain') {
res.statusCode = 200;
res.setHeader('Content-Type', 'text/plain');
res.end('text');
}, 1000);
}
}
if (p === '/slow') {
res.statusCode = 200;
res.setHeader('Content-Type', 'text/plain');
res.write('test');
setTimeout(function() {
res.end('test');
}, 1000);
}
if (p === '/options') {
res.statusCode = 200;
res.setHeader('Allow', 'GET, HEAD, OPTIONS');
res.end('hello world');
}
if (p === '/cookie') {
res.statusCode = 200;
res.setHeader('Set-Cookie', ['a=1', 'b=1']);
res.end('cookie');
}
if (p === '/html') {
res.statusCode = 200;
res.setHeader('Content-Type', 'text/html');
res.end('<html></html>');
}
if (p === '/size/chunk') {
res.statusCode = 200;
res.setHeader('Content-Type', 'text/plain');
setTimeout(function() {
if (p === '/json') {
res.statusCode = 200;
res.setHeader('Content-Type', 'application/json');
res.end(JSON.stringify({
name: 'value'
}));
}
if (p === '/gzip') {
res.statusCode = 200;
res.setHeader('Content-Type', 'text/plain');
res.setHeader('Content-Encoding', 'gzip');
zlib.gzip('hello world', function(err, buffer) {
res.end(buffer);
});
}
if (p === '/deflate') {
res.statusCode = 200;
res.setHeader('Content-Type', 'text/plain');
res.setHeader('Content-Encoding', 'deflate');
zlib.deflate('hello world', function(err, buffer) {
res.end(buffer);
});
}
if (p === '/deflate-raw') {
res.statusCode = 200;
res.setHeader('Content-Type', 'text/plain');
res.setHeader('Content-Encoding', 'deflate');
zlib.deflateRaw('hello world', function(err, buffer) {
res.end(buffer);
});
}
if (p === '/sdch') {
res.statusCode = 200;
res.setHeader('Content-Type', 'text/plain');
res.setHeader('Content-Encoding', 'sdch');
res.end('fake sdch string');
}
if (p === '/invalid-content-encoding') {
res.statusCode = 200;
res.setHeader('Content-Type', 'text/plain');
res.setHeader('Content-Encoding', 'gzip');
res.end('fake gzip string');
}
if (p === '/timeout') {
setTimeout(function() {
res.statusCode = 200;
res.setHeader('Content-Type', 'text/plain');
res.end('text');
}, 1000);
}
if (p === '/slow') {
res.statusCode = 200;
res.setHeader('Content-Type', 'text/plain');
res.write('test');
}, 50);
setTimeout(function() {
res.end('test');
}, 100);
}
if (p === '/size/long') {
res.statusCode = 200;
res.setHeader('Content-Type', 'text/plain');
res.end('testtest');
}
if (p === '/encoding/gbk') {
res.statusCode = 200;
res.setHeader('Content-Type', 'text/html');
res.end(convert('<meta charset="gbk"><div>中文</div>', 'gbk'));
}
if (p === '/encoding/gb2312') {
res.statusCode = 200;
res.setHeader('Content-Type', 'text/html');
res.end(convert('<meta http-equiv="Content-Type" content="text/html; charset=gb2312"><div>中文</div>', 'gb2312'));
}
if (p === '/encoding/shift-jis') {
res.statusCode = 200;
res.setHeader('Content-Type', 'text/html; charset=Shift-JIS');
res.end(convert('<div>日本語</div>', 'Shift_JIS'));
}
if (p === '/encoding/euc-jp') {
res.statusCode = 200;
res.setHeader('Content-Type', 'text/xml');
res.end(convert('<?xml version="1.0" encoding="EUC-JP"?><title>日本語</title>', 'EUC-JP'));
}
if (p === '/encoding/utf8') {
res.statusCode = 200;
res.end('中文');
}
if (p === '/encoding/order1') {
res.statusCode = 200;
res.setHeader('Content-Type', 'charset=gbk; text/plain');
res.end(convert('中文', 'gbk'));
}
if (p === '/encoding/order2') {
res.statusCode = 200;
res.setHeader('Content-Type', 'text/plain; charset=gbk; qs=1');
res.end(convert('中文', 'gbk'));
}
if (p === '/encoding/chunked') {
res.statusCode = 200;
res.setHeader('Content-Type', 'text/html');
res.setHeader('Transfer-Encoding', 'chunked');
var padding = 'a';
for (var i = 0; i < 10; i++) {
res.write(padding);
setTimeout(function() {
res.end('test');
}, 1000);
}
res.end(convert('<meta http-equiv="Content-Type" content="text/html; charset=Shift_JIS" /><div>日本語</div>', 'Shift_JIS'));
}
if (p === '/encoding/invalid') {
res.statusCode = 200;
res.setHeader('Content-Type', 'text/html');
res.setHeader('Transfer-Encoding', 'chunked');
// because node v0.12 doesn't have str.repeat
var padding = new Array(120 + 1).join('a');
for (var i = 0; i < 10; i++) {
res.write(padding);
if (p === '/cookie') {
res.statusCode = 200;
res.setHeader('Set-Cookie', ['a=1', 'b=1']);
res.end('cookie');
}
res.end(convert('中文', 'gbk'));
}
if (p === '/redirect/301') {
res.statusCode = 301;
res.setHeader('Location', '/inspect');
res.end();
}
if (p === '/size/chunk') {
res.statusCode = 200;
res.setHeader('Content-Type', 'text/plain');
setTimeout(function() {
res.write('test');
}, 50);
setTimeout(function() {
res.end('test');
}, 100);
}
if (p === '/redirect/302') {
res.statusCode = 302;
res.setHeader('Location', '/inspect');
res.end();
}
if (p === '/size/long') {
res.statusCode = 200;
res.setHeader('Content-Type', 'text/plain');
res.end('testtest');
}
if (p === '/redirect/303') {
res.statusCode = 303;
res.setHeader('Location', '/inspect');
res.end();
}
if (p === '/encoding/gbk') {
res.statusCode = 200;
res.setHeader('Content-Type', 'text/html');
res.end(convert('<meta charset="gbk"><div>中文</div>', 'gbk'));
}
if (p === '/redirect/307') {
res.statusCode = 307;
res.setHeader('Location', '/inspect');
res.end();
}
if (p === '/encoding/gb2312') {
res.statusCode = 200;
res.setHeader('Content-Type', 'text/html');
res.end(convert('<meta http-equiv="Content-Type" content="text/html; charset=gb2312"><div>中文</div>', 'gb2312'));
}
if (p === '/redirect/308') {
res.statusCode = 308;
res.setHeader('Location', '/inspect');
res.end();
}
if (p === '/encoding/shift-jis') {
res.statusCode = 200;
res.setHeader('Content-Type', 'text/html; charset=Shift-JIS');
res.end(convert('<div>日本語</div>', 'Shift_JIS'));
}
if (p === '/redirect/chain') {
res.statusCode = 301;
res.setHeader('Location', '/redirect/301');
res.end();
}
if (p === '/encoding/euc-jp') {
res.statusCode = 200;
res.setHeader('Content-Type', 'text/xml');
res.end(convert('<?xml version="1.0" encoding="EUC-JP"?><title>日本語</title>', 'EUC-JP'));
}
if (p === '/error/redirect') {
res.statusCode = 301;
//res.setHeader('Location', '/inspect');
res.end();
}
if (p === '/encoding/utf8') {
res.statusCode = 200;
res.end('中文');
}
if (p === '/error/400') {
res.statusCode = 400;
res.setHeader('Content-Type', 'text/plain');
res.end('client error');
}
if (p === '/encoding/order1') {
res.statusCode = 200;
res.setHeader('Content-Type', 'charset=gbk; text/plain');
res.end(convert('中文', 'gbk'));
}
if (p === '/error/404') {
res.statusCode = 404;
res.setHeader('Content-Encoding', 'gzip');
res.end();
}
if (p === '/encoding/order2') {
res.statusCode = 200;
res.setHeader('Content-Type', 'text/plain; charset=gbk; qs=1');
res.end(convert('中文', 'gbk'));
}
if (p === '/error/500') {
res.statusCode = 500;
res.setHeader('Content-Type', 'text/plain');
res.end('server error');
}
if (p === '/encoding/chunked') {
res.statusCode = 200;
res.setHeader('Content-Type', 'text/html');
res.setHeader('Transfer-Encoding', 'chunked');
res.write(repeat('a', 10));
res.end(convert('<meta http-equiv="Content-Type" content="text/html; charset=Shift_JIS" /><div>日本語</div>', 'Shift_JIS'));
}
if (p === '/error/reset') {
res.destroy();
}
if (p === '/encoding/invalid') {
res.statusCode = 200;
res.setHeader('Content-Type', 'text/html');
res.setHeader('Transfer-Encoding', 'chunked');
res.write(repeat('a', 1200));
res.end(convert('中文', 'gbk'));
}
if (p === '/error/json') {
res.statusCode = 200;
res.setHeader('Content-Type', 'application/json');
res.end('invalid json');
}
if (p === '/redirect/301') {
res.statusCode = 301;
res.setHeader('Location', '/inspect');
res.end();
}
if (p === '/no-content') {
res.statusCode = 204;
res.end();
}
if (p === '/redirect/302') {
res.statusCode = 302;
res.setHeader('Location', '/inspect');
res.end();
}
if (p === '/no-content/gzip') {
res.statusCode = 204;
res.setHeader('Content-Encoding', 'gzip');
res.end();
}
if (p === '/redirect/303') {
res.statusCode = 303;
res.setHeader('Location', '/inspect');
res.end();
}
if (p === '/not-modified') {
res.statusCode = 304;
res.end();
}
if (p === '/redirect/307') {
res.statusCode = 307;
res.setHeader('Location', '/inspect');
res.end();
}
if (p === '/not-modified/gzip') {
res.statusCode = 304;
res.setHeader('Content-Encoding', 'gzip');
res.end();
}
if (p === '/redirect/308') {
res.statusCode = 308;
res.setHeader('Location', '/inspect');
res.end();
}
if (p === '/inspect') {
res.statusCode = 200;
res.setHeader('Content-Type', 'application/json');
var body = '';
req.on('data', function(c) { body += c });
req.on('end', function() {
res.end(JSON.stringify({
method: req.method,
url: req.url,
headers: req.headers,
body: body
}));
});
}
if (p === '/redirect/chain') {
res.statusCode = 301;
res.setHeader('Location', '/redirect/301');
res.end();
}
if (p === '/multipart') {
res.statusCode = 200;
res.setHeader('Content-Type', 'application/json');
var parser = new Multipart(req.headers['content-type']);
var body = '';
parser.on('part', function(field, part) {
body += field + '=' + part;
});
parser.on('end', function() {
res.end(JSON.stringify({
method: req.method,
url: req.url,
headers: req.headers,
body: body
}));
});
req.pipe(parser);
if (p === '/error/redirect') {
res.statusCode = 301;
//res.setHeader('Location', '/inspect');
res.end();
}
if (p === '/error/400') {
res.statusCode = 400;
res.setHeader('Content-Type', 'text/plain');
res.end('client error');
}
if (p === '/error/404') {
res.statusCode = 404;
res.setHeader('Content-Encoding', 'gzip');
res.end();
}
if (p === '/error/500') {
res.statusCode = 500;
res.setHeader('Content-Type', 'text/plain');
res.end('server error');
}
if (p === '/error/reset') {
res.destroy();
}
if (p === '/error/json') {
res.statusCode = 200;
res.setHeader('Content-Type', 'application/json');
res.end('invalid json');
}
if (p === '/no-content') {
res.statusCode = 204;
res.end();
}
if (p === '/no-content/gzip') {
res.statusCode = 204;
res.setHeader('Content-Encoding', 'gzip');
res.end();
}
if (p === '/not-modified') {
res.statusCode = 304;
res.end();
}
if (p === '/not-modified/gzip') {
res.statusCode = 304;
res.setHeader('Content-Encoding', 'gzip');
res.end();
}
if (p === '/inspect') {
res.statusCode = 200;
res.setHeader('Content-Type', 'application/json');
let body = '';
req.on('data', function(c) { body += c });
req.on('end', function() {
res.end(JSON.stringify({
method: req.method,
url: req.url,
headers: req.headers,
body
}));
});
}
if (p === '/multipart') {
res.statusCode = 200;
res.setHeader('Content-Type', 'application/json');
const parser = new Multipart(req.headers['content-type']);
let body = '';
parser.on('part', function(field, part) {
body += field + '=' + part;
});
parser.on('end', function() {
res.end(JSON.stringify({
method: req.method,
url: req.url,
headers: req.headers,
body: body
}));
});
req.pipe(parser);
}
}
}
if (require.main === module) {
const server = new TestServer;
server.start(() => {
console.log(`Server started listening at port ${server.port}`);
});
}

File diff suppressed because it is too large Load Diff