feat: emit stable json errors
This commit is contained in:
parent
1f22139070
commit
675e3fb45d
4 changed files with 108 additions and 4 deletions
|
|
@ -1227,6 +1227,18 @@ pub struct EmptyArgs {}
|
|||
|
||||
pub async fn run() -> Result<()> {
|
||||
let cli = Cli::parse();
|
||||
let json = cli.json || cli.jsonl;
|
||||
match run_inner(cli).await {
|
||||
Ok(()) => Ok(()),
|
||||
Err(error) if json => {
|
||||
print_json_error(&error)?;
|
||||
std::process::exit(1);
|
||||
}
|
||||
Err(error) => Err(error),
|
||||
}
|
||||
}
|
||||
|
||||
async fn run_inner(cli: Cli) -> Result<()> {
|
||||
if let Command::Completions { shell } = &cli.command {
|
||||
print_completions(*shell);
|
||||
return Ok(());
|
||||
|
|
@ -1328,6 +1340,47 @@ pub async fn run() -> Result<()> {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
fn print_json_error(error: &anyhow::Error) -> Result<()> {
|
||||
let message = error.to_string();
|
||||
let detail = format!("{error:#}");
|
||||
let hint = json_error_hint(&detail);
|
||||
println!(
|
||||
"{}",
|
||||
serde_json::to_string_pretty(&serde_json::json!({
|
||||
"type": "error",
|
||||
"code": json_error_code(&detail),
|
||||
"message": message,
|
||||
"detail": detail,
|
||||
"hint": hint,
|
||||
}))?
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn json_error_code(detail: &str) -> &'static str {
|
||||
let lower = detail.to_ascii_lowercase();
|
||||
if lower.contains("connect to daemon") || lower.contains("connection refused") {
|
||||
"daemon_unavailable"
|
||||
} else if lower.contains("unauthorized") || lower.contains("missing grant") {
|
||||
"unauthorized"
|
||||
} else if lower.contains("peer candidate not found") {
|
||||
"peer_not_found"
|
||||
} else if lower.contains("resource not found") {
|
||||
"resource_not_found"
|
||||
} else if lower.contains("invalid") {
|
||||
"invalid_input"
|
||||
} else {
|
||||
"command_failed"
|
||||
}
|
||||
}
|
||||
|
||||
fn json_error_hint(detail: &str) -> Option<String> {
|
||||
detail
|
||||
.lines()
|
||||
.find_map(|line| line.strip_prefix("next: "))
|
||||
.map(ToOwned::to_owned)
|
||||
}
|
||||
|
||||
fn print_guide(topic: Option<GuideTopic>, json: bool) -> Result<()> {
|
||||
let (name, body) = match topic {
|
||||
None => ("index", GUIDE_INDEX),
|
||||
|
|
@ -3931,3 +3984,30 @@ fn shell_quote_command(command: &[String]) -> String {
|
|||
.collect::<Vec<_>>()
|
||||
.join(" ")
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn json_error_classification_is_stable_for_common_failures() {
|
||||
assert_eq!(
|
||||
json_error_code("connect to daemon at /tmp/geth.sock: No such file or directory"),
|
||||
"daemon_unavailable"
|
||||
);
|
||||
assert_eq!(
|
||||
json_error_code("unauthorized: missing grant"),
|
||||
"unauthorized"
|
||||
);
|
||||
assert_eq!(
|
||||
json_error_code("peer candidate not found: node:missing"),
|
||||
"peer_not_found"
|
||||
);
|
||||
assert_eq!(
|
||||
json_error_hint(
|
||||
"peer candidate not found: node:missing\nnext: import a peer card with `geth peer import <path>`",
|
||||
),
|
||||
Some("import a peer card with `geth peer import <path>`".to_owned())
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue