Compare commits

..

6 Commits

Author SHA1 Message Date
John Hiesey 80ef3ca4ed Improve test reliability and bump deps 2021-04-13 22:29:31 -07:00
John Hiesey a2ba46d0ab Also support opts.timeout 2021-04-13 21:05:59 -07:00
John Hiesey 2c44eeb142 Update readme 2021-04-13 17:12:33 -07:00
John Hiesey 2572062eae Add timeout test 2021-04-13 16:27:08 -07:00
John Hiesey 681541d968 Emit error on destroy(err) 2021-04-13 16:23:47 -07:00
John Hiesey 54f5c2caae Add ClientRequest.setTimeout 2021-04-13 14:55:08 -07:00
7 changed files with 93 additions and 26 deletions
+7 -1
View File
@@ -1,4 +1,5 @@
sauce_connect: true
providers:
- airtap-sauce
browsers:
- name: chrome
version: -2..latest
@@ -20,3 +21,8 @@ scripts:
browserify:
- options:
dedupe: false
presets:
local:
providers: airtap-manual
browsers:
- name: manual
+1 -3
View File
@@ -84,7 +84,7 @@ option, which applies to opening the connection.
* `http.Agent` is only a stub
* The 'socket', 'connect', 'upgrade', and 'continue' events on `http.ClientRequest`.
* Any operations, including `request.setTimeout`, that operate directly on the underlying
* Any operations, other than `request.setTimeout`, that operate directly on the underlying
socket.
* Any options that are disallowed for security reasons. This includes setting or getting
certain headers.
@@ -94,8 +94,6 @@ 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` event/option and `setTimeout` functions, which operate on the underlying
socket, are not available. However, see `options.requestTimeout` above.
## Example
+44 -6
View File
@@ -54,6 +54,8 @@ var ClientRequest = module.exports = function (opts) {
}
self._mode = decideMode(preferBinary, useFetch)
self._fetchTimer = null
self._socketTimeout = null
self._socketTimer = null
self.on('finish', function () {
self._onFinish()
@@ -96,6 +98,10 @@ ClientRequest.prototype._onFinish = function () {
return
var opts = self._opts
if ('timeout' in opts && opts.timeout !== 0) {
self.setTimeout(opts.timeout)
}
var headersObj = self._headers
var body = null
if (opts.method !== 'GET' && opts.method !== 'HEAD') {
@@ -143,9 +149,10 @@ ClientRequest.prototype._onFinish = function () {
signal: signal
}).then(function (response) {
self._fetchResponse = response
self._resetTimers(false)
self._connect()
}, function (reason) {
global.clearTimeout(self._fetchTimer)
self._resetTimers(true)
if (!self._destroyed)
self.emit('error', reason)
})
@@ -201,6 +208,7 @@ ClientRequest.prototype._onFinish = function () {
xhr.onerror = function () {
if (self._destroyed)
return
self._resetTimers(true)
self.emit('error', new Error('XHR error'))
}
@@ -232,13 +240,15 @@ function statusValid (xhr) {
ClientRequest.prototype._onXHRProgress = function () {
var self = this
self._resetTimers(false)
if (!statusValid(self._xhr) || self._destroyed)
return
if (!self._response)
self._connect()
self._response._onXHRProgress()
self._response._onXHRProgress(self._resetTimers.bind(self))
}
ClientRequest.prototype._connect = function () {
@@ -247,7 +257,7 @@ ClientRequest.prototype._connect = function () {
if (self._destroyed)
return
self._response = new IncomingMessage(self._xhr, self._fetchResponse, self._mode, self._fetchTimer)
self._response = new IncomingMessage(self._xhr, self._fetchResponse, self._mode, self._resetTimers.bind(self))
self._response.on('error', function(err) {
self.emit('error', err)
})
@@ -262,16 +272,35 @@ ClientRequest.prototype._write = function (chunk, encoding, cb) {
cb()
}
ClientRequest.prototype.abort = ClientRequest.prototype.destroy = function () {
ClientRequest.prototype._resetTimers = function (done) {
var self = this
global.clearTimeout(self._socketTimer)
self._socketTimer = null
if (done) {
global.clearTimeout(self._fetchTimer)
self._fetchTimer = null
} else if (self._socketTimeout) {
self._socketTimer = global.setTimeout(function () {
self.emit('timeout')
}, self._socketTimeout)
}
}
ClientRequest.prototype.abort = ClientRequest.prototype.destroy = function (err) {
var self = this
self._destroyed = true
global.clearTimeout(self._fetchTimer)
self._resetTimers(true)
if (self._response)
self._response._destroyed = true
if (self._xhr)
self._xhr.abort()
else if (self._fetchAbortController)
self._fetchAbortController.abort()
if (err)
self.emit('error', err)
}
ClientRequest.prototype.end = function (data, encoding, cb) {
@@ -284,8 +313,17 @@ ClientRequest.prototype.end = function (data, encoding, cb) {
stream.Writable.prototype.end.call(self, data, encoding, cb)
}
ClientRequest.prototype.setTimeout = function (timeout, cb) {
var self = this
if (cb)
self.once('timeout', cb)
self._socketTimeout = timeout
self._resetTimers(false)
}
ClientRequest.prototype.flushHeaders = function () {}
ClientRequest.prototype.setTimeout = function () {}
ClientRequest.prototype.setNoDelay = function () {}
ClientRequest.prototype.setSocketKeepAlive = function () {}
+9 -6
View File
@@ -10,7 +10,7 @@ var rStates = exports.readyStates = {
DONE: 4
}
var IncomingMessage = exports.IncomingMessage = function (xhr, response, mode, fetchTimer) {
var IncomingMessage = exports.IncomingMessage = function (xhr, response, mode, resetTimers) {
var self = this
stream.Readable.call(self)
@@ -43,6 +43,7 @@ var IncomingMessage = exports.IncomingMessage = function (xhr, response, mode, f
if (capability.writableStream) {
var writable = new WritableStream({
write: function (chunk) {
resetTimers(false)
return new Promise(function (resolve, reject) {
if (self._destroyed) {
reject()
@@ -54,7 +55,7 @@ var IncomingMessage = exports.IncomingMessage = function (xhr, response, mode, f
})
},
close: function () {
global.clearTimeout(fetchTimer)
resetTimers(true)
if (!self._destroyed)
self.push(null)
},
@@ -66,7 +67,7 @@ var IncomingMessage = exports.IncomingMessage = function (xhr, response, mode, f
try {
response.body.pipeTo(writable).catch(function (err) {
global.clearTimeout(fetchTimer)
resetTimers(true)
if (!self._destroyed)
self.emit('error', err)
})
@@ -79,15 +80,15 @@ var IncomingMessage = exports.IncomingMessage = function (xhr, response, mode, f
reader.read().then(function (result) {
if (self._destroyed)
return
resetTimers(result.done)
if (result.done) {
global.clearTimeout(fetchTimer)
self.push(null)
return
}
self.push(Buffer.from(result.value))
read()
}).catch(function (err) {
global.clearTimeout(fetchTimer)
resetTimers(true)
if (!self._destroyed)
self.emit('error', err)
})
@@ -146,7 +147,7 @@ IncomingMessage.prototype._read = function () {
}
}
IncomingMessage.prototype._onXHRProgress = function () {
IncomingMessage.prototype._onXHRProgress = function (resetTimers) {
var self = this
var xhr = self._xhr
@@ -193,6 +194,7 @@ IncomingMessage.prototype._onXHRProgress = function () {
}
}
reader.onload = function () {
resetTimers(true)
self.push(null)
}
// reader.onerror = ??? // TODO: this
@@ -202,6 +204,7 @@ IncomingMessage.prototype._onXHRProgress = function () {
// The ms-stream case handles end separately in reader.onload()
if (self._xhr.readyState === rStates.DONE && self._mode !== 'ms-stream') {
resetTimers(true)
self.push(null)
}
}
+7 -5
View File
@@ -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": "airtap --loopback airtap.local -- test/browser/*.js",
"test-browser-local": "airtap --no-instrument --local 8080 -- test/browser/*.js"
"test-browser": "airtap --concurrency 1 -- test/browser/*.js",
"test-browser-local": "airtap --preset local -- test/browser/*.js"
},
"author": "John Hiesey",
"license": "MIT",
@@ -33,13 +33,15 @@
"xtend": "^4.0.2"
},
"devDependencies": {
"airtap": "^3.0.0",
"airtap": "^4.0.3",
"airtap-manual": "^1.0.0",
"airtap-sauce": "^1.1.0",
"basic-auth": "^2.0.1",
"brfs": "^2.0.2",
"cookie-parser": "^1.4.5",
"express": "^4.17.1",
"tape": "^5.0.0",
"ua-parser-js": "^0.7.21",
"tape": "^5.2.2",
"ua-parser-js": "^0.7.28",
"webworkify": "^1.5.0"
}
}
+24 -4
View File
@@ -4,7 +4,7 @@ var test = require('tape')
var http = require('../..')
test('timeout', function (t) {
test('requestTimeout', function (t) {
var req = http.get({
path: '/browserify.png?copies=5',
requestTimeout: 10 // ms
@@ -16,14 +16,14 @@ test('timeout', function (t) {
})
})
req.on('requestTimeout', function () {
t.pass('got timeout')
t.pass('got requestTimeout')
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) {
test.skip('no requestTimeout after success', function (t) {
var req = http.get({
path: '/basic.txt',
requestTimeout: 50000 // ms
@@ -38,6 +38,26 @@ test.skip('no timeout after success', function (t) {
})
})
req.on('requestTimeout', function () {
t.fail('unexpected timeout')
t.fail('unexpected requestTimeout')
})
})
test('setTimeout', function (t) {
t.plan(2)
var req = http.get({
path: '/browserify.png?copies=5'
}, function (res) {
res.on('data', function (data) {
})
res.on('end', function () {
t.fail('request completed (should have timed out)')
})
})
req.setTimeout(10, function () {
t.pass('got setTimeout callback')
})
req.on('timeout', function () {
t.pass('got timeout')
})
})
+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.AIRTAP_PORT) || 8199
var port = parseInt(process.env.AIRTAP_SUPPORT_PORT) || 8199
console.log('Test server listening on port', port)
server.listen(port)