diff --git a/CHANGELOG.md b/CHANGELOG.md
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,10 @@
 # Changelog
 
+## 0.1.5.0
+
+- Convert userDisplayName into a Maybe value [#30](https://github.com/softwarefactory-project/matrix-client-haskell/issues/30)
+- Improve empty response handling [#32](https://github.com/softwarefactory-project/matrix-client-haskell/issues/32)
+
 ## 0.1.4.3
 
 - Add missing export for Dir.
diff --git a/matrix-client.cabal b/matrix-client.cabal
--- a/matrix-client.cabal
+++ b/matrix-client.cabal
@@ -1,6 +1,6 @@
 cabal-version:       2.4
 name:                matrix-client
-version:             0.1.4.3
+version:             0.1.5.0
 synopsis:            A matrix client library
 description:
     Matrix client is a library to interface with https://matrix.org.
diff --git a/src/Network/Matrix/Client.hs b/src/Network/Matrix/Client.hs
--- a/src/Network/Matrix/Client.hs
+++ b/src/Network/Matrix/Client.hs
@@ -137,7 +137,7 @@
   )
 where
 
-import Control.Monad (mzero, void)
+import Control.Monad (mzero)
 import Control.Monad.IO.Class (MonadIO(liftIO))
 import Data.Aeson (FromJSON (..), ToJSON (..), Value (Object, String), encode, genericParseJSON, genericToJSON, object, withObject, withText, (.:), (.:?), (.=))
 import qualified Data.Aeson as Aeson
@@ -200,9 +200,9 @@
 
 -- | 'logout' allows you to destroy a session token.
 logout :: ClientSession -> MatrixIO ()
-logout session@ClientSession {..} = do
+logout session = do
   req <- mkLogoutRequest session
-  fmap (() <$) $ doRequest' @Value manager req
+  doRequestExpectEmptyResponse session "logout" req
 
 -- | The session record, use 'createSession' to create it.
 data ClientSession = ClientSession
@@ -226,6 +226,16 @@
 doRequest :: FromJSON a => ClientSession -> HTTP.Request -> MatrixIO a
 doRequest ClientSession {..} = doRequest' manager
 
+-- | Same as 'doRequest' but expect an empty JSON response @{}@
+-- which is converted to an empty Haskell tuple @()@.
+doRequestExpectEmptyResponse :: ClientSession -> String -> HTTP.Request -> MatrixIO ()
+doRequestExpectEmptyResponse sess apiName request = fmap ensureEmptyObject <$> doRequest sess request
+  where
+    ensureEmptyObject :: Value -> ()
+    ensureEmptyObject value = case value of
+      Object xs | xs == mempty -> ()
+      _ -> error $ "Unknown " <> apiName <> " response: " <> show value
+
 -- | 'getTokenOwner' gets information about the owner of a given access token.
 getTokenOwner :: ClientSession -> MatrixIO UserID
 getTokenOwner session =
@@ -249,12 +259,12 @@
   request <- mkRequest session True $ "/_matrix/client/v3/rooms/" <> rid <> "/event/" <> eid
   doRequest session request
 
-data User = User { userDisplayName :: T.Text, userAvatarUrl :: Maybe T.Text }
+data User = User { userDisplayName :: Maybe T.Text, userAvatarUrl :: Maybe T.Text }
   deriving Show
 
 instance FromJSON User where
   parseJSON = withObject "User" $ \o -> do
-    userDisplayName <- o .: "display_name"
+    userDisplayName <- o .:? "display_name"
     userAvatarUrl <- o .:? "avatar_url"
     pure $ User {..}
 
@@ -529,7 +539,7 @@
       Right crr -> case (crrID crr, crrMessage crr) of
         (Just roomID, _) -> pure $ RoomID roomID
         (_, Just message) -> Left $ MatrixError "UNKNOWN" message Nothing
-        _ -> Left $ MatrixError "UNKOWN" "" Nothing
+        _ -> Left $ MatrixError "UNKNOWN" "" Nothing
 
 newtype RoomAlias = RoomAlias T.Text deriving (Show, Eq, Ord, Hashable)
 
@@ -568,17 +578,17 @@
 setRoomAlias :: ClientSession -> RoomAlias -> RoomID -> MatrixIO ()
 setRoomAlias session (RoomAlias alias) (RoomID roomId)= do
   request <- mkRequest session True $ "/_matrix/client/v3/directory/room/" <> escapeUriComponent alias
-  doRequest
-    session $
+  doRequestExpectEmptyResponse session "set room alias" $
       request { HTTP.method = "PUT"
               , HTTP.requestBody = HTTP.RequestBodyLBS $ encode $ object [("room_id" .= roomId)]
               }
+
 -- | Delete a mapping of room alias to room ID.
 -- https://spec.matrix.org/v1.1/client-server-api/#delete_matrixclientv3directoryroomroomalias
 deleteRoomAlias :: ClientSession -> RoomAlias -> MatrixIO ()
 deleteRoomAlias session (RoomAlias alias) = do
   request <- mkRequest session True $ "/_matrix/client/v3/directory/room/" <> escapeUriComponent alias
-  doRequest session $ request { HTTP.method = "DELETE" }
+  doRequestExpectEmptyResponse session "delete room alias" $ request { HTTP.method = "DELETE" }
 
 data ResolvedAliases = ResolvedAliases [RoomAlias]
 
@@ -631,7 +641,7 @@
 inviteToRoom session (RoomID rid) (UserID uid) reason = do
   request <- mkRequest session True $ "/_matrix/client/v3/rooms/" <> rid <> "/invite"
   let body = object $ [("user_id", toJSON uid)] <> catMaybes [fmap (("reason",) . toJSON) reason]
-  doRequest session $
+  doRequestExpectEmptyResponse session "invite" $
       request { HTTP.method = "POST"
               , HTTP.requestBody = HTTP.RequestBodyLBS $ encode body
               }
@@ -671,25 +681,14 @@
 forgetRoom :: ClientSession -> RoomID -> MatrixIO ()
 forgetRoom session (RoomID roomId) = do
   request <- mkRequest session True $ "/_matrix/client/v3/rooms/" <> roomId <> "/forget"
-  fmap ensureEmptyObject <$> doRequest session (request {HTTP.method = "POST"})
-  where
-    ensureEmptyObject :: Value -> ()
-    ensureEmptyObject value = case value of
-      Object xs | xs == mempty -> ()
-      _anyOther -> error $ "Unknown forget response: " <> show value
-     
+  doRequestExpectEmptyResponse session "forget" (request {HTTP.method = "POST"})
 
 -- | Stop participating in a particular room.
 -- https://spec.matrix.org/v1.1/client-server-api/#post_matrixclientv3roomsroomidleave
 leaveRoomById :: ClientSession -> RoomID -> MatrixIO ()
 leaveRoomById session (RoomID roomId) = do
   request <- mkRequest session True $ "/_matrix/client/r0/rooms/" <> roomId <> "/leave"
-  fmap ensureEmptyObject <$> doRequest session (request {HTTP.method = "POST"})
-  where
-    ensureEmptyObject :: Value -> ()
-    ensureEmptyObject value = case value of
-      Object xs | xs == mempty -> ()
-      _anyOther -> error $ "Unknown leave response: " <> show value
+  doRequestExpectEmptyResponse session "leave" (request {HTTP.method = "POST"})
 
 -- | Kick a user from the room.
 -- https://spec.matrix.org/v1.1/client-server-api/#post_matrixclientv3roomsroomidkick
@@ -697,15 +696,10 @@
 kickUser session (RoomID roomId) (UserID uid) reason = do
   request <- mkRequest session True $ "/_matrix/client/v3/rooms/" <> roomId <> "/kick"
   let body = object $ [("user_id", toJSON uid)] <> catMaybes [fmap (("reason",) . toJSON) reason]
-  fmap (fmap ensureEmptyObject) $ doRequest session $
+  doRequestExpectEmptyResponse session "kick" $
       request { HTTP.method = "POST"
               , HTTP.requestBody = HTTP.RequestBodyLBS $ encode body
               }
-  where
-    ensureEmptyObject :: Value -> ()
-    ensureEmptyObject value = case value of
-      Object xs | xs == mempty -> ()
-      _anyOther -> error $ "Unknown leave response: " <> show value
 
 -- | Ban a user in the room. If the user is currently in the room, also kick them.
 -- https://spec.matrix.org/v1.1/client-server-api/#post_matrixclientv3roomsroomidban
@@ -713,15 +707,10 @@
 banUser session (RoomID roomId) (UserID uid) reason = do
   request <- mkRequest session True $ "/_matrix/client/v3/rooms/" <> roomId <> "/ban"
   let body = object $ [("user_id", toJSON uid)] <> catMaybes [fmap (("reason",) . toJSON) reason]
-  fmap (fmap ensureEmptyObject) $ doRequest session $
+  doRequestExpectEmptyResponse session "ban" $
       request { HTTP.method = "POST"
               , HTTP.requestBody = HTTP.RequestBodyLBS $ encode body
               }
-  where
-    ensureEmptyObject :: Value -> ()
-    ensureEmptyObject value = case value of
-      Object xs | xs == mempty -> ()
-      _anyOther -> error $ "Unknown leave response: " <> show value
 
 -- | Unban a user from the room. This allows them to be invited to the
 -- room, and join if they would otherwise be allowed to join according
@@ -731,15 +720,10 @@
 unbanUser session (RoomID roomId) (UserID uid) reason = do
   request <- mkRequest session True $ "/_matrix/client/v3/rooms/" <> roomId <> "/unban"
   let body = object $ [("user_id", toJSON uid)] <> catMaybes [fmap (("reason",) . toJSON) reason]
-  fmap (fmap ensureEmptyObject) $ doRequest session $
+  doRequestExpectEmptyResponse session "unban" $
       request { HTTP.method = "POST"
               , HTTP.requestBody = HTTP.RequestBodyLBS $ encode body
               }
-  where
-    ensureEmptyObject :: Value -> ()
-    ensureEmptyObject value = case value of
-      Object xs | xs == mempty -> ()
-      _anyOther -> error $ "Unknown leave response: " <> show value
 
 data Visibility = Public | Private
   deriving (Show)
@@ -775,15 +759,10 @@
 setRoomVisibility session (RoomID rid) visibility = do
   request <- mkRequest session True $ "/_matrix/client/v3/directory/list/room/" <> rid
   let body = object $ [("visibility", toJSON visibility)]
-  fmap (fmap ensureEmptyObject) $ doRequest session $
+  doRequestExpectEmptyResponse session "set room visibility" $
       request { HTTP.method = "PUT"
               , HTTP.requestBody = HTTP.RequestBodyLBS $ encode body
               }
-  where
-    ensureEmptyObject :: Value -> ()
-    ensureEmptyObject value = case value of
-      Object xs | xs == mempty -> ()
-      _anyOther -> error $ "Unknown setRoomVisibility response: " <> show value
 
 -- | A pagination token from a previous request, allowing clients to
 -- get the next (or previous) batch of rooms. The direction of
@@ -1310,11 +1289,10 @@
 setAccountData' :: (ToJSON a) => ClientSession -> UserID -> T.Text -> a -> MatrixIO ()
 setAccountData' session userID t value = do
   request <- mkRequest session True $ accountDataPath userID t
-  void <$> (doRequest session $ request
+  doRequestExpectEmptyResponse session "set account data" $ request
              { HTTP.method = "PUT"
              , HTTP.requestBody = HTTP.RequestBodyLBS $ encode value
-             } :: MatrixIO Aeson.Object
-           )
+             }
 
 accountDataPath :: UserID -> T.Text -> T.Text
 accountDataPath (UserID userID) t =
diff --git a/test/Spec.hs b/test/Spec.hs
--- a/test/Spec.hs
+++ b/test/Spec.hs
@@ -26,7 +26,7 @@
     _ -> do
       putStrLn "Skipping integration test"
       pure $ pure mempty
-  hspec (parallel $ spec >> runIntegration)
+  hspec (parallel spec >> runIntegration)
 
 integration :: ClientSession -> ClientSession -> Spec
 integration sess1 sess2 = do
@@ -44,26 +44,32 @@
           )
       case resp of
         Left err -> meError err `shouldBe` "Alias already exists"
-        Right (RoomID roomID) -> roomID `shouldSatisfy` (/= mempty)
+        Right (RoomID room) -> room `shouldSatisfy` (/= mempty)
     it "join room" $ do
       resp <- joinRoom sess1 "#test:localhost"
       case resp of
         Left err -> error (show err)
-        Right (RoomID roomID) -> roomID `shouldSatisfy` (/= mempty)
+        Right (RoomID room) -> room `shouldSatisfy` (/= mempty)
       resp' <- joinRoom sess2 "#test:localhost"
       case resp' of
         Left err -> error (show err)
-        Right (RoomID roomID) -> roomID `shouldSatisfy` (/= mempty)
+        Right (RoomID room) -> room `shouldSatisfy` (/= mempty)
     it "send message and reply" $ do
       -- Flush previous events
       Right sr <- sync sess2 Nothing Nothing Nothing Nothing
-      Right [room] <- getJoinedRooms sess1
+      Right (room:_) <- getJoinedRooms sess1
       let msg body = RoomMessageText $ MessageText body TextType Nothing Nothing
       let since = srNextBatch sr
       Right eventID <- sendMessage sess1 room (EventRoomMessage $ msg "Hello") (TxnID since)
       Right reply <- sendMessage sess2 room (EventRoomReply eventID $ msg "Hi!") (TxnID since)
       reply `shouldNotBe` eventID
 
+    it "invite private room" $ do
+      Right room <- createRoom sess1 $ RoomCreateRequest PrivateChat "private" "private-test" "A test"
+      Right user <- getTokenOwner sess2
+      Right inviteResult <- inviteToRoom sess1 room user (Just "Welcome!")
+      inviteResult `shouldBe` ()
+
 spec :: Spec
 spec = describe "unit tests" $ do
   it "decode unknown" $
@@ -112,10 +118,10 @@
     rateLimitSelector :: MatrixException -> Bool
     rateLimitSelector MatrixRateLimit = True
     checkPause op action = do
-      MkSystemTime start _ <- getSystemTime
+      MkSystemTime startTS _ <- getSystemTime
       void action
-      MkSystemTime end _ <- getSystemTime
-      (end - start) `shouldSatisfy` (`op` 1)
+      MkSystemTime endTS _ <- getSystemTime
+      (endTS - startTS) `shouldSatisfy` (`op` 1)
     encodePretty =
       Aeson.encodePretty'
         ( Aeson.defConfig {Aeson.confIndent = Aeson.Spaces 0, Aeson.confCompare = compare @Text}
