mirror of
https://github.com/element-hq/synapse.git
synced 2026-08-14 13:40:39 +00:00
Don't 500 /sync when an always-included event has no state
filter_and_transform_events_for_client computes the requesting user's membership (MSC4115) from the state after each event, fetched only for non-outlier events. An event let through by always_include_ids therefore has no state to look in if it is flagged as an outlier — which happens when an out-of-band membership event is later de-outliered (we are also in the room and received it over federation) and enters the current state (sync's always_include_ids), while the copy being filtered (e.g. from a worker's not-yet-invalidated cache) still carries the outlier flag. This hit the 'unreachable' raise, failing the whole /sync with a 500. Return the event without a membership annotation instead (MSC4115 permits omitting it), and downgrade the neighbouring call-invite assert with the same reachability hole to a guard. Fixes #19858. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Uk8aPxHn3BHCe52L226jdG
This commit is contained in:
co-authored by
Claude Fable 5
parent
ec6c53e7df
commit
ed885f590a
@@ -0,0 +1 @@
|
||||
Fix a bug where `/sync` could return a 500 error ("Missing state for event that is not user's own membership") when an out-of-band membership event, since de-outliered into the room's current state, appeared in a room timeline.
|
||||
+28
-17
@@ -196,21 +196,17 @@ async def filter_and_transform_events_for_client(
|
||||
# Filter out call invites in public rooms, as this would potentially
|
||||
# ring a lot of users.
|
||||
if event.type == EventTypes.CallInvite and not event.is_state():
|
||||
# `state_after_event` should only be None if the event is an outlier,
|
||||
# and earlier code should filter out outliers entirely.
|
||||
#
|
||||
# In addition, we only create outliers locally for out-of-band
|
||||
# invite rejections, invites received over federation, or state
|
||||
# events needed to authorise other events. None of this applies to
|
||||
# call invites.
|
||||
assert state_after_event is not None
|
||||
|
||||
room_join_rules = state_after_event.get((EventTypes.JoinRules, ""))
|
||||
if (
|
||||
room_join_rules is not None
|
||||
and room_join_rules.content.get("join_rule") == JoinRules.PUBLIC
|
||||
):
|
||||
return None
|
||||
# `state_after_event` should only be None if the event is an outlier
|
||||
# (or our copy of the event is stale and still flagged as one, see
|
||||
# below); in either case we can't check the join rules, so let the
|
||||
# event through.
|
||||
if state_after_event is not None:
|
||||
room_join_rules = state_after_event.get((EventTypes.JoinRules, ""))
|
||||
if (
|
||||
room_join_rules is not None
|
||||
and room_join_rules.content.get("join_rule") == JoinRules.PUBLIC
|
||||
):
|
||||
return None
|
||||
|
||||
# Annotate the event with the user's membership after the event.
|
||||
#
|
||||
@@ -224,8 +220,23 @@ async def filter_and_transform_events_for_client(
|
||||
elif state_after_event is not None:
|
||||
user_membership_event = state_after_event.get((EventTypes.Member, user_id))
|
||||
else:
|
||||
# unreachable!
|
||||
raise Exception("Missing state for event that is not user's own membership")
|
||||
# We have no state for this event: it was flagged as an outlier
|
||||
# when we fetched the state above, but `always_include_ids` let it
|
||||
# through anyway. This can happen when an out-of-band membership
|
||||
# event (e.g. an invite, or a rejection of one, received while not
|
||||
# in the room) is later de-outliered — because we are also in the
|
||||
# room and received it over federation — and enters the current
|
||||
# state, while the copy of the event we are working with (e.g. from
|
||||
# a worker's not-yet-invalidated cache) still carries the outlier
|
||||
# flag. We don't know the user's membership at this point in the
|
||||
# DAG, which MSC4115 permits — return the event unannotated rather
|
||||
# than failing the whole request.
|
||||
logger.info(
|
||||
"filter_and_transform_events_for_client: no state for always-included"
|
||||
" outlier %s; returning it without a membership annotation",
|
||||
event.event_id,
|
||||
)
|
||||
return FilteredEvent(event=filtered, membership=None)
|
||||
|
||||
user_membership = (
|
||||
user_membership_event.membership
|
||||
|
||||
@@ -665,6 +665,31 @@ class FilterEventsOutOfBandEventsForClientTestCase(
|
||||
[],
|
||||
)
|
||||
|
||||
# ... unless the events are in `always_include_ids`. This mirrors what
|
||||
# happens when an out-of-band membership event is later de-outliered
|
||||
# (because we are also in the room and received it over federation) and
|
||||
# enters the current state — which sync passes as `always_include_ids`
|
||||
# — while the copy of the event being filtered still carries the
|
||||
# outlier flag. We can't compute the requesting user's membership at
|
||||
# such an event (we have no state for it), but it must be let through
|
||||
# without a membership annotation rather than failing the whole
|
||||
# request.
|
||||
#
|
||||
# Regression test for https://github.com/element-hq/synapse/issues/19858.
|
||||
filtered_events = self.get_success(
|
||||
filter_and_transform_events_for_client(
|
||||
self.hs.get_storage_controllers(),
|
||||
"@other:test",
|
||||
[invite_event, reject_event],
|
||||
always_include_ids=frozenset({invite_event_id}),
|
||||
)
|
||||
)
|
||||
self.assertEqual(
|
||||
[e.event.event_id for e in filtered_events],
|
||||
[invite_event_id],
|
||||
)
|
||||
self.assertEqual([e.membership for e in filtered_events], [None])
|
||||
|
||||
|
||||
async def inject_visibility_event(
|
||||
hs: HomeServer,
|
||||
|
||||
Reference in New Issue
Block a user