Repository: matrix-org/synapse

Issue:
`/_matrix/federation/v1/state` returns a 500 error if called for an outlier
```
2022-02-24 09:59:19,270 - synapse.http.server - 100 - ERROR - GET-281 - Failed handle request via 'FederationStateV1Servlet': <SynapseRequest at 0x7f683468c0a0 method='GET' uri='/_matrix/federation/v1/state/%21ItGBZyOeGywOWwJbSK%3Alocalhost%3A8840?event_id=%242DMzm1OALGG81SL30L-ArnxcWKfwrlvkv-bqehxWNdk' clientproto='HTTP/1.1' site='8849'>
Traceback (most recent call last):
  File "/venv/lib/python3.9/site-packages/twisted/internet/defer.py", line 1660, in _inlineCallbacks
    result = current_context.run(gen.send, result)
StopIteration: 0

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "/venv/lib/python3.9/site-packages/twisted/internet/defer.py", line 1660, in _inlineCallbacks
    result = current_context.run(gen.send, result)
StopIteration: {'ed25519:a_VhKa': FetchKeyResult(verify_key=<nacl.signing.VerifyKey object at 0x7f6834b83550>, valid_until_ts=1645782543370)}

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "/venv/lib/python3.9/site-packages/synapse/http/server.py", line 269, in _async_render_wrapper
    callback_return = await self._async_render(request)
  File "/venv/lib/python3.9/site-packages/synapse/http/server.py", line 471, in _async_render
    callback_return = await raw_callback_return
  File "/venv/lib/python3.9/site-packages/synapse/federation/transport/server/_base.py", line 323, in new_func
    response = await func(
  File "/venv/lib/python3.9/site-packages/synapse/federation/transport/server/federation.py", line 160, in on_GET
    return await self.handler.on_room_state_request(
  File "/venv/lib/python3.9/site-packages/synapse/federation/federation_server.py", line 535, in on_room_state_request
    await self._state_resp_cache.wrap(
  File "/venv/lib/python3.9/site-packages/synapse/util/caches/response_cache.py", line 250, in wrap
    return await make_deferred_yieldable(entry.result.observe())
  File "/venv/lib/python3.9/site-packages/twisted/internet/defer.py", line 1660, in _inlineCallbacks
    result = current_context.run(gen.send, result)
  File "/venv/lib/python3.9/site-packages/synapse/util/caches/response_cache.py", line 246, in cb
    return await callback(*args, **kwargs)
  File "/venv/lib/python3.9/site-packages/synapse/federation/federation_server.py", line 581, in _on_context_state_request_compute
    pdus: Iterable[EventBase] = await self.handler.get_state_for_pdu(
  File "/venv/lib/python3.9/site-packages/synapse/handlers/federation.py", line 1015, in get_state_for_pdu
    del results[(event.type, event.state_key)]
KeyError: ('m.room.member', '@anon-20220224_094858-726:localhost:8881')



Discussion:
seems like a 404 would make more sense here (which is what you get if you call it on a completely unknown event).

related: `/_matrix/federation/v1/state_ids` returns an empty response:

```json
{"pdu_ids":[],"auth_chain_ids":[]}
```


Resolving patch:
diff --git a/synapse/federation/federation_server.py b/synapse/federation/federation_server.py
--- a/synapse/federation/federation_server.py
+++ b/synapse/federation/federation_server.py
@@ -22,7 +22,6 @@
     Callable,
     Collection,
     Dict,
-    Iterable,
     List,
     Optional,
     Tuple,
@@ -577,10 +576,10 @@ async def _on_state_ids_request_compute(
     async def _on_context_state_request_compute(
         self, room_id: str, event_id: Optional[str]
     ) -> Dict[str, list]:
+        pdus: Collection[EventBase]
         if event_id:
-            pdus: Iterable[EventBase] = await self.handler.get_state_for_pdu(
-                room_id, event_id
-            )
+            event_ids = await self.handler.get_state_ids_for_pdu(room_id, event_id)
+            pdus = await self.store.get_events_as_list(event_ids)
         else:
             pdus = (await self.state.get_current_state(room_id)).values()
 
diff --git a/synapse/handlers/federation.py b/synapse/handlers/federation.py
--- a/synapse/handlers/federation.py
+++ b/synapse/handlers/federation.py
@@ -950,54 +950,35 @@ async def on_make_knock_request(
 
         return event
 
-    async def get_state_for_pdu(self, room_id: str, event_id: str) -> List[EventBase]:
-        """Returns the state at the event. i.e. not including said event."""
-
-        event = await self.store.get_event(event_id, check_room_id=room_id)
-
-        state_groups = await self.state_store.get_state_groups(room_id, [event_id])
-
-        if state_groups:
-            _, state = list(state_groups.items()).pop()
-            results = {(e.type, e.state_key): e for e in state}
-
-            if event.is_state():
-                # Get previous state
-                if "replaces_state" in event.unsigned:
-                    prev_id = event.unsigned["replaces_state"]
-                    if prev_id != event.event_id:
-                        prev_event = await self.store.get_event(prev_id)
-                        results[(event.type, event.state_key)] = prev_event
-                else:
-                    del results[(event.type, event.state_key)]
-
-            res = list(results.values())
-            return res
-        else:
-            return []
-
     async def get_state_ids_for_pdu(self, room_id: str, event_id: str) -> List[str]:
         """Returns the state at the event. i.e. not including said event."""
         event = await self.store.get_event(event_id, check_room_id=room_id)
+        if event.internal_metadata.outlier:
+            raise NotFoundError("State not known at event %s" % (event_id,))
 
         state_groups = await self.state_store.get_state_groups_ids(room_id, [event_id])
 
-        if state_groups:
-            _, state = list(state_groups.items()).pop()
-            results = state
+        # get_state_groups_ids should return exactly one result
+        assert len(state_groups) == 1
 
-            if event.is_state():
-                # Get previous state
-                if "replaces_state" in event.unsigned:
-                    prev_id = event.unsigned["replaces_state"]
-                    if prev_id != event.event_id:
-                        results[(event.type, event.state_key)] = prev_id
-                else:
-                    results.pop((event.type, event.state_key), None)
+        state_map = next(iter(state_groups.values()))
 
-            return list(results.values())
-        else:
-            return []
+        state_key = event.get_state_key()
+        if state_key is not None:
+            # the event was not rejected (get_event raises a NotFoundError for rejected
+            # events) so the state at the event should include the event itself.
+            assert (
+                state_map.get((event.type, state_key)) == event.event_id
+            ), "State at event did not include event itself"
+
+            # ... but we need the state *before* that event
+            if "replaces_state" in event.unsigned:
+                prev_id = event.unsigned["replaces_state"]
+                state_map[(event.type, state_key)] = prev_id
+            else:
+                del state_map[(event.type, state_key)]
+
+        return list(state_map.values())
 
     async def on_backfill_request(
         self, origin: str, room_id: str, pdu_list: List[str], limit: int


Test patch: