Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion crates/ov_cli/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,14 @@ impl Error {
} else if error.is_builder() {
Self::Client(format!("{context}: {error}"))
} else {
Self::Network(format!("{context}: {error}"))
let mut message = format!("{context}: {error}");
let mut source = std::error::Error::source(&error);
while let Some(cause) = source {
message.push_str(": ");
message.push_str(&cause.to_string());
source = cause.source();
}
Self::Network(message)
}
}

Expand Down
152 changes: 103 additions & 49 deletions crates/ov_cli/src/error_ui.rs
Original file line number Diff line number Diff line change
Expand Up @@ -244,15 +244,22 @@ pub(crate) fn report_for_runtime_error(command: impl Into<String>, error: &Error
"ov config",
copy(language, "Create a config", "创建配置"),
)]),
Error::Config(message) => ErrorReport::new(copy(language, "Configuration Error", "配置错误"), message)
.with_command(command)
.with_actions(config_error_actions(message, language)),
Error::Language(message) => ErrorReport::new(copy(language, "Language Error", "语言错误"), message)
.with_command(command)
.with_actions(vec![
ErrorAction::new("ov language en", copy(language, "Use English", "使用英文")),
ErrorAction::new("ov language zh-CN", copy(language, "Use Simplified Chinese", "使用简体中文")),
]),
Error::Config(message) => {
ErrorReport::new(copy(language, "Configuration Error", "配置错误"), message)
.with_command(command)
.with_actions(config_error_actions(message, language))
}
Error::Language(message) => {
ErrorReport::new(copy(language, "Language Error", "语言错误"), message)
.with_command(command)
.with_actions(vec![
ErrorAction::new("ov language en", copy(language, "Use English", "使用英文")),
ErrorAction::new(
"ov language zh-CN",
copy(language, "Use Simplified Chinese", "使用简体中文"),
),
])
}
Error::Timeout(message) => ErrorReport::new(
copy(language, "Request Timeout", "请求超时"),
copy(
Expand All @@ -264,24 +271,37 @@ pub(crate) fn report_for_runtime_error(command: impl Into<String>, error: &Error
.with_command(command)
.with_detail(message)
.with_actions(vec![
ErrorAction::new("ov config", copy(language, "Increase the request timeout", "提高请求超时时间")),
ErrorAction::new("ov config show", copy(language, "Show the active config", "查看当前配置")),
]),
Error::Network(message) => ErrorReport::new(
copy(language, "Connection Error", "连接错误"),
copy(
language,
"Could not reach OpenViking. The server may be offline, or this config points to the wrong URL.",
"无法连接 OpenViking。服务器可能未启动,或当前配置指向了错误的 URL。",
ErrorAction::new(
"ov config",
copy(language, "Increase the request timeout", "提高请求超时时间"),
),
ErrorAction::new(
"ov config show",
copy(language, "Show the active config", "查看当前配置"),
),
)
.with_command(command)
.with_detail(message)
.with_actions(vec![
ErrorAction::new("ov config validate", copy(language, "Check the active config", "检查当前配置")),
ErrorAction::new("ov health", copy(language, "Run a quick server health check", "快速检查服务器健康状态")),
ErrorAction::new("ov config switch", copy(language, "Switch to another config", "切换到其他配置")),
]),
Error::Network(message) => {
ErrorReport::new(copy(language, "Connection Error", "连接错误"), message)
.with_command(command)
.with_actions(vec![
ErrorAction::new(
"ov config validate",
copy(language, "Check the active config", "检查当前配置"),
),
ErrorAction::new(
"ov health",
copy(
language,
"Run a quick server health check",
"快速检查服务器健康状态",
),
),
ErrorAction::new(
"ov config switch",
copy(language, "Switch to another config", "切换到其他配置"),
),
])
}
Error::Api {
message, details, ..
} if error.code() == "REFRESH_FAILED" => {
Expand Down Expand Up @@ -320,10 +340,18 @@ pub(crate) fn report_for_runtime_error(command: impl Into<String>, error: &Error
)
.with_command(command)
.with_actions(vec![
ErrorAction::new("ov config", copy(language, "Edit this config", "编辑这个配置")),
ErrorAction::new("ov config switch", copy(language, "Use another config", "使用其他配置")),
ErrorAction::new(
"ov config",
copy(language, "Edit this config", "编辑这个配置"),
),
ErrorAction::new(
"ov config switch",
copy(language, "Use another config", "使用其他配置"),
),
]),
Error::Api { message, details, .. } => {
Error::Api {
message, details, ..
} => {
let mut report = ErrorReport::new(
copy(language, "OpenViking API Error", "OpenViking API 错误"),
api_error_message(error.code(), message),
Expand All @@ -338,34 +366,59 @@ pub(crate) fn report_for_runtime_error(command: impl Into<String>, error: &Error
copy(language, "Command Error", "命令错误"),
sentence_case_error(message),
)
.with_command(command.clone())
.with_actions(contextual_help_actions(&command, language)),
.with_command(command.clone())
.with_actions(contextual_help_actions(&command, language)),
Error::Parse(message) => ErrorReport::new(
copy(language, "Parse Error", "解析错误"),
sentence_case_error(message),
)
.with_command(command.clone())
.with_actions(contextual_help_actions(&command, language)),
Error::Output(message) => ErrorReport::new(copy(language, "Output Error", "输出错误"), message).with_command(command),
.with_command(command.clone())
.with_actions(contextual_help_actions(&command, language)),
Error::Output(message) => {
ErrorReport::new(copy(language, "Output Error", "输出错误"), message)
.with_command(command)
}
Error::InvalidPath(message) => ErrorReport::new(
copy(language, "Invalid Path", "路径无效"),
sentence_case_error(message),
)
.with_command(command.clone())
.with_actions(contextual_help_actions(&command, language)),
Error::Io(error) => ErrorReport::new(copy(language, "IO Error", "IO 错误"), copy(language, "OpenViking could not read or write a file.", "OpenViking 无法读取或写入文件。"))
.with_command(command)
.with_detail(error.to_string()),
Error::Serialization(error) => {
ErrorReport::new(copy(language, "Serialization Error", "序列化错误"), copy(language, "OpenViking could not parse structured data.", "OpenViking 无法解析结构化数据。"))
.with_command(command)
.with_detail(error.to_string())
}
Error::Zip(error) => ErrorReport::new(copy(language, "Archive Error", "压缩包错误"), copy(language, "OpenViking could not process the archive.", "OpenViking 无法处理压缩包。"))
.with_command(command)
.with_detail(error.to_string()),
Error::AlreadyReported => ErrorReport::new(copy(language, "Command Error", "命令错误"), copy(language, "The command failed.", "命令执行失败。"))
.with_command(command),
.with_command(command.clone())
.with_actions(contextual_help_actions(&command, language)),
Error::Io(error) => ErrorReport::new(
copy(language, "IO Error", "IO 错误"),
copy(
language,
"OpenViking could not read or write a file.",
"OpenViking 无法读取或写入文件。",
),
)
.with_command(command)
.with_detail(error.to_string()),
Error::Serialization(error) => ErrorReport::new(
copy(language, "Serialization Error", "序列化错误"),
copy(
language,
"OpenViking could not parse structured data.",
"OpenViking 无法解析结构化数据。",
),
)
.with_command(command)
.with_detail(error.to_string()),
Error::Zip(error) => ErrorReport::new(
copy(language, "Archive Error", "压缩包错误"),
copy(
language,
"OpenViking could not process the archive.",
"OpenViking 无法处理压缩包。",
),
)
.with_command(command)
.with_detail(error.to_string()),
Error::AlreadyReported => ErrorReport::new(
copy(language, "Command Error", "命令错误"),
copy(language, "The command failed.", "命令执行失败。"),
)
.with_command(command),
}
}

Expand Down Expand Up @@ -1179,7 +1232,8 @@ Usage: ov config [OPTIONS] [COMMAND]
let rendered = strip_ansi(&render_report(&report, false));

assert!(rendered.contains("Connection Error"));
assert!(rendered.contains("Could not reach OpenViking"));
assert!(rendered.contains("connection refused"));
assert!(!rendered.contains("server may be offline"));
assert!(rendered.contains("ov config validate"));
assert!(rendered.contains("ov health"));
assert!(rendered.contains("ov config switch"));
Expand Down
53 changes: 53 additions & 0 deletions crates/ov_cli/tests/connection_errors.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
use std::{
io::{Read, Write},
process::{Command, Stdio},
};

#[test]
fn connection_errors_include_the_underlying_cause() {
let listener = std::net::TcpListener::bind("127.0.0.1:0").unwrap();
let url = format!("http://{}", listener.local_addr().unwrap());
let server = std::thread::spawn(move || {
for _ in 0..2 {
let (mut stream, _) = listener.accept().unwrap();
stream
.set_read_timeout(Some(std::time::Duration::from_secs(5)))
.unwrap();
let mut request = [0; 4096];
assert!(stream.read(&mut request).unwrap() > 0);
stream.write_all(b"not HTTP\r\n\r\n").unwrap();
}
});
let dir = tempfile::tempdir().unwrap();
let config = dir.path().join("ovcli.conf");
std::fs::write(&config, serde_json::json!({"url": url}).to_string()).unwrap();
for format in ["json", "table"] {
let result = Command::new(env!("CARGO_BIN_EXE_ov"))
.current_dir(dir.path())
.env("OPENVIKING_CLI_CONFIG_FILE", &config)
.env("OPENVIKING_LANG", "en")
.env("NO_PROXY", "127.0.0.1")
.env("no_proxy", "127.0.0.1")
.env("NO_COLOR", "1")
.stdin(Stdio::null())
.args(["-o", format, "health"])
.output()
.unwrap();
assert!(!result.status.success(), "{result:?}");
let text = String::from_utf8_lossy(&result.stderr);
let message = text
.replace('│', " ")
.split_whitespace()
.collect::<Vec<_>>()
.join(" ");
assert!(message.contains("invalid HTTP version"), "{text}");
if format == "json" {
let data: serde_json::Value = serde_json::from_slice(&result.stderr).unwrap();
assert_eq!(data["error"]["code"], "UNAVAILABLE");
} else {
assert!(text.contains("Connection Error"), "{text}");
assert!(!text.contains("server may be offline"), "{text}");
}
}
server.join().unwrap();
}
Loading