diff --git a/src/api/client_server/message.rs b/src/api/client_server/message.rs
index d332bcff606fcca808b4ccd956dd8f4c3cd358e9..32a2100385a582e198aca6ceecbd703b73d6d720 100644
--- a/src/api/client_server/message.rs
+++ b/src/api/client_server/message.rs
@@ -9,6 +9,7 @@
     },
     events::{StateEventType, TimelineEventType},
 };
+use serde_json::from_str;
 use std::{
     collections::{BTreeMap, HashSet},
     sync::Arc,
@@ -48,6 +49,52 @@ pub async fn send_message_event_route(
         ));
     }
 
+    // certain event types require certain fields to be valid in request bodies.
+    // this helps prevent attempting to handle events that we can't deserialise later so don't waste resources on it.
+    //
+    // see https://spec.matrix.org/v1.9/client-server-api/#events-2 for what's required per event type.
+    match body.event_type.to_string().into() {
+        TimelineEventType::RoomMessage => {
+            let body_field = body.body.body.get_field::<String>("body");
+            let msgtype_field = body.body.body.get_field::<String>("msgtype");
+
+            if body_field.is_err() {
+                return Err(Error::BadRequest(
+                    ErrorKind::InvalidParam,
+                    "'body' field in JSON request is invalid",
+                ));
+            }
+
+            if msgtype_field.is_err() {
+                return Err(Error::BadRequest(
+                    ErrorKind::InvalidParam,
+                    "'msgtype' field in JSON request is invalid",
+                ));
+            }
+        }
+        TimelineEventType::RoomName => {
+            let name_field = body.body.body.get_field::<String>("name");
+
+            if name_field.is_err() {
+                return Err(Error::BadRequest(
+                    ErrorKind::InvalidParam,
+                    "'name' field in JSON request is invalid",
+                ));
+            }
+        }
+        TimelineEventType::RoomTopic => {
+            let topic_field = body.body.body.get_field::<String>("topic");
+
+            if topic_field.is_err() {
+                return Err(Error::BadRequest(
+                    ErrorKind::InvalidParam,
+                    "'topic' field in JSON request is invalid",
+                ));
+            }
+        }
+        _ => {} // event may be custom/experimental or can be empty don't do anything with it
+    };
+
     // Check if this is a new transaction id
     if let Some(response) =
         services()
@@ -79,7 +126,7 @@ pub async fn send_message_event_route(
         .build_and_append_pdu(
             PduBuilder {
                 event_type: body.event_type.to_string().into(),
-                content: serde_json::from_str(body.body.body.json().get())
+                content: from_str(body.body.body.json().get())
                     .map_err(|_| Error::BadRequest(ErrorKind::BadJson, "Invalid JSON body."))?,
                 unsigned: Some(unsigned),
                 state_key: None,
diff --git a/src/api/server_server.rs b/src/api/server_server.rs
index 1570b54161fbe4f43da7cce85daf6eea53cfe98a..41c215d8dd6737876a426c748b5f6da39b7e1b82 100644
--- a/src/api/server_server.rs
+++ b/src/api/server_server.rs
@@ -1822,8 +1822,10 @@ pub async fn create_invite_route(
         ));
     }
 
-    let mut signed_event = utils::to_canonical_object(&body.event)
-        .map_err(|_| Error::BadRequest(ErrorKind::InvalidParam, "Invite event is invalid."))?;
+    let mut signed_event = utils::to_canonical_object(&body.event).map_err(|e| {
+        error!("Failed to convert invite event to canonical JSON: {}", e);
+        Error::BadRequest(ErrorKind::InvalidParam, "Invite event is invalid.")
+    })?;
 
     ruma::signatures::hash_and_sign_event(
         services().globals.server_name().as_str(),
diff --git a/src/service/rooms/timeline/mod.rs b/src/service/rooms/timeline/mod.rs
index 11f1d701b1f6a2bc456388b5d65b7c9d95f68163..be65815a34730334e5d9b1faea594d3c623921ee 100644
--- a/src/service/rooms/timeline/mod.rs
+++ b/src/service/rooms/timeline/mod.rs
@@ -260,8 +260,17 @@ pub async fn append_pdu<'a>(
                         unsigned.insert(
                             "prev_content".to_owned(),
                             CanonicalJsonValue::Object(
-                                utils::to_canonical_object(prev_state.content.clone())
-                                    .expect("event is valid, we just created it"),
+                                utils::to_canonical_object(prev_state.content.clone()).map_err(
+                                    |e| {
+                                        error!(
+                                            "Failed to convert prev_state to canonical JSON: {}",
+                                            e
+                                        );
+                                        Error::bad_database(
+                                            "Failed to convert prev_state to canonical JSON.",
+                                        )
+                                    },
+                                )?,
                             ),
                         );
                     }
@@ -802,7 +811,7 @@ struct RoomCreate {
             |k, s| auth_events.get(&(k.clone(), s.to_owned())),
         )
         .map_err(|e| {
-            error!("Auth check for PDU {:?} failed: {:?}", &pdu, e);
+            error!("Auth check failed: {:?}", e);
             Error::bad_database("Auth check failed.")
         })?;
 
@@ -815,7 +824,7 @@ struct RoomCreate {
 
         // Hash and sign
         let mut pdu_json = utils::to_canonical_object(&pdu).map_err(|e| {
-            error!("Failed to convert PDU {:?} to canonical JSON: {}", &pdu, e);
+            error!("Failed to convert PDU to canonical JSON: {}", e);
             Error::bad_database("Failed to convert PDU to canonical JSON.")
         })?;
 
@@ -1105,7 +1114,10 @@ pub fn redact_pdu(&self, event_id: &EventId, reason: &PduEvent) -> Result<()> {
             pdu.redact(room_version_id, reason)?;
             self.replace_pdu(
                 &pdu_id,
-                &utils::to_canonical_object(&pdu).expect("PDU is an object"),
+                &utils::to_canonical_object(&pdu).map_err(|e| {
+                    error!("Failed to convert PDU to canonical JSON: {}", e);
+                    Error::bad_database("Failed to convert PDU to canonical JSON.")
+                })?,
                 &pdu,
             )?;
         }