Summary
YRoom._send_server_awareness broadcasts awareness updates with anyio.create_task_group, but the except Exception as e: self.log.error("...: %s", e) block formats the resulting BaseExceptionGroup via str(e), which collapses to messages like
Error while broadcasting awareness changes: unhandled errors in a TaskGroup (1 sub-exception)
The real sub-exceptions (typically websockets.ConnectionClosed* for clients that disconnected mid-broadcast, or genuine bugs) are never written to the logs, so production failures are effectively un-triagable.
Reproduction
pycrdt-websocket == 0.16.1, anyio >= 4, Python 3.12+.
- Open a YRoom session with multiple clients; close one tab while another client is sending updates so an awareness broadcast races a disconnect.
- The server logs only the opaque
unhandled errors in a TaskGroup (N sub-exceptions) message, with no information about which client failed or why.
Source (0.16.1, pycrdt/websocket/yroom.py:375)
async def _send_server_awareness(self, state: bytes) -> None:
try:
async with create_task_group() as tg:
for client in self.clients:
self.log.debug(
"Sending awareness from server to client with endpoint: %s",
client.path,
)
tg.start_soon(client.send, state)
except Exception as e:
self.log.error("Error while broadcasting awareness changes: %s", e)
Suggested fix
Catch BaseExceptionGroup explicitly, flatten it, and log each sub-exception with its type. Optionally demote the known ConnectionClosed* cases to debug, since client disconnects mid-broadcast are routine:
def _flatten_eg(eg: BaseExceptionGroup) -> list[BaseException]:
out: list[BaseException] = []
for e in eg.exceptions:
out.extend(_flatten_eg(e) if isinstance(e, BaseExceptionGroup) else [e])
return out
async def _send_server_awareness(self, state: bytes) -> None:
try:
async with create_task_group() as tg:
for client in self.clients:
tg.start_soon(client.send, state)
except BaseExceptionGroup as eg:
for e in _flatten_eg(eg):
self.log.error(
"Error while broadcasting awareness changes: %s: %s",
type(e).__name__,
e,
)
except Exception as e:
self.log.error(
"Error while broadcasting awareness changes: %s: %s",
type(e).__name__,
e,
)
Happy to send a PR if maintainers want one.
Summary
YRoom._send_server_awarenessbroadcasts awareness updates withanyio.create_task_group, but theexcept Exception as e: self.log.error("...: %s", e)block formats the resultingBaseExceptionGroupviastr(e), which collapses to messages likeThe real sub-exceptions (typically
websockets.ConnectionClosed*for clients that disconnected mid-broadcast, or genuine bugs) are never written to the logs, so production failures are effectively un-triagable.Reproduction
pycrdt-websocket == 0.16.1,anyio >= 4, Python 3.12+.unhandled errors in a TaskGroup (N sub-exceptions)message, with no information about which client failed or why.Source (0.16.1,
pycrdt/websocket/yroom.py:375)Suggested fix
Catch
BaseExceptionGroupexplicitly, flatten it, and log each sub-exception with its type. Optionally demote the knownConnectionClosed*cases todebug, since client disconnects mid-broadcast are routine:Happy to send a PR if maintainers want one.