fastify / fastify/sse

writeToStream hangs if a backpressured write never emits drain or error

Open
#54 0 comments 0 reactions 0 assignees View on GitHub
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

Open the contributing 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.