commit
8cf1541fb0
|
@ -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 } ]
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"require": [
|
||||
"babel-register"
|
||||
],
|
||||
"sourceMap": false,
|
||||
"instrument": false
|
||||
}
|
|
@ -1,7 +1,8 @@
|
|||
language: node_js
|
||||
node_js:
|
||||
- "0.10"
|
||||
- "0.12"
|
||||
- "4"
|
||||
- "6"
|
||||
- "node"
|
||||
env:
|
||||
- FORMDATA_VERSION=1.0.0
|
||||
|
|
27
CHANGELOG.md
27
CHANGELOG.md
|
@ -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
|
||||
|
|
|
@ -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.
|
||||
|
|
112
README.md
112
README.md
|
@ -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) {
|
||||
.then(res => res.json())
|
||||
.then(json => console.log(json));
|
||||
|
||||
// node 7+ with async function
|
||||
|
||||
(async function () {
|
||||
const res = await fetch('https://api.github.com/users/github');
|
||||
const json = await res.json();
|
||||
console.log(json);
|
||||
});
|
||||
|
||||
// node 0.12+, yield with co
|
||||
|
||||
var co = require('co');
|
||||
co(function *() {
|
||||
var res = yield fetch('https://api.github.com/users/github');
|
||||
var json = yield res.json();
|
||||
console.log(res);
|
||||
});
|
||||
})();
|
||||
```
|
||||
|
||||
See [test cases](https://github.com/bitinn/node-fetch/blob/master/test/test.js) for more examples.
|
||||
|
|
|
@ -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
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
|
@ -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
271
index.js
|
@ -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;
|
260
lib/body.js
260
lib/body.js
|
@ -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;
|
141
lib/headers.js
141
lib/headers.js
|
@ -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;
|
||||
};
|
|
@ -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);
|
||||
};
|
|
@ -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
|
||||
});
|
||||
};
|
37
package.json
37
package.json
|
@ -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"
|
||||
}
|
||||
|
|
|
@ -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];
|
||||
}
|
||||
};
|
|
@ -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
|
||||
});
|
|
@ -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;
|
|
@ -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 };
|
|
@ -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';
|
|
@ -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;
|
|
@ -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
|
||||
};
|
|
@ -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
|
||||
});
|
||||
}
|
|
@ -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
|
||||
});
|
|
@ -1,14 +1,13 @@
|
|||
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() {
|
||||
export default class TestServer {
|
||||
constructor() {
|
||||
this.server = http.createServer(this.router);
|
||||
this.port = 30001;
|
||||
this.hostname = 'localhost';
|
||||
|
@ -18,19 +17,18 @@ function TestServer() {
|
|||
this.server.on('connection', function(socket) {
|
||||
socket.setTimeout(1500);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
TestServer.prototype.start = function(cb) {
|
||||
start(cb) {
|
||||
this.server.listen(this.port, this.hostname, cb);
|
||||
}
|
||||
}
|
||||
|
||||
TestServer.prototype.stop = function(cb) {
|
||||
stop(cb) {
|
||||
this.server.close(cb);
|
||||
}
|
||||
}
|
||||
|
||||
TestServer.prototype.router = function(req, res) {
|
||||
|
||||
var p = parse(req.url).pathname;
|
||||
router(req, res) {
|
||||
let p = parse(req.url).pathname;
|
||||
|
||||
if (p === '/hello') {
|
||||
res.statusCode = 200;
|
||||
|
@ -190,10 +188,7 @@ TestServer.prototype.router = function(req, res) {
|
|||
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);
|
||||
}
|
||||
res.write(repeat('a', 10));
|
||||
res.end(convert('<meta http-equiv="Content-Type" content="text/html; charset=Shift_JIS" /><div>日本語</div>', 'Shift_JIS'));
|
||||
}
|
||||
|
||||
|
@ -201,11 +196,7 @@ TestServer.prototype.router = function(req, res) {
|
|||
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);
|
||||
}
|
||||
res.write(repeat('a', 1200));
|
||||
res.end(convert('中文', 'gbk'));
|
||||
}
|
||||
|
||||
|
@ -304,14 +295,14 @@ TestServer.prototype.router = function(req, res) {
|
|||
if (p === '/inspect') {
|
||||
res.statusCode = 200;
|
||||
res.setHeader('Content-Type', 'application/json');
|
||||
var body = '';
|
||||
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: body
|
||||
body
|
||||
}));
|
||||
});
|
||||
}
|
||||
|
@ -319,8 +310,8 @@ TestServer.prototype.router = function(req, res) {
|
|||
if (p === '/multipart') {
|
||||
res.statusCode = 200;
|
||||
res.setHeader('Content-Type', 'application/json');
|
||||
var parser = new Multipart(req.headers['content-type']);
|
||||
var body = '';
|
||||
const parser = new Multipart(req.headers['content-type']);
|
||||
let body = '';
|
||||
parser.on('part', function(field, part) {
|
||||
body += field + '=' + part;
|
||||
});
|
||||
|
@ -334,4 +325,12 @@ TestServer.prototype.router = function(req, res) {
|
|||
});
|
||||
req.pipe(parser);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (require.main === module) {
|
||||
const server = new TestServer;
|
||||
server.start(() => {
|
||||
console.log(`Server started listening at port ${server.port}`);
|
||||
});
|
||||
}
|
||||
|
|
1231
test/test.js
1231
test/test.js
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue