Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 12 additions & 1 deletion plugins/platforms/matrix/adapter.py
Original file line number Diff line number Diff line change
Expand Up @@ -2351,7 +2351,18 @@ async def _dispatch_sync(self, sync_data: Dict[str, Any]) -> None:
if inspect.isawaitable(tasks):
tasks = await tasks
if tasks:
await asyncio.gather(*tasks)
# return_exceptions=True so one failing event handler doesn't abort
# the whole gather and silently drop the SIBLING events in the same
# sync response (a bare gather re-raises the first exception, leaving
# the rest of the batch unprocessed). Mirrors the invite/redaction
# gathers above. Surface each failure instead of swallowing it.
results = await asyncio.gather(*tasks, return_exceptions=True)
for result in results:
if isinstance(result, Exception):
logger.warning(
"Matrix: event handler failed during sync dispatch: %s",
result,
)

def _is_self_sender(self, sender: str) -> bool:
"""Return True if the sender refers to the bot's own account.
Expand Down
33 changes: 33 additions & 0 deletions tests/gateway/test_matrix.py
Original file line number Diff line number Diff line change
Expand Up @@ -5276,3 +5276,36 @@ async def test_flag_clears_when_second_connect_resolves_device_id(self):
assert None not in _verify_call.args[0]["@bot:example.org"]

await adapter.disconnect()


class TestMatrixDispatchSyncIsolation:
"""A failing mautrix event handler must not abort the whole sync batch.

``_dispatch_sync`` gathers the per-event handler tasks. Without
``return_exceptions=True`` the first exception aborts the gather and the
sibling events in the same sync response are silently dropped.
"""

@pytest.mark.asyncio
async def test_dispatch_sync_isolates_failing_handler(self, caplog):
import logging

adapter = _make_adapter()
ran = {"ok": False}

async def _boom():
raise RuntimeError("handler boom")

async def _ok():
ran["ok"] = True

client = MagicMock()
client.handle_sync = MagicMock(return_value=[_boom(), _ok()])
adapter._client = client

with caplog.at_level(logging.WARNING):
# Must not raise despite the failing handler.
await adapter._dispatch_sync({"next_batch": "s1"})

assert ran["ok"] is True # the sibling handler still ran
assert "event handler failed" in caplog.text # failure surfaced, not swallowed
Loading