writeToStream hangs if a backpressured write never emits drain or error
- Dominant language
- JavaScript
- Stars
- 28
- Forks
- 4
- PR merge metrics
- No merged PRs in 30d
Description
## What happens
`SSEContext.writeToStream` (v0.6.0, also current `main`) waits only for `drain` or `error` after `raw.write()` returns false:
```js
const canWrite = this.reply.raw.write(data)
if (canWrite) {
resolve()
} else {
const onDrain = () => { this.reply.raw.off('error', onError); resolve() }
const onError = (err) => { /* cleanup */; resolve() }
this.reply.raw.once('drain', onDrain)
this.reply.raw.once('error', onError)
}
```
A socket that is backpressured and then closes *without* emitting `error` (half-close / peer gone / some proxies) never fires either listener. `await reply.sse.send(...)` then stays pending for the life of the process.
`#isConnected` / `onClose` / `cleanup()` can all run in that situation. They do not settle the in-flight `writeToStream` promise.
There is also no `AbortSignal` (or equivalent) so the caller cannot cancel the wait.
## Why it matters
Any long-lived `keepAlive()` handler that `await`s `send()` can wedge: the HTTP handler never returns, the idle/abort path never reaches `reply.sse.close()`, and the session stays open from the server's point of view even though the client is gone.
We hit this on a production Fastify service and had to race `send()` against our own abort as a workaround. Happy to drop that once `send()` cannot hang.
## Suggested direction
When `canWrite === false`, also listen for `close` (and/or `finish`) on `reply.raw` and resolve/reject the same way `onError` does.
Nice-to-have: let `send()` take an `AbortSignal` so a server-side idle timeout can abandon a stuck write without waiting for the socket.
I can send a PR if the approach looks right.
Contributor guide
Research direction
Start at SSEContext.writeToStream and trace how reply.raw handles backpressure, close, finish, and error events. Reproduce a write that returns false and then closes without error, and verify that await reply.sse.send(...) settles instead of remaining pending; consider the separately suggested AbortSignal only if the intended scope includes cancellation.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- javascript
- Domain
- api, backend
- Issue type
- Bug
- Difficulty
- 3/5
- Estimated time
- 1-2 days
- Activity status
- Active
- Clarity
- Mostly clear
- Newbie friendliness
- 70/100