Skip to content

_send_server_awareness logs ExceptionGroup as opaque "unhandled errors in a TaskGroup" string #141

@yamaaaaaa31

Description

@yamaaaaaa31

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions