Skip to content

fix(events): ownership-safe SSE client handles (fixes #483) - #512

Open
mathieucarbou wants to merge 1 commit into
mainfrom
fix/483
Open

mathieucarbou wants to merge 1 commit into
mainfrom
fix/483

Conversation

@mathieucarbou

@mathieucarbou mathieucarbou commented Nov 13, 2026 •

Copy link
Copy Markdown
Member

AsyncEventSource clients were stored as unique_ptr and exposed to applications only as raw pointers inside callbacks, making it impossible to safely target one connection later (close one session, send to a subset) without use-after-free risk.

Rework client lifetime after the design suggested in review of #483:

  • AsyncEventSourceClient now derives from enable_shared_from_this and the server stores shared_ptr handles; all AsyncTCP callbacks pin the client for the duration of the call so no callback can free the object while its frame is still live.
  • Client construction/registration moves from the constructor (where shared_from_this() is illegal) to AsyncEventSourceResponse::_respond() via _addClient(); the request is pinned for the connect callbacks.
  • New app API: onConnectSafe()/onDisconnectSafe() deliver a stable AsyncEventSourceClientPtr handle that may be stored past the callback and used as a connection identifier; onConnectWithRequest() adds the originating request for session association; getClients() returns a safe snapshot. The classic onConnect()/onDisconnect() signatures are unchanged (wrapped into the shared slot, last registered wins).
  • close() iterates a snapshot instead of the live list (fixes iterator invalidation on synchronous disconnect), ~AsyncEventSource detaches surviving clients, and an app-dropped last handle now closes the underlying connection safely.

Changed tested on a hosted build.

@mathieucarbou

Copy link
Copy Markdown
Member Author

@willmmiles I saw we still had a user report in SSE code in this PR: #483
This PR was closed because the solution wrong but I saw you left a comment on how we could fix the use reported bug.
I created this PR: it is backward compatible but provides 3 new callbacks allowing the reception of a shared client.

AsyncEventSource clients were stored as unique_ptr and exposed to
applications only as raw pointers inside callbacks, making it impossible
to safely target one connection later (close one session, send to a
subset) without use-after-free risk.

Rework client lifetime after the design suggested in review of #483:
- AsyncEventSourceClient now derives from enable_shared_from_this and
  the server stores shared_ptr handles; all AsyncTCP callbacks pin the
  client for the duration of the call so no callback can free the
  object while its frame is still live.
- Client construction/registration moves from the constructor (where
  shared_from_this() is illegal) to AsyncEventSourceResponse::_respond()
  via _addClient(); the request is pinned for the connect callbacks.
- New app API: onConnectSafe()/onDisconnectSafe() deliver a stable
  AsyncEventSourceClientPtr handle that may be stored past the callback
  and used as a connection identifier; onConnectWithRequest() adds the
  originating request for session association; getClients() returns a
  safe snapshot. The classic onConnect()/onDisconnect() signatures are
  unchanged (wrapped into the shared slot, last registered wins).
- close() iterates a snapshot instead of the live list (fixes iterator
  invalidation on synchronous disconnect), ~AsyncEventSource detaches
  surviving clients, and an app-dropped last handle now closes the
  underlying connection safely.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Type: Bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant