Compare commits

...

25 Commits

Author SHA1 Message Date
John Hiesey 168dbac90c 1.5.0 2015-08-20 01:21:15 -07:00
John Hiesey a91ed0db64 Modify feature testing to avoid warnings in Chrome
Previously, testing for 'ms-stream' and 'moz-chunked-arraybuffer'
support on the xhr object caused warnings to appear in Chrome.
Since fetch will always be used instead of these when available,
short-circuit and return false when fetch is available.
2015-08-20 01:06:34 -07:00
John Hiesey da4085e33d Make content-type header correct by default
The default behavior now preserves the 'content-type' header, at
the expense of not providing pseudo-streming in Safari, generic
WebKit, and older Chrome. Applications that require streaming
and already use 'prefer-stream' mode will not be affected.

The 'prefer-fast' mode is now deprecated, as that behavior now
matches the default. The 'allow-wrong-content-type' mode is new;
it gives the same behavior as the old default in case binary
streaming is essential.

Fixes #8
2015-08-20 01:01:40 -07:00
John Hiesey 6d17b406a9 1.4.1 2015-08-07 18:18:02 -07:00
John Hiesey 1a4a89a5e4 Add test for data in request.end() 2015-08-07 18:02:33 -07:00
John Hiesey 838aaffac7 Fix passing data to request.end()
Previously this would result in an exception.
Thanks @magnuslundstedt for finding the bug!
2015-08-07 17:55:43 -07:00
John Hiesey f70ac39bf4 1.4.0 2015-08-06 22:44:10 -07:00
John Hiesey ed525a4a2d Merge pull request #11 from tjmehta/add-http-methods-from-node
Add http methods from node v0.12
2015-08-06 22:33:45 -07:00
Tejesh Mehta b162e21d92 Add http methods from node v0.12 2015-08-06 17:39:17 -07:00
John Hiesey c8c0966c63 1.3.0 2015-07-20 16:10:29 -07:00
John Hiesey efaad2a3c7 Add keywords 2015-07-20 16:07:30 -07:00
John Hiesey 9059849673 Make withCredentials default to false
This may break compatibility with code that was written for
`http-browserify`. Fixes #7
2015-07-20 16:01:24 -07:00
John Hiesey aeabb2cc70 Merge pull request #6 from cesarandreu/consistent-fetch
Fix inconsistency between XMLHttpRequest and fetch
2015-07-20 00:32:46 -07:00
Cesar Andreu 588e5874f3 Add test for cookie behavior 2015-07-19 17:50:30 -07:00
Cesar Andreu 5fc4858d6d Use same-origin instead of omit with fetch credentials
In order to have parity with XMLHttpRequest fetch credentials should use same-origin instead of omit
Setting XMLHttpRequest.withCredentials to false has no effect on same-site request
2015-07-19 17:06:28 -07:00
John Hiesey 22b3cc7acb v1.2.1 2015-07-17 00:50:31 -07:00
John Hiesey 5ffc869490 Fix readme to point to new saucelabs account 2015-07-17 00:35:03 -07:00
John Hiesey cc74bc83c8 v1.2.0 2015-07-13 18:18:13 -07:00
John Hiesey 5e81240a05 add process.nextTick for 'close' 2015-07-13 18:14:30 -07:00
John Hiesey 29a2ad2032 Add abort() test 2015-07-13 16:57:31 -07:00
John Hiesey 6b544c54a3 Fix spelling of 'destroyed' 2015-07-13 16:45:46 -07:00
John Hiesey 734974d279 Improve behavior of request.prototype.abort() and add destroy synonym 2015-07-13 16:37:04 -07:00
John Hiesey 57ce59c207 Re-add close event on response 2015-07-13 16:07:46 -07:00
John Hiesey 15181f5586 v1.1.1 2015-07-12 21:26:03 -07:00
John Hiesey 916ab55402 Hide warnings for unsafe headers 2015-07-12 21:25:08 -07:00
13 changed files with 260 additions and 35 deletions
+14 -7
View File
@@ -1,6 +1,6 @@
# stream-http [![Build Status](https://travis-ci.org/jhiesey/stream-http.svg?branch=master)](https://travis-ci.org/jhiesey/stream-http)
[![Sauce Test Status](https://saucelabs.com/browser-matrix/jhiesey.svg)](https://saucelabs.com/u/jhiesey)
[![Sauce Test Status](https://saucelabs.com/browser-matrix/stream-http.svg)](https://saucelabs.com/u/stream-http)
This module is an implementation of node's native `http` module for the browser.
It tries to match node's api and behavior as closely as possible, but some features
@@ -51,14 +51,21 @@ but sometimes it helps to get a little input from the user.
* The `options.mode` field passed into `http.request` or `http.get` can take on one of the
following values:
* 'default' (or any falsy value, including undefined): Try to provide partial data before
the equest completes, but not at the cost of correctness for binary data. In some cases
the implementation may be a bit slow.
the request completes, but not at the cost of correctness for binary data or correctness of
the 'content-type' response header. This mode will also avoid slower code paths whenever
possible, which is particularly useful when making large requests in a browser like Safari
that has a weaker javascript engine.
* 'allow-wrong-content-type': Provides partial data in more cases than 'default', but
at the expense of causing the 'content-type' response header to be incorrectly reported
(as 'text/plain; charset=x-user-defined') in some browsers, notably Safari and Chrome 42
and older. Preserves binary data whenever possible. In some cases the implementation may
also be a bit slow. This was the default in versions of this module before 1.5.
* 'prefer-stream': Provide data before the request completes even if binary data (anything
that isn't a single-byte ASCII or utf8 character) will be corrupted. Of course, this option
is only safe for text data.
* 'prefer-fast': Use an implementation that does less processing even if it means that
partial data isn't available. This is particularly useful when making large requests in
a browser like Safari that has a weaker javascript engine.
is only safe for text data. May also cause the 'content-type' response header to be
incorrectly reported (as 'text/plain; charset=x-user-defined').
* 'prefer-fast': Deprecated; now a synonym for 'default', which has the same performance
characteristics as this mode did in versions before 1.5.
### Features missing compared to node
+24 -5
View File
@@ -25,9 +25,6 @@ http.request = function (opts, cb) {
opts.hostname = opts.hostname || hostHostname || window.location.hostname
opts.port = opts.port || hostPort || defaultPort
if (opts.withCredentials === undefined)
opts.withCredentials = true
// Also valid opts.auth, opts.mode
var req = new ClientRequest(opts)
@@ -48,8 +45,30 @@ http.Agent.defaultMaxSockets = 4
http.STATUS_CODES = statusCodes
http.METHODS = [
'CHECKOUT',
'CONNECT',
'COPY',
'DELETE',
'GET',
'HEAD',
'LOCK',
'M-SEARCH',
'MERGE',
'MKACTIVITY',
'MKCOL',
'MOVE',
'NOTIFY',
'OPTIONS',
'PATCH',
'POST',
'PROPFIND',
'PROPPATCH',
'PURGE',
'PUT',
'DELETE' // TODO: include the methods from RFC 2616 and 2518?
]
'REPORT',
'SEARCH',
'SUBSCRIBE',
'TRACE',
'UNLOCK',
'UNSUBSCRIBE'
]
+8 -3
View File
@@ -17,12 +17,17 @@ function checkTypeSupport (type) {
return false
}
var haveArrayBuffer = isFunction(window.ArrayBuffer)
// For some strange reason, Safari 7.0 reports typeof window.ArrayBuffer === 'object'.
// Safari 7.1 appears to have fixed this bug.
var haveArrayBuffer = typeof window.ArrayBuffer !== 'undefined'
var haveSlice = haveArrayBuffer && isFunction(window.ArrayBuffer.prototype.slice)
exports.arraybuffer = haveArrayBuffer && checkTypeSupport('arraybuffer')
exports.msstream = haveSlice && checkTypeSupport('ms-stream')
exports.mozchunkedarraybuffer = haveArrayBuffer && checkTypeSupport('moz-chunked-arraybuffer')
// These next two tests unavoidably show warnings in Chrome. Since fetch will always
// be used if it's available, just return false for these to avoid the warnings.
exports.msstream = !exports.fetch && haveSlice && checkTypeSupport('ms-stream')
exports.mozchunkedarraybuffer = !exports.fetch && haveArrayBuffer &&
checkTypeSupport('moz-chunked-arraybuffer')
exports.overrideMimeType = isFunction(xhr.overrideMimeType)
exports.vbArray = isFunction(window.VBArray)
+58 -15
View File
@@ -1,10 +1,11 @@
// var Base64 = require('Base64')
var capability = require('./capability')
var foreach = require('foreach')
var indexOf = require('indexof')
var inherits = require('inherits')
var keys = require('object-keys')
var response = require('./response')
var stream = require('stream')
var inherits = require('inherits')
var IncomingMessage = response.IncomingMessage
var rStates = response.readyStates
@@ -41,14 +42,15 @@ var ClientRequest = module.exports = function (opts) {
var preferBinary
if (opts.mode === 'prefer-streaming') {
// If streaming is a high priority but binary compatibility isn't
// If streaming is a high priority but binary compatibility and
// the accuracy of the 'content-type' header aren't
preferBinary = false
} else if (opts.mode === 'prefer-fast') {
// If binary is preferred for speed
preferBinary = true
} else if (!opts.mode || opts.mode === 'default') {
// By default, use binary if text streaming may corrupt data
} else if (opts.mode === 'allow-wrong-content-type') {
// If streaming is more important than preserving the 'content-type' header
preferBinary = !capability.overrideMimeType
} else if (!opts.mode || opts.mode === 'default' || opts.mode === 'prefer-fast') {
// Use binary if text streaming may corrupt data or the content-type header, or for speed
preferBinary = true
} else {
throw new Error('Invalid value for opts.mode')
}
@@ -63,7 +65,14 @@ inherits(ClientRequest, stream.Writable)
ClientRequest.prototype.setHeader = function (name, value) {
var self = this
self._headers[name.toLowerCase()] = {
var lowerName = name.toLowerCase()
// This check is not necessary, but it prevents warnings from browsers about setting unsafe
// headers. To be honest I'm not entirely sure hiding these warnings is a good thing, but
// http-browserify did it, so I will too.
if (indexOf(unsafeHeaders, lowerName) !== -1)
return
self._headers[lowerName] = {
name: name,
value: value
}
@@ -82,6 +91,8 @@ ClientRequest.prototype.removeHeader = function (name) {
ClientRequest.prototype._onFinish = function () {
var self = this
if (self._destroyed)
return
var opts = self._opts
var headersObj = self._headers
@@ -109,7 +120,7 @@ ClientRequest.prototype._onFinish = function () {
headers: headers,
body: body,
mode: 'cors',
credentials: opts.withCredentials ? 'include' : 'omit'
credentials: opts.withCredentials ? 'include' : 'same-origin'
}).then(function (response) {
self._fetchResponse = response
self._connect()
@@ -159,6 +170,8 @@ ClientRequest.prototype._onFinish = function () {
}
xhr.onerror = function () {
if (self._destroyed)
return
self.emit('error', new Error('XHR error'))
}
@@ -188,7 +201,7 @@ function statusValid (xhr) {
ClientRequest.prototype._onXHRProgress = function () {
var self = this
if (!statusValid(self._xhr) || self._failed)
if (!statusValid(self._xhr) || self._destroyed)
return
if (!self._response)
@@ -200,6 +213,9 @@ ClientRequest.prototype._onXHRProgress = function () {
ClientRequest.prototype._connect = function () {
var self = this
if (self._destroyed)
return
self._response = new IncomingMessage(self._xhr, self._fetchResponse, self._mode)
self.emit('response', self._response)
}
@@ -211,10 +227,15 @@ ClientRequest.prototype._write = function (chunk, encoding, cb) {
cb()
}
ClientRequest.prototype.abort = function () {
ClientRequest.prototype.abort = ClientRequest.prototype.destroy = function () {
var self = this
self._destroyed = true
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
}
ClientRequest.prototype.end = function (data, encoding, cb) {
@@ -224,13 +245,35 @@ ClientRequest.prototype.end = function (data, encoding, cb) {
data = undefined
}
if (data)
stream.Writable.push.call(self, data, encoding)
stream.Writable.prototype.end.call(self, cb)
stream.Writable.prototype.end.call(self, data, encoding, cb)
}
ClientRequest.prototype.flushHeaders = function () {}
ClientRequest.prototype.setTimeout = function () {}
ClientRequest.prototype.setNoDelay = function () {}
ClientRequest.prototype.setSocketKeepAlive = function () {}
// Taken from http://www.w3.org/TR/XMLHttpRequest/#the-setrequestheader%28%29-method
var unsafeHeaders = [
'accept-charset',
'accept-encoding',
'access-control-request-headers',
'access-control-request-method',
'connection',
'content-length',
'cookie',
'cookie2',
'date',
'dnt',
'expect',
'host',
'keep-alive',
'origin',
'referer',
'te',
'trailer',
'transfer-encoding',
'upgrade',
'user-agent',
'via'
]
+11 -1
View File
@@ -1,7 +1,7 @@
var capability = require('./capability')
var foreach = require('foreach')
var stream = require('stream')
var inherits = require('inherits')
var stream = require('stream')
var rStates = exports.readyStates = {
UNSENT: 0,
@@ -21,6 +21,14 @@ var IncomingMessage = exports.IncomingMessage = function (xhr, response, mode) {
self.trailers = {}
self.rawTrailers = []
// Fake the 'close' event, but only once 'end' fires
self.on('end', function () {
// The nextTick is necessary to prevent the 'request' module from causing an infinite loop
process.nextTick(function () {
self.emit('close')
})
})
if (mode === 'fetch') {
self._fetchResponse = response
@@ -37,6 +45,8 @@ var IncomingMessage = exports.IncomingMessage = function (xhr, response, mode) {
var reader = response.body.getReader()
function read () {
reader.read().then(function (result) {
if (self._destroyed)
return
if (result.done) {
self.push(null)
return
+10 -1
View File
@@ -1,6 +1,6 @@
{
"name": "stream-http",
"version": "1.1.0",
"version": "1.5.0",
"description": "Streaming http in the browser",
"main": "index.js",
"repository": {
@@ -15,9 +15,17 @@
},
"author": "John Hiesey",
"license": "MIT",
"keywords": [
"http",
"stream",
"streaming",
"xhr",
"http-browserify"
],
"dependencies": {
"builtin-status-codes": "~1.0.0",
"foreach": "^2.0.5",
"indexof": "0.0.1",
"inherits": "^2.0.1",
"object-keys": "1.0.4",
"xtend": "^4.0.0"
@@ -25,6 +33,7 @@
"devDependencies": {
"basic-auth": "^1.0.3",
"brfs": "^1.4.0",
"cookie-parser": "^1.3.5",
"express": "^4.13.0",
"tape": "^4.0.0",
"ua-parser-js": "^0.7.7",
+48
View File
@@ -0,0 +1,48 @@
var Buffer = require('buffer').Buffer
var fs = require('fs')
var test = require('tape')
var http = require('../..')
test('abort before response', function (t) {
var req = http.get('/basic.txt', function (res) {
t.fail('unexpected response')
})
req.abort()
t.end()
})
test('abort on response', function (t) {
var req = http.get('/basic.txt', function (res) {
req.abort()
t.end()
res.on('end', function () {
t.fail('unexpected end')
})
res.on('data', function (data) {
t.fail('unexpected data')
})
})
})
test('abort on data', function (t) {
var req = http.get('/browserify.png?copies=5', function (res) {
var firstData = true
res.on('end', function () {
t.fail('unexpected end')
})
res.on('data', function (data) {
if (firstData) {
firstData = false
req.abort()
t.end()
} else {
t.fail('unexpected data')
}
})
})
})
+5 -2
View File
@@ -26,7 +26,10 @@ for(var i = 0; i < COPIES; i++) {
}
test('binary streaming', function (t) {
http.get('/browserify.png?copies=' + COPIES, function (res) {
http.get({
path: '/browserify.png?copies=' + COPIES,
mode: 'allow-wrong-content-type'
}, function (res) {
var buffers = []
res.on('end', function () {
if (skipVerification)
@@ -50,7 +53,7 @@ test('binary streaming', function (t) {
test('large binary request without streaming', function (t) {
http.get({
path: '/browserify.png?copies=' + COPIES,
mode: 'prefer-fast',
mode: 'default',
}, function (res) {
var buffers = []
res.on('end', function () {
+25
View File
@@ -0,0 +1,25 @@
var Buffer = require('buffer').Buffer
var test = require('tape')
var http = require('../..')
test('cookie', function (t) {
var cookie = 'hello=world'
window.document.cookie = cookie
http.get({
path: '/cookie',
withCredentials: false
}, function (res) {
var buffers = []
res.on('end', function () {
t.ok(new Buffer(cookie).equals(Buffer.concat(buffers)), 'hello cookie echoed')
t.end()
})
res.on('data', function (data) {
buffers.push(data)
})
})
})
+29
View File
@@ -2,6 +2,7 @@ var Buffer = require('buffer').Buffer
var fs = require('fs')
var keys = require('object-keys')
var test = require('tape')
var UAParser = require('ua-parser-js')
var http = require('../..')
@@ -53,4 +54,32 @@ test('headers', function (t) {
buffers.push(data)
})
})
})
test('content-type response header', function (t) {
http.get('/testHeaders', function (res) {
t.equal(res.headers['content-type'], 'application/json', 'content-type preserved')
t.end()
})
})
var browser = (new UAParser()).setUA(navigator.userAgent).getBrowser()
var browserName = browser.name
var browserVersion = browser.major
// The content-type header is broken when 'prefer-streaming' or 'allow-wrong-content-type'
// is passed in browsers that rely on xhr.overrideMimeType(), namely older chrome and newer safari
var wrongMimeType = ((browserName === 'Chrome' && browserVersion <= 42) ||
((browserName === 'Safari' || browserName === 'Mobile Safari') && browserVersion >= 6))
test('content-type response header with forced streaming', function (t) {
http.get({
path: '/testHeaders',
mode: 'prefer-streaming'
}, function (res) {
if (wrongMimeType)
t.equal(res.headers['content-type'], 'text/plain; charset=x-user-defined', 'content-type overridden')
else
t.equal(res.headers['content-type'], 'application/json', 'content-type preserved')
t.end()
})
})
+20
View File
@@ -25,4 +25,24 @@ test('post text', function (t) {
req.write(reference)
req.end()
})
test('post text with data in end()', function (t) {
var req = http.request({
path: '/echo',
method: 'POST'
}, function (res) {
var buffers = []
res.on('end', function () {
t.ok(reference.equals(Buffer.concat(buffers)), 'echoed contents match')
t.end()
})
res.on('data', function (data) {
buffers.push(data)
})
})
req.end(reference)
})
+1 -1
View File
@@ -85,7 +85,7 @@ test('Test withCredentials param', function(t) {
t.equal( request._xhr.withCredentials, true, 'xhr.withCredentials should be true')
var request = http.get({ url: url }, noop)
t.equal( request._xhr.withCredentials, true, 'xhr.withCredentials should be true')
t.equal( request._xhr.withCredentials, false, 'xhr.withCredentials should be false')
t.end()
})
+7
View File
@@ -1,3 +1,4 @@
var cookieParser = require('cookie-parser')
var basicAuth = require('basic-auth')
var express = require('express')
var fs = require('fs')
@@ -46,6 +47,12 @@ app.get('/testHeaders', function (req, res) {
res.end()
})
app.get('/cookie', cookieParser(), function (req, res) {
res.setHeader('Content-Type', 'text/plain')
res.write('hello=' + req.cookies.hello)
res.end()
})
app.get('/auth', function (req, res) {
var user = basicAuth(req)