fix(matrix): decline dead/abandoned invites instead of retrying forever (#56222)

An invite to a room with no remaining members surfaces as "no servers
in the room have been provided" or "room not found" on join. The pending
invite was never cleared, so every gateway startup re-attempted the join
and re-emitted the warning indefinitely.

Detect that specific failure mode by narrow error-message match and call
leave_room to decline the invite; transient/network errors leave the
invite untouched for the next sync. Adds 5 tests.

Reimplements the matrix portion of #33953 onto the current plugin adapter
(gateway/platforms/matrix.py was relocated to
plugins/platforms/matrix/adapter.py since the PR was opened). The two
gateway/status.py fixes from that PR (wrapper-subcommand rejection,
psutil start-time fallback) already landed on main independently.

Reported by @Bougey; original patch authored by @KiraKatana.
This commit is contained in:
Teknium 2026-07-01 02:44:18 -07:00 committed by GitHub
parent 88d6e833f1
commit 275e293f54
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 97 additions and 0 deletions

View file

@ -2947,6 +2947,19 @@ class MatrixAdapter(BasePlatformAdapter):
return True
except Exception as exc:
logger.warning("Matrix: error joining %s: %s", room_id, exc)
# Abandoned rooms (no current members) surface as "no servers
# in the room have been provided" or "room not found". The
# pending invite keeps retrying every startup unless we
# explicitly leave it. The match is narrow enough that
# transient failures still leave the invite untouched for the
# next try.
msg = str(exc).lower()
if ("no servers" in msg) or ("room not found" in msg):
try:
await self._client.leave_room(RoomID(room_id))
logger.info("Matrix: declined dead invite to %s", room_id)
except Exception:
pass
return False
def _schedule_invite_join(

View file

@ -4527,3 +4527,87 @@ class TestCreateMatrixSession:
assert session.connector is fake_connector
finally:
await session.close()
class TestMatrixDeadInviteHandling:
"""Tests for _join_room_by_id auto-leaving dead/abandoned rooms.
Regression: when a room had no current members, ``join_room`` raised
``MUnknown: Can't join remote room because no servers that are in the
room have been provided``. The pending invite stayed in the bot's view
of the world, so every gateway restart re-attempted the join and
re-emitted the warning indefinitely. There was no path that ever
cleared the invite.
"""
def setup_method(self):
self.adapter = _make_adapter()
self.adapter._refresh_dm_cache = AsyncMock()
@pytest.mark.asyncio
async def test_no_servers_error_triggers_leave(self):
join_err = Exception(
"Can't join remote room because no servers that are in the "
"room have been provided."
)
self.adapter._client = types.SimpleNamespace(
join_room=AsyncMock(side_effect=join_err),
leave_room=AsyncMock(),
)
result = await self.adapter._join_room_by_id("!dead:example.org")
assert result is False
self.adapter._client.leave_room.assert_awaited_once()
# leave_room receives a RoomID-wrapped value; verify the underlying str.
leave_arg = self.adapter._client.leave_room.await_args.args[0]
assert str(leave_arg) == "!dead:example.org"
@pytest.mark.asyncio
async def test_room_not_found_error_triggers_leave(self):
join_err = Exception("M_NOT_FOUND: Room not found")
self.adapter._client = types.SimpleNamespace(
join_room=AsyncMock(side_effect=join_err),
leave_room=AsyncMock(),
)
await self.adapter._join_room_by_id("!gone:example.org")
self.adapter._client.leave_room.assert_awaited_once()
@pytest.mark.asyncio
async def test_transient_error_does_not_trigger_leave(self):
"""A network blip or 5xx must NOT decline the invite — the bot
should retry on the next sync cycle."""
join_err = Exception("Connection reset by peer")
self.adapter._client = types.SimpleNamespace(
join_room=AsyncMock(side_effect=join_err),
leave_room=AsyncMock(),
)
result = await self.adapter._join_room_by_id("!transient:example.org")
assert result is False
self.adapter._client.leave_room.assert_not_awaited()
@pytest.mark.asyncio
async def test_successful_join_does_not_attempt_leave(self):
self.adapter._client = types.SimpleNamespace(
join_room=AsyncMock(return_value=None),
leave_room=AsyncMock(),
)
result = await self.adapter._join_room_by_id("!alive:example.org")
assert result is True
assert "!alive:example.org" in self.adapter._joined_rooms
self.adapter._client.leave_room.assert_not_awaited()
@pytest.mark.asyncio
async def test_leave_room_failure_is_swallowed(self):
"""If leave_room itself fails (e.g. server returns 500), the helper
must still return False cleanly rather than re-raise."""
self.adapter._client = types.SimpleNamespace(
join_room=AsyncMock(side_effect=Exception("no servers in the room")),
leave_room=AsyncMock(side_effect=Exception("500 internal")),
)
result = await self.adapter._join_room_by_id("!brokenleave:example.org")
assert result is False