diff --git a/src/github/api.rs b/src/github/api.rs index f1159b5..94f8a8b 100644 --- a/src/github/api.rs +++ b/src/github/api.rs @@ -1,3 +1,4 @@ +use crate::utils::ResponseExt; use anyhow::{bail, Context}; use hyper_old_types::header::{Link, RelationType}; use log::{debug, trace}; @@ -138,7 +139,7 @@ impl GitHub { }; Ok(self .send(Method::POST, &format!("orgs/{org}/teams"), body)? - .json()?) + .json_annotated()?) } } @@ -366,7 +367,7 @@ impl GitHub { } else { Ok(self .send(Method::POST, &format!("orgs/{org}/repos"), req)? - .json()?) + .json_annotated()?) } } @@ -761,7 +762,7 @@ impl GitHub { ) -> Result, anyhow::Error> { let resp = self.req(method.clone(), url)?.send()?; match resp.status() { - StatusCode::OK => Ok(Some(resp.json().with_context(|| { + StatusCode::OK => Ok(Some(resp.json_annotated().with_context(|| { format!("Failed to decode response body on {method} request to '{url}'") })?)), StatusCode::NOT_FOUND => Ok(None), @@ -785,7 +786,7 @@ impl GitHub { .send()? .custom_error_for_status()?; - let res: GraphResult = resp.json().with_context(|| { + let res: GraphResult = resp.json_annotated().with_context(|| { format!("Failed to decode response body on graphql request with query '{query}'") })?; if let Some(error) = res.errors.get(0) { @@ -1059,19 +1060,3 @@ pub(crate) enum BranchProtectionOp { CreateForRepo(String), UpdateBranchProtection(String), } - -trait ResponseExt { - fn custom_error_for_status(self) -> anyhow::Result; -} - -impl ResponseExt for Response { - fn custom_error_for_status(self) -> anyhow::Result { - match self.error_for_status_ref() { - Ok(_) => Ok(self), - Err(err) => { - let body = self.text()?; - Err(err).context(format!("Body: {:?}", body)) - } - } - } -} diff --git a/src/main.rs b/src/main.rs index 110739e..93a8d0e 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,6 +1,7 @@ mod github; mod mailgun; mod team_api; +mod utils; mod zulip; use crate::github::SyncGitHub; diff --git a/src/team_api.rs b/src/team_api.rs index 71ceab3..d984170 100644 --- a/src/team_api.rs +++ b/src/team_api.rs @@ -1,3 +1,4 @@ +use crate::utils::ResponseExt; use log::{debug, info, trace}; use std::borrow::Cow; use std::path::PathBuf; @@ -47,7 +48,9 @@ impl TeamApi { .unwrap_or_else(|_| Cow::Borrowed(rust_team_data::v1::BASE_URL)); let url = format!("{base}/{url}"); trace!("http request: GET {}", url); - Ok(reqwest::blocking::get(&url)?.error_for_status()?.json()?) + Ok(reqwest::blocking::get(&url)? + .error_for_status()? + .json_annotated()?) } TeamApi::Local(ref path) => { let dest = tempfile::tempdir()?; diff --git a/src/utils.rs b/src/utils.rs new file mode 100644 index 0000000..71708d9 --- /dev/null +++ b/src/utils.rs @@ -0,0 +1,41 @@ +use anyhow::Context; +use reqwest::blocking::Response; +use serde::de::DeserializeOwned; +use std::str::FromStr; + +pub trait ResponseExt { + fn custom_error_for_status(self) -> anyhow::Result; + fn json_annotated(self) -> anyhow::Result; +} + +impl ResponseExt for Response { + fn custom_error_for_status(self) -> anyhow::Result { + match self.error_for_status_ref() { + Ok(_) => Ok(self), + Err(err) => { + let body = self.text()?; + Err(err).context(format!("Body: {:?}", body)) + } + } + } + + /// Try to load the response as JSON. If it fails, include the response body + /// as text in the error message, so that it is easier to understand what was + /// the problem. + fn json_annotated(self) -> anyhow::Result { + let text = self.text()?; + + serde_json::from_str::(&text).with_context(|| { + // Try to at least deserialize as generic JSON, to provide a more readable + // visualization of the response body. + let body_content = serde_json::Value::from_str(&text) + .and_then(|v| serde_json::to_string_pretty(&v)) + .unwrap_or(text); + + format!( + "Cannot deserialize type `{}` from the following response body:\n{body_content}", + std::any::type_name::(), + ) + }) + } +}