Skip to content

Commit 38e8b3b

Browse files
authored
implement read timeout (#10)
1 parent f1963a7 commit 38e8b3b

File tree

3 files changed

+102
-0
lines changed

3 files changed

+102
-0
lines changed

README.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,14 @@ By default, EventSource makes a `GET` request. You can specify a different HTTP
128128
var eventSourceInitDict = { method: 'POST', body: 'n=100' };
129129
```
130130

131+
### Read timeout
132+
133+
TCP connections can sometimes fail without the client detecting an I/O error, in which case EventSource could hang forever waiting for events. Setting a `readTimeoutMillis` will cause EventSource to drop and retry the connection if that number of milliseconds ever elapses without receiving any new data from the server. If the server is known to send any "heartbeat" data at regular intervals (such as a `:` comment line, which is ignored in SSE) to indicate that the connection is still alive, set the read timeout to some number longer than that interval.
134+
135+
```javascript
136+
var eventSourceInitDict = { readTimeoutMillis: 30000 };
137+
````
138+
131139
### Special HTTPS configuration
132140

133141
In Node.js, you can customize the behavior of HTTPS requests by specifying, for instance, additional trusted CA certificates. You may use any of the special TLS options supported by Node's [`tls.connect()`](https://nodejs.org/api/tls.html#tls_tls_connect_options_callback) and [`tls.createSecureContext()`](https://nodejs.org/api/tls.html#tls_tls_createsecurecontext_options) (depending on what version of Node you are using) by putting them in an object in the `https` property of your configuration:

lib/eventsource.js

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -222,6 +222,7 @@ function EventSource (url, eventSourceInitDict) {
222222
var buf
223223
var startingPos = 0
224224
var startingFieldLength = -1
225+
225226
res.on('data', function (chunk) {
226227
buf = buf ? Buffer.concat([buf, chunk]) : chunk
227228
if (isFirst && hasBom(buf)) {
@@ -280,6 +281,10 @@ function EventSource (url, eventSourceInitDict) {
280281
})
281282
})
282283

284+
if (config.readTimeoutMillis) {
285+
req.setTimeout(config.readTimeoutMillis)
286+
}
287+
283288
if (config.body) {
284289
req.write(config.body)
285290
}
@@ -288,6 +293,11 @@ function EventSource (url, eventSourceInitDict) {
288293
failed({ message: err.message })
289294
})
290295

296+
req.on('timeout', function () {
297+
failed({ message: 'Read timeout, received no data in ' + config.readTimeoutMillis +
298+
'ms, assuming connection is dead' })
299+
})
300+
291301
if (req.setNoDelay) req.setNoDelay(true)
292302
req.end()
293303
}

test/eventsource_test.js

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1585,6 +1585,90 @@ describe('Proxying', function () {
15851585
})
15861586
})
15871587

1588+
describe('read timeout', function () {
1589+
var briefDelay = 1
1590+
1591+
function makeStreamHandler (timeBetweenEvents) {
1592+
var requestCount = 0
1593+
return function (req, res) {
1594+
requestCount++
1595+
res.writeHead(200, {'Content-Type': 'text/event-stream'})
1596+
var eventPrefix = 'request-' + requestCount
1597+
res.write('') // turns on chunking
1598+
res.write('data: ' + eventPrefix + '-event-1\n\n')
1599+
setTimeout(() => {
1600+
if (res.writableEnded || res.finished) {
1601+
// don't try to write any more if the connection's already been closed
1602+
return
1603+
}
1604+
res.write('data: ' + eventPrefix + '-event-2\n\n')
1605+
}, timeBetweenEvents)
1606+
}
1607+
}
1608+
1609+
it('drops connection if read timeout elapses', function (done) {
1610+
var readTimeout = 50
1611+
var timeBetweenEvents = 100
1612+
createServer(function (err, server) {
1613+
if (err) return done(err)
1614+
1615+
server.on('request', makeStreamHandler(timeBetweenEvents))
1616+
1617+
var es = new EventSource(server.url, {
1618+
initialRetryDelayMillis: briefDelay,
1619+
readTimeoutMillis: readTimeout
1620+
})
1621+
var events = []
1622+
var errors = []
1623+
es.onmessage = function (event) {
1624+
events.push(event)
1625+
if (events.length === 2) {
1626+
es.close()
1627+
assert.equal('request-1-event-1', events[0].data)
1628+
assert.equal('request-2-event-1', events[1].data)
1629+
assert.equal(1, errors.length)
1630+
assert.ok(/^Read timeout/.test(errors[0].message),
1631+
'Unexpected error message: ' + errors[0].message)
1632+
server.close(done)
1633+
}
1634+
}
1635+
es.onerror = function (err) {
1636+
errors.push(err)
1637+
}
1638+
})
1639+
})
1640+
1641+
it('does not drop connection if read timeout does not elapse', function (done) {
1642+
var readTimeout = 100
1643+
var timeBetweenEvents = 50
1644+
createServer(function (err, server) {
1645+
if (err) return done(err)
1646+
1647+
server.on('request', makeStreamHandler(timeBetweenEvents))
1648+
1649+
var es = new EventSource(server.url, {
1650+
initialRetryDelayMillis: briefDelay,
1651+
readTimeoutMillis: readTimeout
1652+
})
1653+
var events = []
1654+
var errors = []
1655+
es.onmessage = function (event) {
1656+
events.push(event)
1657+
if (events.length === 2) {
1658+
es.close()
1659+
assert.equal('request-1-event-1', events[0].data)
1660+
assert.equal('request-1-event-2', events[1].data)
1661+
assert.equal(0, errors.length)
1662+
server.close(done)
1663+
}
1664+
}
1665+
es.onerror = function (err) {
1666+
errors.push(err)
1667+
}
1668+
})
1669+
})
1670+
})
1671+
15881672
describe('EventSource object', function () {
15891673
it('declares support for custom properties', function () {
15901674
assert.equal(true, EventSource.supportedOptions.headers)

0 commit comments

Comments
 (0)