Skip to content

Commit

Permalink
implement read timeout (#10)
Browse files Browse the repository at this point in the history
  • Loading branch information
eli-darkly authored Jan 26, 2021
1 parent f1963a7 commit 38e8b3b
Show file tree
Hide file tree
Showing 3 changed files with 102 additions and 0 deletions.
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,14 @@ By default, EventSource makes a `GET` request. You can specify a different HTTP
var eventSourceInitDict = { method: 'POST', body: 'n=100' };
```

### Read timeout

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.

```javascript
var eventSourceInitDict = { readTimeoutMillis: 30000 };
````

### Special HTTPS configuration

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:
Expand Down
10 changes: 10 additions & 0 deletions lib/eventsource.js
Original file line number Diff line number Diff line change
Expand Up @@ -222,6 +222,7 @@ function EventSource (url, eventSourceInitDict) {
var buf
var startingPos = 0
var startingFieldLength = -1

res.on('data', function (chunk) {
buf = buf ? Buffer.concat([buf, chunk]) : chunk
if (isFirst && hasBom(buf)) {
Expand Down Expand Up @@ -280,6 +281,10 @@ function EventSource (url, eventSourceInitDict) {
})
})

if (config.readTimeoutMillis) {
req.setTimeout(config.readTimeoutMillis)
}

if (config.body) {
req.write(config.body)
}
Expand All @@ -288,6 +293,11 @@ function EventSource (url, eventSourceInitDict) {
failed({ message: err.message })
})

req.on('timeout', function () {
failed({ message: 'Read timeout, received no data in ' + config.readTimeoutMillis +
'ms, assuming connection is dead' })
})

if (req.setNoDelay) req.setNoDelay(true)
req.end()
}
Expand Down
84 changes: 84 additions & 0 deletions test/eventsource_test.js
Original file line number Diff line number Diff line change
Expand Up @@ -1585,6 +1585,90 @@ describe('Proxying', function () {
})
})

describe('read timeout', function () {
var briefDelay = 1

function makeStreamHandler (timeBetweenEvents) {
var requestCount = 0
return function (req, res) {
requestCount++
res.writeHead(200, {'Content-Type': 'text/event-stream'})
var eventPrefix = 'request-' + requestCount
res.write('') // turns on chunking
res.write('data: ' + eventPrefix + '-event-1\n\n')
setTimeout(() => {
if (res.writableEnded || res.finished) {
// don't try to write any more if the connection's already been closed
return
}
res.write('data: ' + eventPrefix + '-event-2\n\n')
}, timeBetweenEvents)
}
}

it('drops connection if read timeout elapses', function (done) {
var readTimeout = 50
var timeBetweenEvents = 100
createServer(function (err, server) {
if (err) return done(err)

server.on('request', makeStreamHandler(timeBetweenEvents))

var es = new EventSource(server.url, {
initialRetryDelayMillis: briefDelay,
readTimeoutMillis: readTimeout
})
var events = []
var errors = []
es.onmessage = function (event) {
events.push(event)
if (events.length === 2) {
es.close()
assert.equal('request-1-event-1', events[0].data)
assert.equal('request-2-event-1', events[1].data)
assert.equal(1, errors.length)
assert.ok(/^Read timeout/.test(errors[0].message),
'Unexpected error message: ' + errors[0].message)
server.close(done)
}
}
es.onerror = function (err) {
errors.push(err)
}
})
})

it('does not drop connection if read timeout does not elapse', function (done) {
var readTimeout = 100
var timeBetweenEvents = 50
createServer(function (err, server) {
if (err) return done(err)

server.on('request', makeStreamHandler(timeBetweenEvents))

var es = new EventSource(server.url, {
initialRetryDelayMillis: briefDelay,
readTimeoutMillis: readTimeout
})
var events = []
var errors = []
es.onmessage = function (event) {
events.push(event)
if (events.length === 2) {
es.close()
assert.equal('request-1-event-1', events[0].data)
assert.equal('request-1-event-2', events[1].data)
assert.equal(0, errors.length)
server.close(done)
}
}
es.onerror = function (err) {
errors.push(err)
}
})
})
})

describe('EventSource object', function () {
it('declares support for custom properties', function () {
assert.equal(true, EventSource.supportedOptions.headers)
Expand Down

0 comments on commit 38e8b3b

Please sign in to comment.