Repository: matrix-org/synapse

Issue:
_contains_display_name evaluator barfs on events with no body
given an incoming event with no `content.body`:
```
2020-06-15 19:00:06,270 - synapse.federation.federation_server - 290 - ERROR - PUT-1359406-$1592244001432863GBGGz:matrix.org- Failed to handle PDU $1592244001432863GBGGz:matrix.org
...
Traceback (most recent call last):
  File "/opt/synapse/synapse/synapse/federation/federation_server.py", line 279, in process_pdus_for_room
    await self._handle_received_pdu(origin, pdu)
  File "/opt/synapse/synapse/synapse/federation/federation_server.py", line 658, in _handle_received_pdu
    await self.handler.on_receive_pdu(origin, pdu, sent_to_us_directly=True)
  File "/opt/synapse/synapse/synapse/handlers/federation.py", line 412, in on_receive_pdu
    await self._process_received_pdu(origin, pdu, state=state)
  File "/opt/synapse/synapse/synapse/handlers/federation.py", line 690, in _process_received_pdu
    context = await self._handle_new_event(origin, event, state=state)
  File "/opt/synapse/synapse/synapse/handlers/federation.py", line 1841, in _handle_new_event
    event, context
  File "/opt/synapse/synapse/synapse/push/action_generator.py", line 43, in handle_push_actions_for_event
    yield self.bulk_evaluator.action_for_event_by_user(event, context)
  File "/opt/synapse/env3/lib/python3.5/site-packages/twisted/internet/defer.py", line 1418, in _inlineCallbacks
    result = g.send(result)
  File "/opt/synapse/synapse/synapse/push/bulk_push_rule_evaluator.py", line 190, in action_for_event_by_user
    evaluator, rule["conditions"], uid, display_name, condition_cache
  File "/opt/synapse/synapse/synapse/push/bulk_push_rule_evaluator.py", line 215, in _condition_checker
    res = evaluator.matches(cond, uid, display_name)
  File "/opt/synapse/synapse/synapse/push/push_rule_evaluator.py", line 107, in matches
    return self._contains_display_name(display_name)
  File "/opt/synapse/synapse/synapse/push/push_rule_evaluator.py", line 161, in _contains_display_name
    return r.search(body)
TypeError: expected string or bytes-like object
2020-06-15 19:00:06,285 - synapse.logging.context - 396 - WARNING - PUT-1359406-$1592244001432863GBGGz:matrix.org- Re-starting finished log context PUT-1359406-$1592244001432863GBGGz:matrix.org
```


Discussion:
We check right above this whether body exists

https://github.com/matrix-org/synapse/blob/7cb8b4bc67042a39bd1b0e05df46089a2fce1955/synapse/push/push_rule_evaluator.py#L150-L151

I wonder if this `body` in this case is an object or something here.
my bad.

```json
{
  "content": {
    "body": 1
  },
  "event_id": "$1592244001432863GBGGz:matrix.org",
  "origin_server_ts": 1592244001222,
  "sender": "@richvdh:matrix.org",
  "type": "org.richvdh.n",
  "unsigned": {
    "age": 3129,
    "transaction_id": "m1592244001050.2"
  },
  "room_id": "!qOZKfwKPirAoSosXrf:matrix.org"
}
```
and now the whole damn room is broken :-S. subsequent events give:

```
Exception: Error fetching missing prev_events for $1592244953434486sDgfG:matrix.org: expected string or bytes-like object
```

Resolving patch:
diff --git a/synapse/push/push_rule_evaluator.py b/synapse/push/push_rule_evaluator.py
--- a/synapse/push/push_rule_evaluator.py
+++ b/synapse/push/push_rule_evaluator.py
@@ -131,7 +131,7 @@ def _event_match(self, condition: dict, user_id: str) -> bool:
         # XXX: optimisation: cache our pattern regexps
         if condition["key"] == "content.body":
             body = self._event.content.get("body", None)
-            if not body:
+            if not body or not isinstance(body, str):
                 return False
 
             return _glob_matches(pattern, body, word_boundary=True)
@@ -147,7 +147,7 @@ def _contains_display_name(self, display_name: str) -> bool:
             return False
 
         body = self._event.content.get("body", None)
-        if not body:
+        if not body or not isinstance(body, str):
             return False
 
         # Similar to _glob_matches, but do not treat display_name as a glob.


Test patch:
diff --git a/tests/push/test_push_rule_evaluator.py b/tests/push/test_push_rule_evaluator.py
--- a/tests/push/test_push_rule_evaluator.py
+++ b/tests/push/test_push_rule_evaluator.py
@@ -21,7 +21,7 @@
 
 
 class PushRuleEvaluatorTestCase(unittest.TestCase):
-    def setUp(self):
+    def _get_evaluator(self, content):
         event = FrozenEvent(
             {
                 "event_id": "$event_id",
@@ -29,37 +29,58 @@ def setUp(self):
                 "sender": "@user:test",
                 "state_key": "",
                 "room_id": "@room:test",
-                "content": {"body": "foo bar baz"},
+                "content": content,
             },
             RoomVersions.V1,
         )
         room_member_count = 0
         sender_power_level = 0
         power_levels = {}
-        self.evaluator = PushRuleEvaluatorForEvent(
+        return PushRuleEvaluatorForEvent(
             event, room_member_count, sender_power_level, power_levels
         )
 
     def test_display_name(self):
         """Check for a matching display name in the body of the event."""
+        evaluator = self._get_evaluator({"body": "foo bar baz"})
+
         condition = {
             "kind": "contains_display_name",
         }
 
         # Blank names are skipped.
-        self.assertFalse(self.evaluator.matches(condition, "@user:test", ""))
+        self.assertFalse(evaluator.matches(condition, "@user:test", ""))
 
         # Check a display name that doesn't match.
-        self.assertFalse(self.evaluator.matches(condition, "@user:test", "not found"))
+        self.assertFalse(evaluator.matches(condition, "@user:test", "not found"))
 
         # Check a display name which matches.
-        self.assertTrue(self.evaluator.matches(condition, "@user:test", "foo"))
+        self.assertTrue(evaluator.matches(condition, "@user:test", "foo"))
 
         # A display name that matches, but not a full word does not result in a match.
-        self.assertFalse(self.evaluator.matches(condition, "@user:test", "ba"))
+        self.assertFalse(evaluator.matches(condition, "@user:test", "ba"))
 
         # A display name should not be interpreted as a regular expression.
-        self.assertFalse(self.evaluator.matches(condition, "@user:test", "ba[rz]"))
+        self.assertFalse(evaluator.matches(condition, "@user:test", "ba[rz]"))
 
         # A display name with spaces should work fine.
-        self.assertTrue(self.evaluator.matches(condition, "@user:test", "foo bar"))
+        self.assertTrue(evaluator.matches(condition, "@user:test", "foo bar"))
+
+    def test_no_body(self):
+        """Not having a body shouldn't break the evaluator."""
+        evaluator = self._get_evaluator({})
+
+        condition = {
+            "kind": "contains_display_name",
+        }
+        self.assertFalse(evaluator.matches(condition, "@user:test", "foo"))
+
+    def test_invalid_body(self):
+        """A non-string body should not break the evaluator."""
+        condition = {
+            "kind": "contains_display_name",
+        }
+
+        for body in (1, True, {"foo": "bar"}):
+            evaluator = self._get_evaluator({"body": body})
+            self.assertFalse(evaluator.matches(condition, "@user:test", "foo"))