From dd1f2295541a14cd1d32139abf5e0bd83d886585 Mon Sep 17 00:00:00 2001
From: gluax <16431709+gluax@users.noreply.github.com>
Date: Thu, 4 Apr 2024 15:57:04 -0700
Subject: [PATCH] refactor(snops): flattten error object

---
 crates/snops-common/src/rpc/error.rs | 48 +++++++++---------
 crates/snops/src/cannon/error.rs     | 66 ++++++++++++-------------
 crates/snops/src/env/error.rs        | 73 ++++++++++++++++------------
 crates/snops/src/error.rs            | 37 ++++++--------
 crates/snops/src/schema/error.rs     | 44 +++++++++--------
 crates/snops/src/schema/storage.rs   |  4 ++
 crates/snops/src/server/error.rs     | 28 +++++------
 7 files changed, 151 insertions(+), 149 deletions(-)

diff --git a/crates/snops-common/src/rpc/error.rs b/crates/snops-common/src/rpc/error.rs
index 23b41010..ee032c8e 100644
--- a/crates/snops-common/src/rpc/error.rs
+++ b/crates/snops-common/src/rpc/error.rs
@@ -3,14 +3,29 @@ use strum_macros::AsRefStr;
 use thiserror::Error;
 
 #[macro_export]
-macro_rules! impl_serialize_pretty_error {
+macro_rules! impl_into_type_str {
     ($name:path) => {
-        impl Serialize for $name {
-            fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
-            where
-                S: Serializer,
-            {
-                PrettyError::from(self).serialize(serializer)
+        impl From<&$name> for String {
+            fn from(e: &$name) -> Self {
+                e.as_ref().to_string()
+            }
+        }
+    };
+
+    ($name:path, |_| $body:expr) => {
+        impl From<&$name> for String {
+            fn from(_: &$name) -> Self {
+                $body
+            }
+        }
+    };
+
+    ($name:path, |$from_var:ident| $body:expr) => {
+        impl From<&$name> for String {
+            fn from($from_var: &$name) -> Self {
+                use $name::*;
+
+                $body
             }
         }
     };
@@ -45,25 +60,6 @@ macro_rules! impl_into_status_code {
     };
 }
 
-#[derive(Debug, Serialize)]
-pub struct PrettyError {
-    #[serde(rename = "type")]
-    pub ty: String,
-    pub error: String,
-}
-
-impl<E> From<&E> for PrettyError
-where
-    E: std::error::Error + AsRef<str>,
-{
-    fn from(error: &E) -> Self {
-        Self {
-            ty: error.as_ref().to_string(),
-            error: error.to_string(),
-        }
-    }
-}
-
 #[derive(Debug, Error, Serialize, Deserialize, AsRefStr)]
 pub enum AgentError {
     #[error("invalid agent state")]
diff --git a/crates/snops/src/cannon/error.rs b/crates/snops/src/cannon/error.rs
index 5ee5759c..893f81e7 100644
--- a/crates/snops/src/cannon/error.rs
+++ b/crates/snops/src/cannon/error.rs
@@ -2,9 +2,7 @@ use std::path::PathBuf;
 
 use axum::http::StatusCode;
 use serde::{ser::SerializeStruct, Serialize, Serializer};
-use snops_common::{
-    impl_into_status_code, impl_serialize_pretty_error, rpc::error::PrettyError, state::NodeKey,
-};
+use snops_common::{impl_into_status_code, impl_into_type_str, state::NodeKey};
 use strum_macros::AsRefStr;
 use thiserror::Error;
 
@@ -14,13 +12,13 @@ use crate::error::{CommandError, StateError};
 #[derive(Debug, Error, AsRefStr)]
 pub enum AuthorizeError {
     /// For when a bad AOT command is run
-    #[error("command error: {0}")]
+    #[error(transparent)]
     Command(#[from] CommandError),
     /// For if invalid JSON is returned from the AOT command
     #[error("expected function, fee, and broadcast fields in response")]
     InvalidJson,
     /// For if invalid JSON is returned from the AOT command
-    #[error("parse json error: {0}")]
+    #[error("{0}")]
     Json(#[source] serde_json::Error),
     /// For if invalid JSON is returned from the AOT command
     #[error("expected JSON object in response")]
@@ -32,19 +30,19 @@ impl_into_status_code!(AuthorizeError, |value| match value {
     _ => StatusCode::INTERNAL_SERVER_ERROR,
 });
 
+impl_into_type_str!(AuthorizeError, |value| match value {
+    Command(e) => format!("{}.{}", value.as_ref(), e.as_ref()),
+    _ => value.as_ref().to_string(),
+});
+
 impl Serialize for AuthorizeError {
     fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
     where
         S: Serializer,
     {
         let mut state = serializer.serialize_struct("Error", 2)?;
-        state.serialize_field("type", self.as_ref())?;
-
-        match self {
-            Self::Command(e) => state.serialize_field("error", e),
-            _ => state.serialize_field("error", &self.to_string()),
-        }?;
-
+        state.serialize_field("type", &String::from(self))?;
+        state.serialize_field("error", &self.to_string())?;
         state.end()
     }
 }
@@ -63,7 +61,6 @@ pub enum TransactionDrainError {
 }
 
 impl_into_status_code!(TransactionDrainError);
-impl_serialize_pretty_error!(TransactionDrainError);
 
 #[derive(Debug, Error, AsRefStr)]
 pub enum TransactionSinkError {
@@ -79,7 +76,6 @@ pub enum TransactionSinkError {
 }
 
 impl_into_status_code!(TransactionSinkError);
-impl_serialize_pretty_error!(TransactionSinkError);
 
 #[derive(Debug, Error, AsRefStr)]
 pub enum SourceError {
@@ -102,7 +98,6 @@ pub enum SourceError {
 }
 
 impl_into_status_code!(SourceError);
-impl_serialize_pretty_error!(SourceError);
 
 #[derive(Debug, Error, AsRefStr)]
 pub enum CannonInstanceError {
@@ -175,19 +170,19 @@ impl Serialize for ExecutionContextError {
 
 #[derive(Debug, Error, AsRefStr)]
 pub enum CannonError {
-    #[error("authorize error: {0}")]
+    #[error(transparent)]
     Authorize(#[from] AuthorizeError),
-    #[error("cannon instance error: {0}")]
+    #[error(transparent)]
     CannonInstance(#[from] CannonInstanceError),
-    #[error("command error cannon `{0}`: {1}")]
+    #[error("cannon `{0}`: {1}")]
     Command(usize, #[source] CommandError),
-    #[error("exec ctx error: {0}")]
+    #[error(transparent)]
     ExecutionContext(#[from] ExecutionContextError),
     #[error("target agent offline for {0} `{1}`: {2}")]
     TargetAgentOffline(&'static str, usize, String),
-    #[error("tx drain error: {0}")]
+    #[error(transparent)]
     TransactionDrain(#[from] TransactionDrainError),
-    #[error("tx sink error: {0}")]
+    #[error(transparent)]
     TransactionSink(#[from] TransactionSinkError),
     #[error("send `auth` error for cannon `{0}`: {1}")]
     SendAuthError(
@@ -196,9 +191,9 @@ pub enum CannonError {
     ),
     #[error("send `tx` error for cannon `{0}`: {1}")]
     SendTxError(usize, #[source] tokio::sync::mpsc::error::SendError<String>),
-    #[error("source error: {0}")]
+    #[error(transparent)]
     Source(#[from] SourceError),
-    #[error("state error: {0}")]
+    #[error(transparent)]
     State(#[from] StateError),
 }
 
@@ -215,23 +210,26 @@ impl_into_status_code!(CannonError, |value| match value {
     _ => StatusCode::INTERNAL_SERVER_ERROR,
 });
 
+impl_into_type_str!(CannonError, |value| match value {
+    Authorize(e) => format!("{}.{}", value.as_ref(), String::from(e)),
+    CannonInstance(e) => format!("{}.{}", value.as_ref(), e.as_ref()),
+    Command(_, e) => format!("{}.{}", value.as_ref(), e.as_ref()),
+    ExecutionContext(e) => format!("{}.{}", value.as_ref(), e.as_ref()),
+    TransactionDrain(e) => format!("{}.{}", value.as_ref(), e.as_ref()),
+    TransactionSink(e) => format!("{}.{}", value.as_ref(), e.as_ref()),
+    Source(e) => format!("{}.{}", value.as_ref(), e.as_ref()),
+    State(e) => format!("{}.{}", value.as_ref(), String::from(e)),
+    _ => value.as_ref().to_string(),
+});
+
 impl Serialize for CannonError {
     fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
     where
         S: Serializer,
     {
         let mut state = serializer.serialize_struct("Error", 2)?;
-        state.serialize_field("type", self.as_ref())?;
-
-        match self {
-            Self::Authorize(e) => state.serialize_field("error", e),
-            Self::CannonInstance(e) => state.serialize_field("error", e),
-            Self::Command(_, e) => state.serialize_field("error", e),
-            Self::ExecutionContext(e) => state.serialize_field("error", e),
-            Self::TransactionDrain(e) => state.serialize_field("error", e),
-            Self::TransactionSink(e) => state.serialize_field("error", e),
-            _ => state.serialize_field("error", &self.to_string()),
-        }?;
+        state.serialize_field("type", &String::from(self))?;
+        state.serialize_field("error", &self.to_string())?;
 
         state.end()
     }
diff --git a/crates/snops/src/env/error.rs b/crates/snops/src/env/error.rs
index a26239c5..94b3f872 100644
--- a/crates/snops/src/env/error.rs
+++ b/crates/snops/src/env/error.rs
@@ -1,8 +1,7 @@
 use axum::http::StatusCode;
 use serde::{ser::SerializeStruct, Serialize, Serializer};
 use snops_common::{
-    impl_into_status_code, impl_serialize_pretty_error,
-    rpc::error::PrettyError,
+    impl_into_status_code, impl_into_type_str,
     state::{AgentId, NodeKey},
 };
 use strum_macros::AsRefStr;
@@ -25,11 +24,11 @@ pub enum ExecutionError {
     AgentOffline,
     #[error("env `{0}` not found")]
     EnvNotFound(usize),
-    #[error("cannon error: `{0}`")]
+    #[error(transparent)]
     Cannon(#[from] CannonError),
-    #[error("join error: `{0}`")]
+    #[error("{0}")]
     Join(#[from] JoinError),
-    #[error("reconcile error: `{0}`")]
+    #[error(transparent)]
     Reconcile(#[from] BatchReconcileError),
     #[error("env timeline is already being executed")]
     TimelineAlreadyStarted,
@@ -43,16 +42,21 @@ impl_into_status_code!(ExecutionError, |value| match value {
     _ => StatusCode::INTERNAL_SERVER_ERROR,
 });
 
+impl_into_type_str!(ExecutionError, |value| match value {
+    Cannon(e) => format!("{}.{}", value.as_ref(), &String::from(e)),
+    _ => value.as_ref().to_string(),
+});
+
 impl Serialize for ExecutionError {
     fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
     where
         S: Serializer,
     {
         let mut state = serializer.serialize_struct("Error", 2)?;
-        state.serialize_field("type", self.as_ref())?;
+        state.serialize_field("type", &String::from(self))?;
 
         match self {
-            Self::Cannon(e) => state.serialize_field("error", e),
+            Self::Cannon(e) => state.serialize_field("error", &e.to_string()),
             _ => state.serialize_field("error", &self.to_string()),
         }?;
 
@@ -83,8 +87,6 @@ impl_into_status_code!(DelegationError, |value| match value {
     }
 });
 
-impl_serialize_pretty_error!(DelegationError);
-
 #[derive(Debug, Error, AsRefStr)]
 pub enum PrepareError {
     #[error("duplicate node key: {0}")]
@@ -95,7 +97,7 @@ pub enum PrepareError {
     MissingStorage,
     #[error("cannot have a node with zero replicas")]
     NodeHas0Replicas,
-    #[error("reconcile error: `{0}`")]
+    #[error(transparent)]
     Reconcile(#[from] ReconcileError),
 }
 
@@ -105,16 +107,21 @@ impl_into_status_code!(PrepareError, |value| match value {
     Reconcile(e) => e.into(),
 });
 
+impl_into_type_str!(PrepareError, |value| match value {
+    Reconcile(e) => format!("{}.{}", value.as_ref(), e.as_ref()),
+    _ => value.as_ref().to_string(),
+});
+
 impl Serialize for PrepareError {
     fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
     where
         S: Serializer,
     {
         let mut state = serializer.serialize_struct("Error", 2)?;
-        state.serialize_field("type", self.as_ref())?;
+        state.serialize_field("type", &String::from(self))?;
 
         match self {
-            Self::Reconcile(e) => state.serialize_field("error", e),
+            Self::Reconcile(e) => state.serialize_field("error", &e.to_string()),
             _ => state.serialize_field("error", &self.to_string()),
         }?;
 
@@ -129,7 +136,6 @@ pub enum CleanupError {
 }
 
 impl_into_status_code!(CleanupError, |_| StatusCode::NOT_FOUND);
-impl_serialize_pretty_error!(CleanupError);
 
 #[derive(Debug, Error, AsRefStr)]
 pub enum ReconcileError {
@@ -146,23 +152,21 @@ impl_into_status_code!(ReconcileError, |value| match value {
     EnvNotFound(_) | ExpectedInternalAgentPeer { .. } => StatusCode::NOT_FOUND,
 });
 
-impl_serialize_pretty_error!(ReconcileError);
-
 #[derive(Debug, Error, AsRefStr)]
 pub enum EnvError {
-    #[error("cannon error: `{0}`")]
+    #[error(transparent)]
     Cannon(#[from] CannonError),
-    #[error("cleanup error: `{0}`")]
+    #[error(transparent)]
     Cleanup(#[from] CleanupError),
     #[error("delegation errors occured:{}", .0.iter().map(ToString::to_string).collect::<Vec<_>>().join("\n"))]
     Delegation(Vec<DelegationError>),
-    #[error("exec error: `{0}`")]
+    #[error(transparent)]
     Execution(#[from] ExecutionError),
-    #[error("prepare error: `{0}`")]
+    #[error(transparent)]
     Prepare(#[from] PrepareError),
-    #[error("reconcile error: `{0}`")]
+    #[error(transparent)]
     Reconcile(#[from] ReconcileError),
-    #[error("schema error: `{0}`")]
+    #[error(transparent)]
     Schema(#[from] SchemaError),
 }
 
@@ -176,23 +180,28 @@ impl_into_status_code!(EnvError, |value| match value {
     Schema(e) => e.into(),
 });
 
+impl_into_type_str!(EnvError, |value| match value {
+    Cannon(e) => format!("{}.{}", value.as_ref(), String::from(e)),
+    Cleanup(e) => format!("{}.{}", value.as_ref(), e.as_ref()),
+    Delegation(e) => format!(
+        "{}.{}",
+        value.as_ref(),
+        e.iter().map(|x| x.as_ref()).collect::<Vec<_>>().join(",")
+    ),
+    Execution(e) => format!("{}.{}", value.as_ref(), String::from(e)),
+    Prepare(e) => format!("{}.{}", value.as_ref(), String::from(e)),
+    Reconcile(e) => format!("{}.{}", value.as_ref(), e.as_ref()),
+    Schema(e) => format!("{}.{}", value.as_ref(), String::from(e)),
+});
+
 impl Serialize for EnvError {
     fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
     where
         S: Serializer,
     {
         let mut state = serializer.serialize_struct("Error", 2)?;
-        state.serialize_field("type", self.as_ref())?;
-
-        match self {
-            Self::Cannon(e) => state.serialize_field("error", e),
-            Self::Cleanup(e) => state.serialize_field("error", e),
-            Self::Delegation(e) => state.serialize_field("error", e),
-            Self::Execution(e) => state.serialize_field("error", e),
-            Self::Prepare(e) => state.serialize_field("error", e),
-            Self::Reconcile(e) => state.serialize_field("error", e),
-            Self::Schema(e) => state.serialize_field("error", e),
-        }?;
+        state.serialize_field("type", &String::from(self))?;
+        state.serialize_field("error", &self.to_string())?;
 
         state.end()
     }
diff --git a/crates/snops/src/error.rs b/crates/snops/src/error.rs
index 10e28566..3bfc618c 100644
--- a/crates/snops/src/error.rs
+++ b/crates/snops/src/error.rs
@@ -1,22 +1,21 @@
 use std::process::ExitStatus;
 
 use serde::{ser::SerializeStruct, Serialize, Serializer};
-use snops_common::rpc::error::PrettyError;
 use snops_common::state::AgentId;
-use snops_common::{impl_into_status_code, impl_serialize_pretty_error};
+use snops_common::{impl_into_status_code, impl_into_type_str};
 use strum_macros::AsRefStr;
 use thiserror::Error;
 
 #[derive(Debug, Error, AsRefStr)]
 pub enum CommandError {
-    #[error("error {action} command `{cmd}`: {error}")]
+    #[error("{action} command `{cmd}`: {error}")]
     Action {
         action: &'static str,
         cmd: &'static str,
         #[source]
         error: std::io::Error,
     },
-    #[error("error command `{cmd}` failed with `{status}`: {stderr}")]
+    #[error("command `{cmd}` failed with `{status}`: {stderr}")]
     Status {
         cmd: &'static str,
         status: ExitStatus,
@@ -25,7 +24,6 @@ pub enum CommandError {
 }
 
 impl_into_status_code!(CommandError);
-impl_serialize_pretty_error!(CommandError);
 
 impl CommandError {
     pub fn action(action: &'static str, cmd: &'static str, error: std::io::Error) -> Self {
@@ -42,7 +40,7 @@ impl CommandError {
 }
 
 #[derive(Debug, Error)]
-#[error("deserialize error: `{i}`: `{e}`")]
+#[error("`{i}`: `{e}`")]
 pub struct DeserializeError {
     pub i: usize,
     #[source]
@@ -53,13 +51,13 @@ impl_into_status_code!(DeserializeError);
 
 #[derive(Debug, Error, AsRefStr)]
 pub enum StateError {
-    #[error("common agent error: {0}")]
+    #[error(transparent)]
     Agent(#[from] snops_common::prelude::error::AgentError),
     #[error("source agent has no addr id: `{0}`")]
     NoAddress(AgentId),
-    #[error("common reconcile error: {0}")]
+    #[error(transparent)]
     Reconcile(#[from] snops_common::prelude::error::ReconcileError),
-    #[error("rpc error: {0}")]
+    #[error("{0}")]
     Rpc(#[from] tarpc::client::RpcError),
     #[error("source agent not found id: `{0}`")]
     SourceAgentNotFound(AgentId),
@@ -67,25 +65,20 @@ pub enum StateError {
 
 impl_into_status_code!(StateError);
 
+impl_into_type_str!(StateError, |value| match value {
+    Agent(e) => format!("{}.{}", value.as_ref(), e.as_ref()),
+    Reconcile(e) => format!("{}.{}", value.as_ref(), e.as_ref()),
+    _ => value.as_ref().to_string(),
+});
+
 impl Serialize for StateError {
     fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
     where
         S: Serializer,
     {
         let mut state = serializer.serialize_struct("Error", 2)?;
-        state.serialize_field("type", self.as_ref())?;
-
-        match self {
-            Self::Agent(e) => {
-                let pe = PrettyError::from(e);
-                state.serialize_field("error", &pe)
-            }
-            Self::Reconcile(e) => {
-                let pe = PrettyError::from(e);
-                state.serialize_field("error", &pe)
-            }
-            _ => state.serialize_field("error", &self.to_string()),
-        }?;
+        state.serialize_field("type", &String::from(self))?;
+        state.serialize_field("error", &self.to_string())?;
 
         state.end()
     }
diff --git a/crates/snops/src/schema/error.rs b/crates/snops/src/schema/error.rs
index eb365e48..36307d0b 100644
--- a/crates/snops/src/schema/error.rs
+++ b/crates/snops/src/schema/error.rs
@@ -2,7 +2,7 @@ use std::path::PathBuf;
 
 use axum::http::StatusCode;
 use serde::{ser::SerializeStruct, Serialize, Serializer};
-use snops_common::{impl_into_status_code, impl_serialize_pretty_error, rpc::error::PrettyError};
+use snops_common::{impl_into_status_code, impl_into_type_str};
 use strum_macros::AsRefStr;
 use thiserror::Error;
 use url::Url;
@@ -11,23 +11,23 @@ use crate::error::CommandError;
 
 #[derive(Debug, Error, AsRefStr)]
 pub enum StorageError {
-    #[error("command error id: `{0}`: {1}")]
+    #[error("storage id: `{1}`: {0}")]
     Command(CommandError, String),
-    #[error("error mkdirs for storage generation id: `{0}`: {1}")]
+    #[error("mkdirs for storage generation id: `{0}`: {1}")]
     GenerateStorage(String, #[source] std::io::Error),
-    #[error("error generating genesis id: `{0}`: {1}")]
+    #[error("generating genesis id: `{0}`: {1}")]
     FailedToGenGenesis(String, #[source] std::io::Error),
-    #[error("error fetching genesis block id: `{0}` url: `{1}`: {2}")]
+    #[error("fetching genesis block id: `{0}` url: `{1}`: {2}")]
     FailedToFetchGenesis(String, Url, #[source] reqwest::Error),
-    #[error("error writing genesis block id: `{0}`: {1}")]
+    #[error("writing genesis block id: `{0}`: {1}")]
     FailedToWriteGenesis(String, #[source] std::io::Error),
-    #[error("error taring ledger id: `{0}`: {1}")]
+    #[error("taring ledger id: `{0}`: {1}")]
     FailedToTarLedger(String, #[source] std::io::Error),
     #[error("the specified storage ID {0} doesn't exist, and no generation params were specified")]
     NoGenerationParams(String),
-    #[error("error reading balances {0:#?}: {1}")]
+    #[error("reading balances {0:#?}: {1}")]
     ReadBalances(PathBuf, #[source] std::io::Error),
-    #[error("error parsing balances {0:#?}: {1}")]
+    #[error("parsing balances {0:#?}: {1}")]
     ParseBalances(PathBuf, #[source] serde_json::Error),
 }
 
@@ -38,7 +38,10 @@ impl_into_status_code!(StorageError, |value| match value {
     _ => StatusCode::INTERNAL_SERVER_ERROR,
 });
 
-impl_serialize_pretty_error!(StorageError);
+impl_into_type_str!(StorageError, |value| match value {
+    Command(e, _) => format!("{}.{}", value.as_ref(), e.as_ref()),
+    _ => value.as_ref().to_string(),
+});
 
 #[derive(Debug, Error)]
 #[error("invalid node target string")]
@@ -59,15 +62,13 @@ impl_into_status_code!(KeySourceError, |value| match value {
     InvalidCommitteeIndex(_) => StatusCode::BAD_REQUEST,
 });
 
-impl_serialize_pretty_error!(KeySourceError);
-
 #[derive(Debug, Error, AsRefStr)]
 pub enum SchemaError {
     #[error("key source error: {0}")]
     KeySource(#[from] KeySourceError),
-    #[error("node target error: {0}")]
+    #[error(transparent)]
     NodeTarget(#[from] NodeTargetError),
-    #[error("storage error: {0}")]
+    #[error(transparent)]
     Storage(#[from] StorageError),
 }
 
@@ -77,19 +78,20 @@ impl_into_status_code!(SchemaError, |value| match value {
     Storage(e) => e.into(),
 });
 
+impl_into_type_str!(SchemaError, |value| match value {
+    KeySource(e) => format!("{}.{}", value.as_ref(), e.as_ref()),
+    Storage(e) => format!("{}.{}", value.as_ref(), String::from(e)),
+    _ => value.as_ref().to_string(),
+});
+
 impl Serialize for SchemaError {
     fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
     where
         S: Serializer,
     {
         let mut state = serializer.serialize_struct("Error", 2)?;
-        state.serialize_field("type", self.as_ref())?;
-
-        match self {
-            Self::KeySource(e) => state.serialize_field("error", e),
-            Self::NodeTarget(e) => state.serialize_field("error", &e.to_string()),
-            Self::Storage(e) => state.serialize_field("error", e),
-        }?;
+        state.serialize_field("type", &String::from(self))?;
+        state.serialize_field("error", &self.to_string())?;
 
         state.end()
     }
diff --git a/crates/snops/src/schema/storage.rs b/crates/snops/src/schema/storage.rs
index 4dcb7350..4631a820 100644
--- a/crates/snops/src/schema/storage.rs
+++ b/crates/snops/src/schema/storage.rs
@@ -197,6 +197,8 @@ impl Document {
         let mut base = state.cli.path.join("storage");
         base.push(&id);
 
+        // TODO: The dir can be made by a previous run and the aot stuff can fail
+        // i.e an empty/incomplete directory can exist and we should check those
         let exists = matches!(tokio::fs::try_exists(&base).await, Ok(true));
 
         // TODO: respect self.prefer_existing
@@ -400,6 +402,8 @@ fn pick_commitee_addr(entry: (String, u64)) -> String {
     entry.0
 }
 
+// TODO: function should also take storage id
+// in case of error, the storage id can be used to provide more context
 async fn read_to_addrs<T: DeserializeOwned>(
     f: impl Fn(T) -> String,
     file: PathBuf,
diff --git a/crates/snops/src/server/error.rs b/crates/snops/src/server/error.rs
index 389843d9..159cbd32 100644
--- a/crates/snops/src/server/error.rs
+++ b/crates/snops/src/server/error.rs
@@ -2,7 +2,7 @@ use axum::{response::IntoResponse, Json};
 use http::StatusCode;
 use serde::{ser::SerializeStruct, Serialize, Serializer};
 use serde_json::json;
-use snops_common::{impl_into_status_code, state::AgentId};
+use snops_common::{impl_into_status_code, impl_into_type_str, state::AgentId};
 use thiserror::Error;
 
 use crate::{
@@ -14,13 +14,13 @@ use crate::{
 pub enum ServerError {
     #[error("agent `{0}` not found")]
     AgentNotFound(AgentId),
-    #[error("cannon error: {0}")]
+    #[error(transparent)]
     Cannon(#[from] CannonError),
-    #[error("deserialize error: {0}")]
+    #[error(transparent)]
     Deserialize(#[from] DeserializeError),
-    #[error("env error: {0}")]
+    #[error(transparent)]
     Env(#[from] EnvError),
-    #[error("schema error: {0}")]
+    #[error(transparent)]
     Schema(#[from] SchemaError),
 }
 
@@ -32,21 +32,21 @@ impl_into_status_code!(ServerError, |value| match value {
     Schema(e) => e.into(),
 });
 
+impl_into_type_str!(ServerError, |value| match value {
+    Cannon(e) => format!("{}.{}", value.as_ref(), String::from(e)),
+    Env(e) => format!("{}.{}", value.as_ref(), String::from(e)),
+    Schema(e) => format!("{}.{}", value.as_ref(), String::from(e)),
+    _ => value.as_ref().to_string(),
+});
+
 impl Serialize for ServerError {
     fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
     where
         S: Serializer,
     {
         let mut state = serializer.serialize_struct("Error", 2)?;
-        state.serialize_field("type", self.as_ref())?;
-
-        match self {
-            Self::Cannon(e) => state.serialize_field("error", e),
-            Self::Deserialize(e) => state.serialize_field("error", &e.to_string()),
-            Self::Env(e) => state.serialize_field("error", e),
-            Self::Schema(e) => state.serialize_field("error", e),
-            _ => state.serialize_field("error", &self.to_string()),
-        }?;
+        state.serialize_field("type", &String::from(self))?;
+        state.serialize_field("error", &self.to_string())?;
 
         state.end()
     }