Compare commits

...

10 Commits

Author SHA1 Message Date
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
16 changed files with 6559 additions and 71 deletions
+1 -3
View File
@@ -5,13 +5,11 @@ browsers:
- name: firefox
version: 34..latest
- name: safari
version: 5..latest
version: 7..latest
- name: microsoftedge
version: 13..latest
- name: ie
version: 9..latest
- name: opera
version: 11..latest
- name: iphone
version: '8.1..latest'
- name: android
+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
+4
View File
@@ -1,4 +1,5 @@
var ClientRequest = require('./lib/request')
var IncomingMessage = require('./lib/response')
var extend = require('xtend')
var statusCodes = require('builtin-status-codes')
var url = require('url')
@@ -44,6 +45,9 @@ http.get = function get (opts, cb) {
return req
}
http.ClientRequest = ClientRequest
http.IncomingMessage = IncomingMessage
http.Agent = function () {}
http.Agent.defaultMaxSockets = 4
+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)])
+27 -10
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') {
@@ -102,7 +101,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,12 +130,28 @@ ClientRequest.prototype._onFinish = function () {
})
if (self._mode === 'fetch') {
var signal = null
if (capability.abortController) {
var controller = new AbortController()
signal = controller.signal
self._fetchAbortController = controller
if ('requestTimeout' in opts && opts.requestTimeout !== 0) {
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()
@@ -162,10 +179,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')
}
}
@@ -261,8 +278,8 @@ ClientRequest.prototype.abort = ClientRequest.prototype.destroy = function () {
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) {
+40 -5
View File
@@ -35,13 +35,40 @@ 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) {
return
} else if(self.push(new Buffer(chunk))) {
resolve()
} else {
self._resumeFetch = resolve
}
})
},
close: function () {
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)
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) {
@@ -54,11 +81,11 @@ var IncomingMessage = exports.IncomingMessage = function (xhr, response, mode) {
self.push(new Buffer(result.value))
read()
}).catch(function(err) {
self.emit('error', err)
if (!self._destroyed)
self.emit('error', err)
})
}
read()
} else {
self._xhr = xhr
self._pos = 0
@@ -102,7 +129,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
+6434
View File
File diff suppressed because it is too large Load Diff
+7 -7
View File
@@ -1,6 +1,6 @@
{
"name": "stream-http",
"version": "2.7.2",
"version": "2.8.0",
"description": "Streaming http in the browser",
"main": "index.js",
"repository": {
@@ -29,18 +29,18 @@
"dependencies": {
"builtin-status-codes": "^3.0.0",
"inherits": "^2.0.1",
"readable-stream": "^2.2.6",
"readable-stream": "^2.3.3",
"to-arraybuffer": "^1.0.0",
"xtend": "^4.0.0"
},
"devDependencies": {
"basic-auth": "^1.0.3",
"basic-auth": "^2.0.0",
"brfs": "^1.4.0",
"cookie-parser": "^1.4.3",
"express": "^4.15.2",
"tape": "^4.0.0",
"ua-parser-js": "^0.7.7",
"webworkify": "^1.0.2",
"express": "^4.16.2",
"tape": "^4.8.0",
"ua-parser-js": "^0.7.17",
"webworkify": "^1.5.0",
"zuul": "^3.10.3"
}
}
+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) ||
+22
View File
@@ -0,0 +1,22 @@
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()
})
})
-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))
+1 -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')