chiark / gitweb /
Use Mastodon's JSON error-documents.
authorSimon Tatham <anakin@pobox.com>
Wed, 24 Jan 2024 20:06:44 +0000 (20:06 +0000)
committerSimon Tatham <anakin@pobox.com>
Wed, 24 Jan 2024 20:08:55 +0000 (20:08 +0000)
When a Mastodon API request returns an HTTP error status, it generally
also delivers a JSON error-document containing a string. This is a
much better error message than the plain HTTP code, because often it
goes into more useful detail. For example, if you have an auth failure
because you haven't confirmed the email address on your new account
yet, the HTTP error code can't tell you anything about email address
confirmation, but the error-document will. So we'll use those messages
in preference, if they're available.

TODO.md
src/client.rs
src/types.rs

diff --git a/TODO.md b/TODO.md
index eb1393f5490bd14b11b5a1fd01dd6b7ec1edc4db..eac263d242667654a04330b6e639d564fa4b2f22 100644 (file)
--- a/TODO.md
+++ b/TODO.md
@@ -1,12 +1,3 @@
-# Bugs
-
-Server error reporting: HTTP error codes from Mastodon API requests
-are generally accompanied by a JSON body containing a more useful
-message. For example, if you prematurely try to start the full client
-after an unconfirmed account registration, the error code just says
-"403 Forbidden", but the message body explains that the email address
-isn't confirmed yet, so that a user might actually know what to do.
-
 # Missing features
 
 ## Verbose help
index 8a9c9de4fa0cb7ba296dd84048bc49286584d001..270add3924f919bb0998e16e890f660a0b6b58d5 100644 (file)
@@ -141,6 +141,23 @@ impl From<reqwest::Error> for ClientError {
     }
 }
 
+impl ClientError {
+    fn from_response(url: &str, rsp: reqwest::blocking::Response) -> Self {
+        let rspstatus = rsp.status();
+        let message = if let Ok(text) = rsp.text() {
+            if let Ok(err) = serde_json::from_str::<ServerError>(&text) {
+                err.error
+            } else {
+                rspstatus.to_string()
+            }
+        } else {
+            rspstatus.to_string()
+        };
+
+        ClientError::UrlError(url.to_owned(), message)
+    }
+}
+
 impl std::fmt::Display for ClientError {
     fn fmt(
         &self,
@@ -571,7 +588,7 @@ impl Client {
         let (url, rsp) = self.api_request(Req::get("api/v2/instance"))?;
         let rspstatus = rsp.status();
         let inst: Instance = if !rspstatus.is_success() {
-            Err(ClientError::UrlError(url, rspstatus.to_string()))
+            Err(ClientError::from_response(&url, rsp))
         } else {
             match serde_json::from_str(&rsp.text()?) {
                 Ok(ac) => Ok(ac),
@@ -647,7 +664,7 @@ impl Client {
         let (url, rsp) = self.api_request(req)?;
         let rspstatus = rsp.status();
         let ac: Account = if !rspstatus.is_success() {
-            Err(ClientError::UrlError(url.clone(), rspstatus.to_string()))
+            Err(ClientError::from_response(&url, rsp))
         } else {
             match serde_json::from_str(&rsp.text()?) {
                 Ok(ac) => Ok(ac),
@@ -685,7 +702,7 @@ impl Client {
             self.api_request(Req::get(&("api/v1/polls/".to_owned() + id)))?;
         let rspstatus = rsp.status();
         let poll: Poll = if !rspstatus.is_success() {
-            Err(ClientError::UrlError(url.clone(), rspstatus.to_string()))
+            Err(ClientError::from_response(&url, rsp))
         } else {
             match serde_json::from_str(&rsp.text()?) {
                 Ok(poll) => Ok(poll),
@@ -713,7 +730,7 @@ impl Client {
         )?;
         let rspstatus = rsp.status();
         let rels: Vec<Relationship> = if !rspstatus.is_success() {
-            Err(ClientError::UrlError(url.clone(), rspstatus.to_string()))
+            Err(ClientError::from_response(&url, rsp))
         } else {
             match serde_json::from_str(&rsp.text()?) {
                 Ok(ac) => Ok(ac),
@@ -760,7 +777,7 @@ impl Client {
             self.api_request(Req::get(&("api/v1/statuses/".to_owned() + id)))?;
         let rspstatus = rsp.status();
         let st: Status = if !rspstatus.is_success() {
-            Err(ClientError::UrlError(url.clone(), rspstatus.to_string()))
+            Err(ClientError::from_response(&url, rsp))
         } else {
             match serde_json::from_str(&rsp.text()?) {
                 Ok(st) => Ok(st),
@@ -804,7 +821,7 @@ impl Client {
         ))?;
         let rspstatus = rsp.status();
         let not: Notification = if !rspstatus.is_success() {
-            Err(ClientError::UrlError(url.clone(), rspstatus.to_string()))
+            Err(ClientError::from_response(&url, rsp))
         } else {
             match serde_json::from_str(&rsp.text()?) {
                 Ok(st) => Ok(st),
@@ -1493,7 +1510,7 @@ impl Client {
         let (url, rsp) = self.api_request(req)?;
         let rspstatus = rsp.status();
         let ac: Account = if !rspstatus.is_success() {
-            Err(ClientError::UrlError(url.clone(), rspstatus.to_string()))
+            Err(ClientError::from_response(&url, rsp))
         } else {
             match serde_json::from_str(&rsp.text()?) {
                 Ok(ac) => Ok(ac),
index 3a4f009d6734f8a52d1d1105628baee5fd76811d..ccc8c030be9183077cc8111c17f595f4ab4542d0 100644 (file)
@@ -393,3 +393,8 @@ pub struct Context {
     pub ancestors: Vec<Status>,
     pub descendants: Vec<Status>,
 }
+
+#[derive(Deserialize, Debug, Clone)]
+pub struct ServerError {
+    pub error: String,
+}