diff --git a/changelog.d/19959.bugfix b/changelog.d/19959.bugfix new file mode 100644 index 00000000000..5bca59c9b38 --- /dev/null +++ b/changelog.d/19959.bugfix @@ -0,0 +1 @@ +Don't 500 when calling /context on an out-of-band membership event. diff --git a/synapse/handlers/room.py b/synapse/handlers/room.py index c75fff91706..7c05c70b089 100644 --- a/synapse/handlers/room.py +++ b/synapse/handlers/room.py @@ -1975,6 +1975,25 @@ async def filter_evts(events: list[EventBase]) -> list[FilteredEvent]: if not filtered: raise AuthError(403, "You don't have permission to access that event.") + if event.internal_metadata.outlier: + # The only outliers that pass the visibility filter are the user's own + # out-of-band membership events (e.g. an invite or knock received over + # federation). They have no stream position or state group, so we cannot + # compute surrounding events or state — return the event with an empty + # context rather than failing. Clients legitimately fetch such events, + # e.g. to render a push notification for an invite after the room has + # already been joined. + dummy_token = await StreamToken.START.to_string(self.store) + return EventContext( + events_before=[], + event=filtered[0], + events_after=[], + state=[], + aggregations={}, + start=dummy_token, + end=dummy_token, + ) + results = await self.store.get_events_around( room_id, event_id, before_limit, after_limit, event_filter ) diff --git a/tests/handlers/test_federation.py b/tests/handlers/test_federation.py index 0c7edbaa2da..cca341873ed 100644 --- a/tests/handlers/test_federation.py +++ b/tests/handlers/test_federation.py @@ -359,6 +359,59 @@ def create_invite() -> EventBase: exc=LimitExceededError, ) + def test_context_on_out_of_band_invite(self) -> None: + """/context on the user's own out-of-band invite returns the event with an + empty context instead of a 500: the outlier has no stream position or state + group, so surrounding events and state cannot be computed. Clients fetch such + events e.g. to render a push notification for an invite after the server has + already joined the user (auto-accepted knock).""" + other_server = "otherserver" + other_user = "@otheruser:" + other_server + + user_id = self.register_user("kermit", "test") + tok = self.login("kermit", "test") + room_id = self.helper.create_room_as(room_creator=user_id, tok=tok) + room_version = self.get_success(self.store.get_room_version(room_id)) + + invited_user = self.register_user("invitee", "test") + invited_tok = self.login("invitee", "test") + + invite = make_test_pdu_event( + { + "type": EventTypes.Member, + "content": {"membership": "invite"}, + "room_id": room_id, + "sender": other_user, + "state_key": invited_user, + "depth": 32, + "prev_events": [], + "auth_events": [], + "origin_server_ts": self.clock.time_msec(), + }, + room_version, + ) + self.get_success( + self.handler.on_invite_request(other_server, invite, invite.room_version) + ) + + channel = self.make_request( + "GET", + f"/rooms/{room_id}/context/{invite.event_id}", + access_token=invited_tok, + ) + self.assertEqual(channel.code, 200, channel.result) + self.assertEqual(channel.json_body["event"]["event_id"], invite.event_id) + self.assertEqual(channel.json_body["events_before"], []) + self.assertEqual(channel.json_body["events_after"], []) + + # Another user must NOT be able to fetch it. + channel = self.make_request( + "GET", + f"/rooms/{room_id}/context/{invite.event_id}", + access_token=tok, + ) + self.assertEqual(channel.code, 403, channel.result) + def _build_and_send_join_event( self, other_server: str, other_user: str, room_id: str ) -> EventBase: