Compare commits

...

35 Commits

Author SHA1 Message Date
John Hiesey 65aed45b2d 2.8.3 2018-06-01 17:52:26 -07:00
John Hiesey 05dd964945 Merge pull request #89 from wmhilton/patch-1
Allow setting User-Agent Header
2018-06-01 17:33:29 -07:00
William Hilton 4f73bb1a3c Allow setting User-Agent Header
The User-Agent header has not been forbidden since Firefox 43 (see https://developer.mozilla.org/en-US/docs/Glossary/Forbidden_header_name).

> Note: The User-Agent header is no longer forbidden, as per spec — see forbidden header name list (this was implemented in Firefox 43) — it can now be set in a Fetch Headers object, or via XHR setRequestHeader().

I've had success setting it in Firefox and Edge for `fetch` and it should work for XMLHttpRequest as well but I'm too lazy to look up how to use XMLHttpRequest. I haven't had any luck setting User-Agent it in Chrome yet - may be a Chrome bug - but I'm not seeing any warnings in the terminal when I use it either, so I don't think allowing it will cause any spurious warnings.
2018-06-01 16:47:30 -04:00
John Hiesey 663f88323c 2.8.2 2018-05-09 19:26:31 -07:00
John Hiesey 12a0b3fd2a npm update 2018-05-09 16:37:22 -07:00
John Hiesey 520cc451e8 Clear fetch timeout after request finishes
Instead of right when response is created, which misses timeouts
that occur later
2018-05-08 21:52:39 -07:00
John Hiesey 5c3b919dab Fix edge tests 2018-05-08 21:52:05 -07:00
John Hiesey 5bc3a7205e Add (disabled) test for timeout after success
Unfortunately, reasonable timeout lengths tend to cause
spurious failures, so this test shouldn't be enabled
normally.
2018-05-08 20:37:36 -07:00
John Hiesey 80cbe3c5fe Improve fetch error handling 2018-05-08 17:36:01 -07:00
John Hiesey c3ab1f7bf0 Update package-lock.json 2018-05-08 16:18:22 -07:00
John Hiesey 98095e85eb Bump airtap version 2018-05-08 16:18:11 -07:00
John Hiesey 6547015c3d Also clear fetch timeout on error 2018-05-08 16:13:41 -07:00
John Hiesey 4b8abf52f3 Fetch clearTimeout fixup 2018-05-08 16:07:46 -07:00
John Hiesey 472ed03ea7 Merge pull request #87 from luozhang002/fetchmode
fix:add clearTimeout issue#85
2018-05-08 16:04:48 -07:00
罗章 16ec2ee996 fix:add clearTimeout 2018-05-03 13:24:32 +08:00
John Hiesey 8cf284cd4b 2.8.1 2018-03-14 13:27:52 -07:00
John Hiesey 709d1acc3d Stop trying to run tests on iOS 8
It seems that this is no longer supported by Sauce Labs
2018-03-14 13:14:14 -07:00
John Hiesey f800fad249 Fix incorrect definition of http.IncomingMessage
Also add test for top-level http properties, plus a stub http.globalAgent
Fixes #81
2018-03-14 13:08:47 -07:00
John Hiesey 920dd3cd48 Bump airtap version 2018-02-26 22:50:33 -08:00
John Hiesey 433d1d5e46 Stop testing safari 7 since sauce labs gives errors 2018-02-26 22:50:01 -08:00
John Hiesey 86bfe01159 Re-enable webworker test with workaround 2018-02-26 17:43:26 -08:00
John Hiesey 8188419852 Merge pull request #84 from jhiesey/airtap
Switch from zuul to airtap for running browser tests
2018-02-22 00:56:32 -08:00
John Hiesey aa4882c7c6 Fix running browser tests locally with airtap 2018-02-22 00:40:33 -08:00
John Hiesey d62864934a Disable android 7 tests due to airtap issue
Upstream issue: https://github.com/airtap/browsers/issues/3
2018-02-22 00:29:58 -08:00
John Hiesey e875fc72b0 Switch from zuul to airtap for tests 2018-02-21 22:22:04 -08:00
John Hiesey 38ffb38660 2.8.0 2018-01-15 02:48:35 -08:00
John Hiesey 16c1baf178 Add Uint8Array support for body
This will be used if available. Fixes #57 and #80
2018-01-15 01:16:31 -08:00
John Hiesey 0ecf194fa4 Remove test support for older browsers
Opera (all versions) and Safari < 7 aren't supported by sauce labs
anymore, so don't test them.
2018-01-15 01:16:31 -08:00
John Hiesey d4631b211c Change timeout behavior
Add opts.requestTimeout. Fixes #66
2018-01-15 01:16:31 -08:00
John Hiesey bf580d32dd Expose ClientRequest and IncomingMessage constructors
For compatibility with node. Fixes #76 and #81.
2018-01-15 01:16:31 -08:00
John Hiesey 80f74c2107 Update dependencies and add npm-shrinkwrap.json 2018-01-15 01:16:31 -08:00
John Hiesey c2e1444670 Update documentation 2018-01-15 01:03:26 -08:00
John Hiesey 4fefc64d39 Don't emit error after destroyed 2018-01-15 01:03:26 -08:00
John Hiesey f94670b6b4 Add proper abort/timeout support for fetch
Fixes #26, finally!
2018-01-15 01:03:26 -08:00
John Hiesey 46c35150fc Implement backpressure
Uses WritableStream, which is still quite new.
2018-01-15 01:03:07 -08:00
19 changed files with 7128 additions and 87 deletions
+8 -7
View File
@@ -1,22 +1,23 @@
ui: tape
sauce_connect: true
browsers:
- name: chrome
version: 39..latest
- name: firefox
version: 34..latest
- name: safari
version: 5..latest
- name: microsoftedge
version: 8..latest
- name: MicrosoftEdge
version: 13..latest
- name: ie
version: 9..latest
- name: opera
version: 11..latest
- name: iphone
version: '8.1..latest'
version: '9.3..latest'
- name: android
version: '4.4..latest'
version: '4.4..6.0' # TODO: change this back to latest once https://github.com/airtap/browsers/issues/3 is fixed
server: ./test/server/index.js
scripts:
- "/ie8-polyfill.js"
- "/test-polyfill.js"
browserify:
- options:
dedupe: false
+1 -1
View File
@@ -2,4 +2,4 @@
bundle.js
node_modules
npm-debug.log
.zuulrc
.airtaprc
+5 -1
View File
@@ -1,3 +1,7 @@
language: node_js
node_js:
- "6.4"
- "node"
addons:
sauce_connect: true
hosts:
- airtap.local
+12 -2
View File
@@ -13,6 +13,10 @@ This is heavily inspired by, and intended to replace, [http-browserify](https://
In accordance with its name, `stream-http` tries to provide data to its caller before
the request has completed whenever possible.
Backpressure, allowing the browser to only pull data from the server as fast as it is
consumed, is supported in:
* Chrome >= 58 (using `fetch` and `WritableStream`)
The following browsers support true streaming, where only a small amount of the request
has to be held in memory at once:
* Chrome >= 43 (using the `fetch` API)
@@ -80,6 +84,12 @@ capability. Preserves the correctness of binary data and the 'content-type' resp
* 'prefer-fast': Deprecated; now a synonym for 'default', which has the same performance
characteristics as this mode did in versions before 1.5.
* `options.requestTimeout` allows setting a timeout in millisecionds for XHR and fetch (if
supported by the browser). This is a limit on how long the entire process takes from
beginning to end. Note that this is not the same as the node `setTimeout` functions,
which apply to pauses in data transfer over the underlying socket, or the node `timeout`
option, which applies to opening the connection.
### Features missing compared to Node
* `http.Agent` is only a stub
@@ -94,8 +104,8 @@ the server.
* `message.trailers` and `message.rawTrailers` will remain empty.
* Redirects are followed silently by the browser, so it isn't possible to access the 301/302
redirect pages.
* The `timeout` options in the `request` method is non-operational in Safari <= 5 and
Opera <= 12.
* The `timeout` event/option and `setTimeout` functions, which operate on the underlying
socket, are not available. However, see `options.requestTimeout` above.
## Example
+6
View File
@@ -1,4 +1,5 @@
var ClientRequest = require('./lib/request')
var response = require('./lib/response')
var extend = require('xtend')
var statusCodes = require('builtin-status-codes')
var url = require('url')
@@ -44,9 +45,14 @@ http.get = function get (opts, cb) {
return req
}
http.ClientRequest = ClientRequest
http.IncomingMessage = response.IncomingMessage
http.Agent = function () {}
http.Agent.defaultMaxSockets = 4
http.globalAgent = new http.Agent()
http.STATUS_CODES = statusCodes
http.METHODS = [
+4
View File
@@ -1,5 +1,9 @@
exports.fetch = isFunction(global.fetch) && isFunction(global.ReadableStream)
exports.writableStream = isFunction(global.WritableStream)
exports.abortController = isFunction(global.AbortController)
exports.blobConstructor = false
try {
new Blob([new ArrayBuffer(1)])
+34 -13
View File
@@ -38,9 +38,8 @@ var ClientRequest = module.exports = function (opts) {
var preferBinary
var useFetch = true
if (opts.mode === 'disable-fetch' || 'timeout' in opts) {
// If the use of XHR should be preferred and includes preserving the 'content-type' header.
// Force XHR to be used since the Fetch API does not yet support timeouts.
if (opts.mode === 'disable-fetch' || ('requestTimeout' in opts && !capability.abortController)) {
// If the use of XHR should be preferred. Not typically needed.
useFetch = false
preferBinary = true
} else if (opts.mode === 'prefer-streaming') {
@@ -57,6 +56,7 @@ var ClientRequest = module.exports = function (opts) {
throw new Error('Invalid value for opts.mode')
}
self._mode = decideMode(preferBinary, useFetch)
self._fetchTimer = null
self.on('finish', function () {
self._onFinish()
@@ -102,7 +102,9 @@ ClientRequest.prototype._onFinish = function () {
var headersObj = self._headers
var body = null
if (opts.method !== 'GET' && opts.method !== 'HEAD') {
if (capability.blobConstructor) {
if (capability.arraybuffer) {
body = toArrayBuffer(Buffer.concat(self._body))
} else if (capability.blobConstructor) {
body = new global.Blob(self._body.map(function (buffer) {
return toArrayBuffer(buffer)
}), {
@@ -129,17 +131,36 @@ ClientRequest.prototype._onFinish = function () {
})
if (self._mode === 'fetch') {
var signal = null
var fetchTimer = null
if (capability.abortController) {
var controller = new AbortController()
signal = controller.signal
self._fetchAbortController = controller
if ('requestTimeout' in opts && opts.requestTimeout !== 0) {
self._fetchTimer = global.setTimeout(function () {
self.emit('requestTimeout')
if (self._fetchAbortController)
self._fetchAbortController.abort()
}, opts.requestTimeout)
}
}
global.fetch(self._opts.url, {
method: self._opts.method,
headers: headersList,
body: body || undefined,
mode: 'cors',
credentials: opts.withCredentials ? 'include' : 'same-origin'
credentials: opts.withCredentials ? 'include' : 'same-origin',
signal: signal
}).then(function (response) {
self._fetchResponse = response
self._connect()
}, function (reason) {
self.emit('error', reason)
global.clearTimeout(self._fetchTimer)
if (!self._destroyed)
self.emit('error', reason)
})
} else {
var xhr = self._xhr = new global.XMLHttpRequest()
@@ -162,10 +183,10 @@ ClientRequest.prototype._onFinish = function () {
if (self._mode === 'text' && 'overrideMimeType' in xhr)
xhr.overrideMimeType('text/plain; charset=x-user-defined')
if ('timeout' in opts) {
xhr.timeout = opts.timeout
if ('requestTimeout' in opts) {
xhr.timeout = opts.requestTimeout
xhr.ontimeout = function () {
self.emit('timeout')
self.emit('requestTimeout')
}
}
@@ -239,7 +260,7 @@ ClientRequest.prototype._connect = function () {
if (self._destroyed)
return
self._response = new IncomingMessage(self._xhr, self._fetchResponse, self._mode)
self._response = new IncomingMessage(self._xhr, self._fetchResponse, self._mode, self._fetchTimer)
self._response.on('error', function(err) {
self.emit('error', err)
})
@@ -257,12 +278,13 @@ ClientRequest.prototype._write = function (chunk, encoding, cb) {
ClientRequest.prototype.abort = ClientRequest.prototype.destroy = function () {
var self = this
self._destroyed = true
global.clearTimeout(self._fetchTimer)
if (self._response)
self._response._destroyed = true
if (self._xhr)
self._xhr.abort()
// Currently, there isn't a way to truly abort a fetch.
// If you like bikeshedding, see https://github.com/whatwg/fetch/issues/27
else if (self._fetchAbortController)
self._fetchAbortController.abort()
}
ClientRequest.prototype.end = function (data, encoding, cb) {
@@ -301,6 +323,5 @@ var unsafeHeaders = [
'trailer',
'transfer-encoding',
'upgrade',
'user-agent',
'via'
]
+49 -7
View File
@@ -10,7 +10,7 @@ var rStates = exports.readyStates = {
DONE: 4
}
var IncomingMessage = exports.IncomingMessage = function (xhr, response, mode) {
var IncomingMessage = exports.IncomingMessage = function (xhr, response, mode, fetchTimer) {
var self = this
stream.Readable.call(self)
@@ -35,30 +35,64 @@ var IncomingMessage = exports.IncomingMessage = function (xhr, response, mode) {
self.statusCode = response.status
self.statusMessage = response.statusText
response.headers.forEach(function(header, key){
response.headers.forEach(function (header, key){
self.headers[key.toLowerCase()] = header
self.rawHeaders.push(key, header)
})
if (capability.writableStream) {
var writable = new WritableStream({
write: function (chunk) {
return new Promise(function (resolve, reject) {
if (self._destroyed) {
reject()
} else if(self.push(new Buffer(chunk))) {
resolve()
} else {
self._resumeFetch = resolve
}
})
},
close: function () {
global.clearTimeout(fetchTimer)
if (!self._destroyed)
self.push(null)
},
abort: function (err) {
if (!self._destroyed)
self.emit('error', err)
}
})
// TODO: this doesn't respect backpressure. Once WritableStream is available, this can be fixed
try {
response.body.pipeTo(writable).catch(function (err) {
global.clearTimeout(fetchTimer)
if (!self._destroyed)
self.emit('error', err)
})
return
} catch (e) {} // pipeTo method isn't defined. Can't find a better way to feature test this
}
// fallback for when writableStream or pipeTo aren't available
var reader = response.body.getReader()
function read () {
reader.read().then(function (result) {
if (self._destroyed)
return
if (result.done) {
global.clearTimeout(fetchTimer)
self.push(null)
return
}
self.push(new Buffer(result.value))
read()
}).catch(function(err) {
self.emit('error', err)
}).catch(function (err) {
global.clearTimeout(fetchTimer)
if (!self._destroyed)
self.emit('error', err)
})
}
read()
} else {
self._xhr = xhr
self._pos = 0
@@ -102,7 +136,15 @@ var IncomingMessage = exports.IncomingMessage = function (xhr, response, mode) {
inherits(IncomingMessage, stream.Readable)
IncomingMessage.prototype._read = function () {}
IncomingMessage.prototype._read = function () {
var self = this
var resolve = self._resumeFetch
if (resolve) {
self._resumeFetch = null
resolve()
}
}
IncomingMessage.prototype._onXHRProgress = function () {
var self = this
+6932
View File
File diff suppressed because it is too large Load Diff
+11 -11
View File
@@ -1,6 +1,6 @@
{
"name": "stream-http",
"version": "2.7.2",
"version": "2.8.3",
"description": "Streaming http in the browser",
"main": "index.js",
"repository": {
@@ -10,8 +10,8 @@
"scripts": {
"test": "npm run test-node && ([ -n \"${TRAVIS_PULL_REQUEST}\" -a \"${TRAVIS_PULL_REQUEST}\" != 'false' ] || npm run test-browser)",
"test-node": "tape test/node/*.js",
"test-browser": "zuul --no-coverage -- test/browser/*.js",
"test-browser-local": "zuul --local 8080 --no-coverage -- test/browser/*.js"
"test-browser": "airtap --loopback airtap.local -- test/browser/*.js",
"test-browser-local": "airtap --no-instrument --local 8080 -- test/browser/*.js"
},
"author": "John Hiesey",
"license": "MIT",
@@ -29,18 +29,18 @@
"dependencies": {
"builtin-status-codes": "^3.0.0",
"inherits": "^2.0.1",
"readable-stream": "^2.2.6",
"readable-stream": "^2.3.6",
"to-arraybuffer": "^1.0.0",
"xtend": "^4.0.0"
},
"devDependencies": {
"basic-auth": "^1.0.3",
"brfs": "^1.4.0",
"airtap": "^0.0.5",
"basic-auth": "^2.0.0",
"brfs": "^1.6.1",
"cookie-parser": "^1.4.3",
"express": "^4.15.2",
"tape": "^4.0.0",
"ua-parser-js": "^0.7.7",
"webworkify": "^1.0.2",
"zuul": "^3.10.3"
"express": "^4.16.3",
"tape": "^4.9.0",
"ua-parser-js": "^0.7.18",
"webworkify": "^1.5.0"
}
}
+2 -2
View File
@@ -8,8 +8,8 @@ var http = require('../..')
var browser = (new UAParser()).setUA(navigator.userAgent).getBrowser()
var browserName = browser.name
var browserVersion = browser.major
// Binary streaming doesn't work in IE10 or below or in Opera
var skipStreamingCheck = (browserName === 'Opera' || (browserName === 'IE' && browserVersion <= 10))
// Binary streaming doesn't work in IE10 or below
var skipStreamingCheck = (browserName === 'IE' && browserVersion <= 10)
// Binary data gets corrupted in IE8 or below
var skipVerification = (browserName === 'IE' && browserVersion <= 8)
+1 -2
View File
@@ -9,8 +9,7 @@ var browser = (new UAParser()).setUA(navigator.userAgent).getBrowser()
var browserName = browser.name
var browserVersion = browser.major
// Binary request bodies don't work in a bunch of browsers
var skipVerification = ((browserName === 'Opera' && browserVersion <= 11) ||
(browserName === 'IE' && browserVersion <= 10) ||
var skipVerification = ((browserName === 'IE' && browserVersion <= 10) ||
(browserName === 'Safari' && browserVersion <= 5) ||
(browserName === 'WebKit' && browserVersion <= 534) || // Old mobile safari
(browserName === 'Android Browser' && browserVersion <= 4))
+2 -2
View File
@@ -8,8 +8,8 @@ var http = require('../..')
var browser = (new UAParser()).setUA(navigator.userAgent).getBrowser()
var browserName = browser.name
var browserVersion = browser.major
// Streaming doesn't work in IE9 or below or in Opera
var skipStreamingCheck = (browserName === 'Opera' || (browserName === 'IE' && browserVersion <= 9))
// Streaming doesn't work in IE9 or below
var skipStreamingCheck = (browserName === 'IE' && browserVersion <= 9)
var COPIES = 1000
var MIN_PIECES = 5
+1 -2
View File
@@ -10,8 +10,7 @@ var browser = (new UAParser()).setUA(navigator.userAgent).getBrowser()
var browserName = browser.name
var browserVersion = browser.major
// Response urls don't work on many browsers
var skipResponseUrl = ((browserName === 'Opera') ||
(browserName === 'IE') ||
var skipResponseUrl = ((browserName === 'IE') ||
(browserName === 'Edge') ||
(browserName === 'Chrome' && browserVersion <= 36) ||
(browserName === 'Firefox' && browserVersion <= 31) ||
+43
View File
@@ -0,0 +1,43 @@
var Buffer = require('buffer').Buffer
var fs = require('fs')
var test = require('tape')
var http = require('../..')
test('timeout', function (t) {
var req = http.get({
path: '/browserify.png?copies=5',
requestTimeout: 10 // ms
}, function (res) {
res.on('data', function (data) {
})
res.on('end', function () {
t.fail('request completed (should have timed out)')
})
})
req.on('requestTimeout', function () {
t.pass('got timeout')
t.end()
})
})
// TODO: reenable this if there's a way to make it simultaneously
// fast and reliable
test.skip('no timeout after success', function (t) {
var req = http.get({
path: '/basic.txt',
requestTimeout: 50000 // ms
}, function (res) {
res.on('data', function (data) {
})
res.on('end', function () {
t.pass('success')
global.setTimeout(function () {
t.end()
}, 50000)
})
})
req.on('requestTimeout', function () {
t.fail('unexpected timeout')
})
})
-33
View File
@@ -1,33 +0,0 @@
var Buffer = require('buffer').Buffer
var fs = require('fs')
var test = require('tape')
var UAParser = require('ua-parser-js')
var url = require('url')
var http = require('../..')
var browser = (new UAParser()).setUA(navigator.userAgent).getBrowser()
var browserName = browser.name
var browserVersion = browser.major
var skipTimeout = ((browserName === 'Opera' && browserVersion <= 12) ||
(browserName === 'Safari' && browserVersion <= 5))
test('emits timeout events', function (t) {
if (skipTimeout) {
return t.skip('Browser does not support setting timeouts')
}
var req = http.request({
path: '/basic.txt',
timeout: 1
})
req.on('timeout', function () {
t.pass('timeout caught')
t.end() // the test will timeout if this does not happen
})
req.end()
})
+1 -2
View File
@@ -8,8 +8,7 @@ var browser = (new UAParser()).setUA(navigator.userAgent).getBrowser()
var browserName = browser.name
var browserVersion = browser.major
// Skip browsers with poor or nonexistant WebWorker support
var skip = ((browserName === 'Opera' && browserVersion <= 12) ||
(browserName === 'IE' && browserVersion <= 10) ||
var skip = ((browserName === 'IE' && browserVersion <= 10) ||
(browserName === 'Safari' && browserVersion <= 5) ||
(browserName === 'WebKit' && browserVersion <= 534) || // Old mobile safari
(browserName === 'Android Browser' && browserVersion <= 4))
+15 -1
View File
@@ -1,4 +1,4 @@
// These tests are teken from http-browserify to ensure compatibility with
// These tests are taken from http-browserify to ensure compatibility with
// that module
var test = require('tape')
var url = require('url')
@@ -17,6 +17,20 @@ var moduleName = require.resolve('../../')
delete require.cache[moduleName]
var http = require('../../')
test('Make sure http object has correct properties', function (t) {
t.ok(http.Agent, 'Agent defined')
t.ok(http.ClientRequest, 'ClientRequest defined')
t.ok(http.ClientRequest.prototype, 'ClientRequest.prototype defined')
t.ok(http.IncomingMessage, 'IncomingMessage defined')
t.ok(http.IncomingMessage.prototype, 'IncomingMessage.prototype defined')
t.ok(http.METHODS, 'METHODS defined')
t.ok(http.STATUS_CODES, 'STATUS_CODES defined')
t.ok(http.get, 'get defined')
t.ok(http.globalAgent, 'globalAgent defined')
t.ok(http.request, 'request defined')
t.end()
})
test('Test simple url string', function(t) {
var testUrl = { path: '/api/foo' }
var request = http.get(testUrl, noop)
+1 -1
View File
@@ -132,6 +132,6 @@ app.use(function (req, res, next) {
app.use(express.static(path.join(__dirname, 'static')))
var port = parseInt(process.env.ZUUL_PORT) || 8199
var port = parseInt(process.env.AIRTAP_PORT) || 8199
console.log('Test server listening on port', port)
server.listen(port)