From 489e627b594b5e2ad44834ca7f65461cf7f3faa6 Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Fri, 30 Oct 2026 10:15:14 +0100 Subject: [PATCH] Reflect out-of-band leaves in `state_after` on `/sync` Out-of-band membership events are outliers. They never enter the current state delta stream, so `_generate_room_entry` returns an empty `state` for the archived room they belong to. MSC4222 says clients "MUST only update their local state using `state_after` and NOT consider the events that appear in the timeline". A client on `state_after` therefore never learns that the user rejected a remote invite or rescinded a knock, and Element Web keeps showing the knock as pending. This copies the user's out-of-band membership event into `state_after`. It reads `room_builder.events`, the unfiltered timeline, as sliding sync does in `get_room_sync_data`: a timeline filter that drops `m.room.member` still gets the leave. The event is already in memory, so sync makes no new query. #20081 and #20314 fixed `state_after` for a leave that empties the room of local users by recovering state in `_compute_state_delta_for_incremental_sync`. An out-of-band leave has no state group and never reaches that function, because `_generate_room_entry` skips state computation for out-of-band rooms. A timeline filter that empties the batch still gets no room at all: the empty-batch shortcut returns first. The FIXME in `tests/handlers/test_sync.py` records that limitation. Co-authored-by: Quentin Gliech Co-Authored-By: Claude Fable 5.1 --- changelog.d/20438.bugfix | 1 + synapse/handlers/sync.py | 19 +++++++++++++++++++ 2 files changed, 20 insertions(+) create mode 100644 changelog.d/20438.bugfix diff --git a/changelog.d/20438.bugfix b/changelog.d/20438.bugfix new file mode 100644 index 0000000000..0e8aed3c04 --- /dev/null +++ b/changelog.d/20438.bugfix @@ -0,0 +1 @@ +Fix a bug where a client using `state_after` ([MSC4222](https://github.com/matrix-org/matrix-spec-proposals/pull/4222)) would keep showing a room as invited or knocked after the user rejected the invite or rescinded the knock. diff --git a/synapse/handlers/sync.py b/synapse/handlers/sync.py index d635465e5c..3ada731e93 100644 --- a/synapse/handlers/sync.py +++ b/synapse/handlers/sync.py @@ -3570,6 +3570,25 @@ class SyncHandler: # An out of band room won't have any state changes. state = {} + # MSC4222 clients do not apply timeline events to room state, so + # the out-of-band membership has to appear in `state_after` or they + # never learn that the user rejected the invite or rescinded the + # knock. Sliding sync does the same in `get_room_sync_data`. + # + # `events` is unfiltered, so a client filter that hides the + # membership event still gets it in `state_after`. A filter that + # empties the timeline batch gets no room at all, as the shortcut + # above returns first (see the FIXME in + # `tests/handlers/test_sync.py`). + if sync_config.use_state_after: + for oob_event in events or (): + if ( + oob_event.type == EventTypes.Member + and oob_event.state_key == sync_config.user.to_string() + and oob_event.internal_metadata.is_out_of_band_membership() + ): + state[(oob_event.type, oob_event.state_key)] = oob_event + summary: JsonDict | None = {} # we include a summary in room responses when we're lazy loading