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
|
# Directory for instrumented libs generated by jscoverage/JSCover
|
||||||
lib-cov
|
lib-cov
|
||||||
|
|
||||||
# Coverage directory used by tools like istanbul
|
# Coverage directory used by tools like nyc and istanbul
|
||||||
|
.nyc_output
|
||||||
coverage
|
coverage
|
||||||
|
|
||||||
# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
|
# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
|
||||||
|
@ -29,3 +30,6 @@ node_modules
|
||||||
|
|
||||||
# OS files
|
# OS files
|
||||||
.DS_Store
|
.DS_Store
|
||||||
|
|
||||||
|
# Babel-compiled files
|
||||||
|
lib
|
||||||
|
|
|
@ -0,0 +1,7 @@
|
||||||
|
{
|
||||||
|
"require": [
|
||||||
|
"babel-register"
|
||||||
|
],
|
||||||
|
"sourceMap": false,
|
||||||
|
"instrument": false
|
||||||
|
}
|
|
@ -1,7 +1,8 @@
|
||||||
language: node_js
|
language: node_js
|
||||||
node_js:
|
node_js:
|
||||||
- "0.10"
|
|
||||||
- "0.12"
|
- "0.12"
|
||||||
|
- "4"
|
||||||
|
- "6"
|
||||||
- "node"
|
- "node"
|
||||||
env:
|
env:
|
||||||
- FORMDATA_VERSION=1.0.0
|
- 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
|
# 1.x release
|
||||||
|
|
||||||
## v1.6.3
|
## v1.6.3
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
Known differences
|
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.
|
- 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`.
|
- 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.
|
- 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.
|
- 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).
|
- 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
110
README.md
|
@ -41,44 +41,40 @@ See Matt Andrews' [isomorphic-fetch](https://github.com/matthew-andrews/isomorph
|
||||||
# Usage
|
# Usage
|
||||||
|
|
||||||
```javascript
|
```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.
|
// if you are using your own Promise library, set it through fetch.Promise. Eg.
|
||||||
// fetch.Promise = require('bluebird');
|
|
||||||
|
// import Bluebird from 'bluebird';
|
||||||
|
// fetch.Promise = Bluebird;
|
||||||
|
|
||||||
// plain text or html
|
// plain text or html
|
||||||
|
|
||||||
fetch('https://github.com/')
|
fetch('https://github.com/')
|
||||||
.then(function(res) {
|
.then(res => res.text())
|
||||||
return res.text();
|
.then(body => console.log(body));
|
||||||
}).then(function(body) {
|
|
||||||
console.log(body);
|
|
||||||
});
|
|
||||||
|
|
||||||
// json
|
// json
|
||||||
|
|
||||||
fetch('https://api.github.com/users/github')
|
fetch('https://api.github.com/users/github')
|
||||||
.then(function(res) {
|
.then(res => res.json())
|
||||||
return res.json();
|
.then(json => console.log(json));
|
||||||
}).then(function(json) {
|
|
||||||
console.log(json);
|
|
||||||
});
|
|
||||||
|
|
||||||
// catching network error
|
// catching network error
|
||||||
// 3xx-5xx responses are NOT network errors, and should be handled in then()
|
// 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
|
// you only need one catch() at the end of your promise chain
|
||||||
|
|
||||||
fetch('http://domain.invalid/')
|
fetch('http://domain.invalid/')
|
||||||
.catch(function(err) {
|
.catch(err => console.error(err));
|
||||||
console.log(err);
|
|
||||||
});
|
|
||||||
|
|
||||||
// stream
|
// stream
|
||||||
// the node.js way is to use stream when possible
|
// the node.js way is to use stream when possible
|
||||||
|
|
||||||
fetch('https://assets-cdn.github.com/images/modules/logos_page/Octocat.png')
|
fetch('https://assets-cdn.github.com/images/modules/logos_page/Octocat.png')
|
||||||
.then(function(res) {
|
.then(res => {
|
||||||
var dest = fs.createWriteStream('./octocat.png');
|
const dest = fs.createWriteStream('./octocat.png');
|
||||||
res.body.pipe(dest);
|
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()
|
// if you prefer to cache binary data in full, use buffer()
|
||||||
// note that buffer() is a node-fetch only API
|
// 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')
|
fetch('https://assets-cdn.github.com/images/modules/logos_page/Octocat.png')
|
||||||
.then(function(res) {
|
.then(res => res.buffer())
|
||||||
return res.buffer();
|
.then(buffer => fileType(buffer))
|
||||||
}).then(function(buffer) {
|
.then(type => { /* ... */ });
|
||||||
fileType(buffer);
|
|
||||||
});
|
|
||||||
|
|
||||||
// meta
|
// meta
|
||||||
|
|
||||||
fetch('https://github.com/')
|
fetch('https://github.com/')
|
||||||
.then(function(res) {
|
.then(res => {
|
||||||
console.log(res.ok);
|
console.log(res.ok);
|
||||||
console.log(res.status);
|
console.log(res.status);
|
||||||
console.log(res.statusText);
|
console.log(res.statusText);
|
||||||
|
@ -108,22 +103,17 @@ fetch('https://github.com/')
|
||||||
// post
|
// post
|
||||||
|
|
||||||
fetch('http://httpbin.org/post', { method: 'POST', body: 'a=1' })
|
fetch('http://httpbin.org/post', { method: 'POST', body: 'a=1' })
|
||||||
.then(function(res) {
|
.then(res => res.json())
|
||||||
return res.json();
|
.then(json => console.log(json));
|
||||||
}).then(function(json) {
|
|
||||||
console.log(json);
|
|
||||||
});
|
|
||||||
|
|
||||||
// post with stream from resumer
|
// post with stream from file
|
||||||
|
|
||||||
var resumer = require('resumer');
|
import { createReadStream } from 'fs';
|
||||||
var stream = resumer().queue('a=1').end();
|
|
||||||
|
const stream = createReadStream('input.txt');
|
||||||
fetch('http://httpbin.org/post', { method: 'POST', body: stream })
|
fetch('http://httpbin.org/post', { method: 'POST', body: stream })
|
||||||
.then(function(res) {
|
.then(res => res.json())
|
||||||
return res.json();
|
.then(json => console.log(json));
|
||||||
}).then(function(json) {
|
|
||||||
console.log(json);
|
|
||||||
});
|
|
||||||
|
|
||||||
// post with JSON
|
// post with JSON
|
||||||
|
|
||||||
|
@ -133,45 +123,37 @@ fetch('http://httpbin.org/post', {
|
||||||
body: JSON.stringify(body),
|
body: JSON.stringify(body),
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
})
|
})
|
||||||
.then(function(res) {
|
.then(res => res.json())
|
||||||
return res.json();
|
.then(json => console.log(json));
|
||||||
}).then(function(json) {
|
|
||||||
console.log(json);
|
|
||||||
});
|
|
||||||
|
|
||||||
// post with form-data (detect multipart)
|
// post with form-data (detect multipart)
|
||||||
|
|
||||||
var FormData = require('form-data');
|
import FormData from 'form-data';
|
||||||
var form = new FormData();
|
|
||||||
|
const form = new FormData();
|
||||||
form.append('a', 1);
|
form.append('a', 1);
|
||||||
fetch('http://httpbin.org/post', { method: 'POST', body: form })
|
fetch('http://httpbin.org/post', { method: 'POST', body: form })
|
||||||
.then(function(res) {
|
.then(res => res.json())
|
||||||
return res.json();
|
.then(json => console.log(json));
|
||||||
}).then(function(json) {
|
|
||||||
console.log(json);
|
|
||||||
});
|
|
||||||
|
|
||||||
// post with form-data (custom headers)
|
// post with form-data (custom headers)
|
||||||
// note that getHeaders() is non-standard API
|
// note that getHeaders() is non-standard API
|
||||||
|
|
||||||
var FormData = require('form-data');
|
import FormData from 'form-data';
|
||||||
var form = new FormData();
|
|
||||||
|
const form = new FormData();
|
||||||
form.append('a', 1);
|
form.append('a', 1);
|
||||||
fetch('http://httpbin.org/post', { method: 'POST', body: form, headers: form.getHeaders() })
|
fetch('http://httpbin.org/post', { method: 'POST', body: form, headers: form.getHeaders() })
|
||||||
.then(function(res) {
|
.then(res => res.json())
|
||||||
return res.json();
|
.then(json => console.log(json));
|
||||||
}).then(function(json) {
|
|
||||||
console.log(json);
|
|
||||||
});
|
|
||||||
|
|
||||||
// node 0.12+, yield with co
|
// node 7+ with async function
|
||||||
|
|
||||||
var co = require('co');
|
(async function () {
|
||||||
co(function *() {
|
const res = await fetch('https://api.github.com/users/github');
|
||||||
var res = yield fetch('https://api.github.com/users/github');
|
const json = await res.json();
|
||||||
var json = yield res.json();
|
console.log(json);
|
||||||
console.log(res);
|
})();
|
||||||
});
|
|
||||||
```
|
```
|
||||||
|
|
||||||
See [test cases](https://github.com/bitinn/node-fetch/blob/master/test/test.js) for more examples.
|
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",
|
"name": "node-fetch",
|
||||||
"version": "1.6.3",
|
"version": "1.6.3",
|
||||||
"description": "A light-weight module that brings window.fetch to node.js and io.js",
|
"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": {
|
"scripts": {
|
||||||
"test": "mocha test/test.js",
|
"build": "cross-env BABEL_ENV=rollup rollup -c",
|
||||||
"report": "istanbul cover _mocha -- -R spec test/test.js",
|
"prepublish": "npm run build",
|
||||||
"coverage": "istanbul cover _mocha --report lcovonly -- -R spec test/test.js && codecov -f coverage/coverage.json"
|
"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": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
|
@ -24,18 +31,32 @@
|
||||||
},
|
},
|
||||||
"homepage": "https://github.com/bitinn/node-fetch",
|
"homepage": "https://github.com/bitinn/node-fetch",
|
||||||
"devDependencies": {
|
"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",
|
"bluebird": "^3.3.4",
|
||||||
"chai": "^3.5.0",
|
"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",
|
"codecov": "^1.0.1",
|
||||||
|
"cross-env": "2.0.1",
|
||||||
"form-data": ">=1.0.0",
|
"form-data": ">=1.0.0",
|
||||||
"istanbul": "^0.4.2",
|
"is-builtin-module": "^1.0.0",
|
||||||
"mocha": "^2.1.0",
|
"mocha": "^3.1.2",
|
||||||
|
"nyc": "^10.0.0",
|
||||||
"parted": "^0.1.1",
|
"parted": "^0.1.1",
|
||||||
"promise": "^7.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": {
|
"dependencies": {
|
||||||
|
"babel-runtime": "^6.11.6",
|
||||||
|
"buffer-to-arraybuffer": "0.0.4",
|
||||||
"encoding": "^0.1.11",
|
"encoding": "^0.1.11",
|
||||||
"is-stream": "^1.0.1"
|
"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
|
* FetchError interface for operational errors
|
||||||
*/
|
*/
|
||||||
|
|
||||||
module.exports = FetchError;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create FetchError instance
|
* Create FetchError instance
|
||||||
*
|
*
|
||||||
|
@ -15,12 +13,9 @@ module.exports = FetchError;
|
||||||
* @param String systemError For Node.js system error
|
* @param String systemError For Node.js system error
|
||||||
* @return FetchError
|
* @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.message = message;
|
||||||
this.type = type;
|
this.type = type;
|
||||||
|
|
||||||
|
@ -29,6 +24,10 @@ function FetchError(message, type, systemError) {
|
||||||
this.code = this.errno = systemError.code;
|
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
|
||||||
|
});
|
593
test/server.js
593
test/server.js
|
@ -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');
|
export default class TestServer {
|
||||||
var parse = require('url').parse;
|
constructor() {
|
||||||
var zlib = require('zlib');
|
this.server = http.createServer(this.router);
|
||||||
var stream = require('stream');
|
this.port = 30001;
|
||||||
var convert = require('encoding').convert;
|
this.hostname = 'localhost';
|
||||||
var Multipart = require('parted').multipart;
|
this.server.on('error', function(err) {
|
||||||
|
console.log(err.stack);
|
||||||
module.exports = TestServer;
|
});
|
||||||
|
this.server.on('connection', function(socket) {
|
||||||
function TestServer() {
|
socket.setTimeout(1500);
|
||||||
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);
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (p === '/deflate') {
|
start(cb) {
|
||||||
res.statusCode = 200;
|
this.server.listen(this.port, this.hostname, cb);
|
||||||
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') {
|
stop(cb) {
|
||||||
res.statusCode = 200;
|
this.server.close(cb);
|
||||||
res.setHeader('Content-Type', 'text/plain');
|
|
||||||
res.setHeader('Content-Encoding', 'deflate');
|
|
||||||
zlib.deflateRaw('hello world', function(err, buffer) {
|
|
||||||
res.end(buffer);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (p === '/sdch') {
|
router(req, res) {
|
||||||
res.statusCode = 200;
|
let p = parse(req.url).pathname;
|
||||||
res.setHeader('Content-Type', 'text/plain');
|
|
||||||
res.setHeader('Content-Encoding', 'sdch');
|
|
||||||
res.end('fake sdch string');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (p === '/invalid-content-encoding') {
|
if (p === '/hello') {
|
||||||
res.statusCode = 200;
|
res.statusCode = 200;
|
||||||
res.setHeader('Content-Type', 'text/plain');
|
res.setHeader('Content-Type', 'text/plain');
|
||||||
res.setHeader('Content-Encoding', 'gzip');
|
res.end('world');
|
||||||
res.end('fake gzip string');
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if (p === '/timeout') {
|
if (p === '/plain') {
|
||||||
setTimeout(function() {
|
|
||||||
res.statusCode = 200;
|
res.statusCode = 200;
|
||||||
res.setHeader('Content-Type', 'text/plain');
|
res.setHeader('Content-Type', 'text/plain');
|
||||||
res.end('text');
|
res.end('text');
|
||||||
}, 1000);
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if (p === '/slow') {
|
if (p === '/options') {
|
||||||
res.statusCode = 200;
|
res.statusCode = 200;
|
||||||
res.setHeader('Content-Type', 'text/plain');
|
res.setHeader('Allow', 'GET, HEAD, OPTIONS');
|
||||||
res.write('test');
|
res.end('hello world');
|
||||||
setTimeout(function() {
|
}
|
||||||
res.end('test');
|
|
||||||
}, 1000);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (p === '/cookie') {
|
if (p === '/html') {
|
||||||
res.statusCode = 200;
|
res.statusCode = 200;
|
||||||
res.setHeader('Set-Cookie', ['a=1', 'b=1']);
|
res.setHeader('Content-Type', 'text/html');
|
||||||
res.end('cookie');
|
res.end('<html></html>');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (p === '/size/chunk') {
|
if (p === '/json') {
|
||||||
res.statusCode = 200;
|
res.statusCode = 200;
|
||||||
res.setHeader('Content-Type', 'text/plain');
|
res.setHeader('Content-Type', 'application/json');
|
||||||
setTimeout(function() {
|
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');
|
res.write('test');
|
||||||
}, 50);
|
setTimeout(function() {
|
||||||
setTimeout(function() {
|
res.end('test');
|
||||||
res.end('test');
|
}, 1000);
|
||||||
}, 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);
|
|
||||||
}
|
}
|
||||||
res.end(convert('<meta http-equiv="Content-Type" content="text/html; charset=Shift_JIS" /><div>日本語</div>', 'Shift_JIS'));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (p === '/encoding/invalid') {
|
if (p === '/cookie') {
|
||||||
res.statusCode = 200;
|
res.statusCode = 200;
|
||||||
res.setHeader('Content-Type', 'text/html');
|
res.setHeader('Set-Cookie', ['a=1', 'b=1']);
|
||||||
res.setHeader('Transfer-Encoding', 'chunked');
|
res.end('cookie');
|
||||||
// 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.end(convert('中文', 'gbk'));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (p === '/redirect/301') {
|
if (p === '/size/chunk') {
|
||||||
res.statusCode = 301;
|
res.statusCode = 200;
|
||||||
res.setHeader('Location', '/inspect');
|
res.setHeader('Content-Type', 'text/plain');
|
||||||
res.end();
|
setTimeout(function() {
|
||||||
}
|
res.write('test');
|
||||||
|
}, 50);
|
||||||
|
setTimeout(function() {
|
||||||
|
res.end('test');
|
||||||
|
}, 100);
|
||||||
|
}
|
||||||
|
|
||||||
if (p === '/redirect/302') {
|
if (p === '/size/long') {
|
||||||
res.statusCode = 302;
|
res.statusCode = 200;
|
||||||
res.setHeader('Location', '/inspect');
|
res.setHeader('Content-Type', 'text/plain');
|
||||||
res.end();
|
res.end('testtest');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (p === '/redirect/303') {
|
if (p === '/encoding/gbk') {
|
||||||
res.statusCode = 303;
|
res.statusCode = 200;
|
||||||
res.setHeader('Location', '/inspect');
|
res.setHeader('Content-Type', 'text/html');
|
||||||
res.end();
|
res.end(convert('<meta charset="gbk"><div>中文</div>', 'gbk'));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (p === '/redirect/307') {
|
if (p === '/encoding/gb2312') {
|
||||||
res.statusCode = 307;
|
res.statusCode = 200;
|
||||||
res.setHeader('Location', '/inspect');
|
res.setHeader('Content-Type', 'text/html');
|
||||||
res.end();
|
res.end(convert('<meta http-equiv="Content-Type" content="text/html; charset=gb2312"><div>中文</div>', 'gb2312'));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (p === '/redirect/308') {
|
if (p === '/encoding/shift-jis') {
|
||||||
res.statusCode = 308;
|
res.statusCode = 200;
|
||||||
res.setHeader('Location', '/inspect');
|
res.setHeader('Content-Type', 'text/html; charset=Shift-JIS');
|
||||||
res.end();
|
res.end(convert('<div>日本語</div>', 'Shift_JIS'));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (p === '/redirect/chain') {
|
if (p === '/encoding/euc-jp') {
|
||||||
res.statusCode = 301;
|
res.statusCode = 200;
|
||||||
res.setHeader('Location', '/redirect/301');
|
res.setHeader('Content-Type', 'text/xml');
|
||||||
res.end();
|
res.end(convert('<?xml version="1.0" encoding="EUC-JP"?><title>日本語</title>', 'EUC-JP'));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (p === '/error/redirect') {
|
if (p === '/encoding/utf8') {
|
||||||
res.statusCode = 301;
|
res.statusCode = 200;
|
||||||
//res.setHeader('Location', '/inspect');
|
res.end('中文');
|
||||||
res.end();
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if (p === '/error/400') {
|
if (p === '/encoding/order1') {
|
||||||
res.statusCode = 400;
|
res.statusCode = 200;
|
||||||
res.setHeader('Content-Type', 'text/plain');
|
res.setHeader('Content-Type', 'charset=gbk; text/plain');
|
||||||
res.end('client error');
|
res.end(convert('中文', 'gbk'));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (p === '/error/404') {
|
if (p === '/encoding/order2') {
|
||||||
res.statusCode = 404;
|
res.statusCode = 200;
|
||||||
res.setHeader('Content-Encoding', 'gzip');
|
res.setHeader('Content-Type', 'text/plain; charset=gbk; qs=1');
|
||||||
res.end();
|
res.end(convert('中文', 'gbk'));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (p === '/error/500') {
|
if (p === '/encoding/chunked') {
|
||||||
res.statusCode = 500;
|
res.statusCode = 200;
|
||||||
res.setHeader('Content-Type', 'text/plain');
|
res.setHeader('Content-Type', 'text/html');
|
||||||
res.end('server error');
|
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') {
|
if (p === '/encoding/invalid') {
|
||||||
res.destroy();
|
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') {
|
if (p === '/redirect/301') {
|
||||||
res.statusCode = 200;
|
res.statusCode = 301;
|
||||||
res.setHeader('Content-Type', 'application/json');
|
res.setHeader('Location', '/inspect');
|
||||||
res.end('invalid json');
|
res.end();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (p === '/no-content') {
|
if (p === '/redirect/302') {
|
||||||
res.statusCode = 204;
|
res.statusCode = 302;
|
||||||
res.end();
|
res.setHeader('Location', '/inspect');
|
||||||
}
|
res.end();
|
||||||
|
}
|
||||||
|
|
||||||
if (p === '/no-content/gzip') {
|
if (p === '/redirect/303') {
|
||||||
res.statusCode = 204;
|
res.statusCode = 303;
|
||||||
res.setHeader('Content-Encoding', 'gzip');
|
res.setHeader('Location', '/inspect');
|
||||||
res.end();
|
res.end();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (p === '/not-modified') {
|
if (p === '/redirect/307') {
|
||||||
res.statusCode = 304;
|
res.statusCode = 307;
|
||||||
res.end();
|
res.setHeader('Location', '/inspect');
|
||||||
}
|
res.end();
|
||||||
|
}
|
||||||
|
|
||||||
if (p === '/not-modified/gzip') {
|
if (p === '/redirect/308') {
|
||||||
res.statusCode = 304;
|
res.statusCode = 308;
|
||||||
res.setHeader('Content-Encoding', 'gzip');
|
res.setHeader('Location', '/inspect');
|
||||||
res.end();
|
res.end();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (p === '/inspect') {
|
if (p === '/redirect/chain') {
|
||||||
res.statusCode = 200;
|
res.statusCode = 301;
|
||||||
res.setHeader('Content-Type', 'application/json');
|
res.setHeader('Location', '/redirect/301');
|
||||||
var body = '';
|
res.end();
|
||||||
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 === '/multipart') {
|
if (p === '/error/redirect') {
|
||||||
res.statusCode = 200;
|
res.statusCode = 301;
|
||||||
res.setHeader('Content-Type', 'application/json');
|
//res.setHeader('Location', '/inspect');
|
||||||
var parser = new Multipart(req.headers['content-type']);
|
res.end();
|
||||||
var body = '';
|
}
|
||||||
parser.on('part', function(field, part) {
|
|
||||||
body += field + '=' + part;
|
if (p === '/error/400') {
|
||||||
});
|
res.statusCode = 400;
|
||||||
parser.on('end', function() {
|
res.setHeader('Content-Type', 'text/plain');
|
||||||
res.end(JSON.stringify({
|
res.end('client error');
|
||||||
method: req.method,
|
}
|
||||||
url: req.url,
|
|
||||||
headers: req.headers,
|
if (p === '/error/404') {
|
||||||
body: body
|
res.statusCode = 404;
|
||||||
}));
|
res.setHeader('Content-Encoding', 'gzip');
|
||||||
});
|
res.end();
|
||||||
req.pipe(parser);
|
}
|
||||||
|
|
||||||
|
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}`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
1237
test/test.js
1237
test/test.js
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue