Skip to content

Add stable IDs for targeted SSE client closure - #483

Closed
horendus wants to merge 1 commit into
ESP32Async:mainfrom
horendus:feature/sse-client-id
Closed

horendus wants to merge 1 commit into
ESP32Async:mainfrom
horendus:feature/sse-client-id

Conversation

@horendus

@horendus horendus commented Sep 19, 2026 •

Copy link
Copy Markdown

Summary

Adds a stable, source-scoped ID for each Server-Sent Events connection and an ownership-safe way to close one connection by ID.

The change also adds onConnectWithRequest(...), so an application can associate the connection ID with request-scoped state such as an authenticated session without retaining an AsyncEventSourceClient* after the callback.

Why

Long-lived SSE is now commonly used by authenticated dashboards and device management interfaces. When one session expires or logs out, applications need to close that session's stream while leaving unrelated users connected.

Today the library exposes an individual AsyncEventSourceClient::close(), but the application receives only a temporary raw pointer and cannot safely look the client up later. The only owner-level operation is AsyncEventSource::close(), which closes every SSE client. Retaining callback pointers risks use-after-free; closing the entire source disrupts other active sessions and creates avoidable reconnect demand.

API

  • AsyncEventSourceClient::id() returns a nonzero ID stable for that connection.
  • AsyncEventSource::onConnectWithRequest(...) exposes request metadata during connection setup while preserving the existing onConnect(...) API.
  • AsyncEventSource::closeClient(id) transfers ownership out of the shared client list before entering AsyncTCP and restores ownership if graceful close is deferred.

IDs are scoped to one AsyncEventSource. Applications retain the ID, not the client pointer, and call closeClient() later from their normal service context.

Compatibility

The existing connect callback, disconnect callback, broadcast send, close(), and client constructor calls remain source compatible. The new request-aware callback has a distinct name, avoiding overload ambiguity for existing onConnect(nullptr) callers.

Validation status

This PR is a draft. Validation completed so far is deliberately narrow:

  • Compiled as part of a full ESP32 firmware using AsyncTCP 3.5.0 and the older Arduino ESP32 2.0.17 core.
  • Live two-session test on that firmware: both SSE streams connected; invalidating session A closed only A; session B stayed connected and authenticated.
  • The ServerSentEvents example demonstrates the request-aware callback and stable client ID, but the upstream example matrix has not yet passed locally.

Before this is marked ready for review, it still needs the repository CI matrix across current supported ESP32, ESP8266, RP2040 and LibreTiny targets, plus focused lifecycle coverage for immediate disconnect, deferred close and ID lookup after disconnect.

The application-level live test also confirmed this is a targeted lifecycle improvement. It does not change lwIP TCP PCB capacity or claim to solve overload from many simultaneously active HTTP connections.

@horendus
horendus marked this pull request as draft September 19, 2026 16:40
@willmmiles

Copy link
Copy Markdown

First off, thank you so much for putting the request in context. It's much more helpful for evaluating design alternatives.

I think there's a broader scope issue here - it's not just close(), but realistically any operation on an AsyncEventSourceClient object isn't safe. I can also envision cases where an application wants to send a message to some subset of clients.

I'm thinking the cleaner answer here is for AsyncEventSource to hold shared_ptr<AsyncEventSourceClient>s instead of raw pointers, and leverage enable_shared_from_this on AsyncEventSourceClient so the onConnect() callback can take a stable shared_ptr or weak_ptr to it. This would allow applications to track client metadata themselves in a safe way, though it may be necessary to mutex the data structures built in one's onConnect and onDisconnect callbacks if they're being interacted with in other tasks. It would also allow making a safe copy of the client list for inspection; the client objects themselves may transition to a "disconnected" state asynchronously, but they wouldn't destruct unexpectedly. Finally, those objects would be usable as stable identifiers.

@github-actions

github-actions Bot commented Nov 6, 2026

Copy link
Copy Markdown

This PR is stale because it has been open 30 days with no activity. Remove stale label or comment or this will be closed in 7 days.

@github-actions github-actions Bot added the Stale label Nov 6, 2026
@github-actions

Copy link
Copy Markdown

This PR was closed because it has been stalled for 7 days with no activity.

@github-actions github-actions Bot closed this Nov 13, 2026
@mathieucarbou

Copy link
Copy Markdown
Member

@willmmiles @horendus : the bot closed this PR. I will leave it closed because the proposed code won't be merged anyway, but feel free to open an issue wrt what Will said. Will's proposal would require if I understand to pass the shared ptr to the callback, which is a breaking API change.

@mathieucarbou

Copy link
Copy Markdown
Member

@willmmiles @horendus : please look at #512

mathieucarbou added a commit that referenced this pull request Nov 13, 2026
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>
mathieucarbou added a commit that referenced this pull request Nov 13, 2026
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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants