本文将详细介绍如何使用 Rust 语言实现一个类似 HTTPie 的命令行 HTTP 客户端工具。这个工具命名为
h,追求极简的使用体验,让 API 测试只需几个字符。
作为开发者,我们每天都在与 API 打交道。无论是调试后端接口、测试第三方服务,还是快速验证某个想法,一个趁手的 HTTP 客户端工具必不可少。
虽然 curl 功能强大,但它的语法对于日常使用来说过于繁琐。比如发送一个带 JSON body 的 POST 请求:
curl -X POST -H "Content-Type: application/json" -d '{"name":"test"}' https://api.example.com/users而使用我们今天要实现的 h 工具,同样的请求只需要:
h p api.example.com/users name=test是不是简洁多了?这就是我们的目标:用最少的输入完成最常见的操作。
在开始编码之前,我们先确定几个核心设计原则:
h,方法可以用简写(g/p/u/d/pa)整个项目采用模块化架构,将功能清晰地分离到不同模块:

这种设计的好处是每个模块职责单一,便于测试和维护。
用户输入到响应输出的完整处理流程如下:

首先创建项目并配置依赖。我们的 Cargo.toml 如下:
[package]
name = "rust_http"
version = "0.1.0"
edition = "2021"
[lib]
name = "rust_http"
path = "src/lib.rs"
[[bin]]
name = "h"
path = "src/main.rs"
[dependencies]
reqwest = { version = "0.11", features = ["json"] }
tokio = { version = "1", features = ["full"] }
clap = { version = "4", features = ["derive"] }
colored = "2"
serde_json = "1"
serde = { version = "1", features = ["derive"] }
urlencoding = "2"
[dev-dependencies]
proptest = "1"依赖说明:
reqwest:Rust 生态中最流行的 HTTP 客户端库tokio:异步运行时,reqwest 需要它来执行异步请求clap:命令行参数解析库,支持 derive 宏简化代码colored:终端颜色输出库serde_json:JSON 序列化/反序列化proptest:属性测试库,用于编写更健壮的测试
URL 解析是第一个要实现的模块。我们希望用户可以用最简短的方式输入 URL:
:8080/api → http://localhost:8080/api(本地开发最常用)api.example.com/users → https://api.example.com/users(自动补全 https)http://... 或 https://... → 原样使用pub struct UrlResolver;
impl UrlResolver {
pub fn resolve(input: &str) -> Result<String, UrlError> {
let input = input.trim();
if input.is_empty() {
return Err(UrlError {
message: "URL cannot be empty".to_string(),
});
}
// 已有协议,直接返回
if input.starts_with("http://") || input.starts_with("https://") {
return Ok(input.to_string());
}
// 以冒号开头后跟端口号,补全为 localhost
if input.starts_with(':') {
let rest = &input[1..];
if rest.chars().next().map(|c| c.is_ascii_digit()).unwrap_or(false) {
return Ok(format!("http://localhost:{}", rest));
}
}
// 默认补全 https://
Ok(format!("https://{}", input))
}
}这个模块虽然简单,但极大地提升了使用体验。开发时测试本地接口,只需要 h :8080/api 就够了。

这是整个工具的核心模块之一。我们通过参数的格式来自动识别用户意图:
模式 | 类型 | 示例 |
|---|---|---|
| JSON 数据 |
|
| 请求头 |
|
| URL 参数 |
|
pub enum ParamType {
JsonData(String, String), // key=value → JSON 字段
Header(String, String), // key:value → 请求头
QueryParam(String, String), // key==value → URL 参数
}
impl Parser {
pub fn parse_param(input: &str) -> Result<ParamType, ParseError> {
// 先检查 query param (key==value)
if let Some(pos) = input.find("==") {
let key = &input[..pos];
let value = &input[pos + 2..];
if !key.is_empty() {
return Ok(ParamType::QueryParam(key.to_string(), value.to_string()));
}
}
// 检查 JSON data (key=value),但不是 ==
if let Some(pos) = input.find('=') {
let next_char = input.chars().nth(pos + 1);
if next_char != Some('=') {
let key = &input[..pos];
let value = &input[pos + 1..];
if !key.is_empty() {
return Ok(ParamType::JsonData(key.to_string(), value.to_string()));
}
}
}
// 检查 header (key:value)
if let Some(pos) = input.find(':') {
let key = &input[..pos];
let value = &input[pos + 1..];
if !key.is_empty() {
return Ok(ParamType::Header(key.to_string(), value.to_string()));
}
}
Err(ParseError {
message: format!(
"Invalid parameter '{}', expected key=value, key:value, or key==value",
input
),
})
}
}注意这里的解析顺序很重要:必须先检查 ==(双等号),再检查 =(单等号),否则 page==1 会被错误地解析为 JSON 数据 {"page": "=1"}。

方法简写也是一个贴心的设计:
pub fn parse_method(input: &str) -> String {
match input.to_lowercase().as_str() {
"g" | "get" => "GET".to_string(),
"p" | "post" => "POST".to_string(),
"u" | "put" => "PUT".to_string(),
"d" | "delete" => "DELETE".to_string(),
"pa" | "patch" => "PATCH".to_string(),
_ => input.to_uppercase(),
}
}这样 h p 就等于 h POST,h d 就等于 h DELETE,大大减少了输入量。
HTTP 请求的发送使用 reqwest 库,我们封装了一个简洁的接口:
pub struct Response {
pub status: u16,
pub status_text: String,
pub headers: Vec<(String, String)>,
pub body: String,
pub duration: Duration,
}
impl Client {
pub async fn send(
method: &str,
url: &str,
headers: &[(String, String)],
body: Option<&str>,
) -> Result<Response, ClientError> {
let client = reqwest::Client::builder()
.timeout(Duration::from_secs(30)) // 30秒超时
.redirect(Policy::limited(10)) // 最多跟随10次重定向
.build()?;
let reqwest_method = match method.to_uppercase().as_str() {
"GET" => reqwest::Method::GET,
"POST" => reqwest::Method::POST,
"PUT" => reqwest::Method::PUT,
"DELETE" => reqwest::Method::DELETE,
"PATCH" => reqwest::Method::PATCH,
_ => return Err(ClientError::other(&format!("Unsupported method: {}", method))),
};
let mut request_builder = client.request(reqwest_method, url);
// 添加请求头
for (key, value) in headers {
request_builder = request_builder.header(key.as_str(), value.as_str());
}
// 添加请求体
if let Some(body_content) = body {
request_builder = request_builder.body(body_content.to_string());
}
let start_time = Instant::now();
let response = request_builder.send().await?;
let duration = start_time.elapsed();
// 提取响应信息...
Ok(Response { status, status_text, headers, body, duration })
}
}错误处理也很重要,我们为不同类型的错误提供了友好的提示:
pub enum ClientErrorKind {
ConnectionFailed(String), // 连接失败
Timeout(Duration), // 请求超时
InvalidUrl(String), // 无效 URL
Other(String), // 其他错误
}当连接失败时,用户会看到 Error: Failed to connect to api.example.com - connection refused,而不是一堆难以理解的堆栈信息。
美观的输出是提升使用体验的关键。我们为 JSON、HTML、XML 实现了语法高亮:
impl Highlighter {
pub fn highlight_json(json: &str) -> String {
if let Ok(value) = serde_json::from_str::<serde_json::Value>(json) {
Self::highlight_json_value(&value, 0)
} else {
json.to_string()
}
}
fn highlight_json_value(value: &serde_json::Value, indent: usize) -> String {
match value {
serde_json::Value::Null => "null".red().to_string(),
serde_json::Value::Bool(b) => b.to_string().magenta().to_string(),
serde_json::Value::Number(n) => n.to_string().yellow().to_string(),
serde_json::Value::String(s) => format!("\"{}\"", s).green().to_string(),
serde_json::Value::Array(arr) => {
// 递归处理数组...
}
serde_json::Value::Object(obj) => {
// 递归处理对象,键用 cyan 颜色...
}
}
}
}JSON 高亮的配色方案:
对于 HTML 和 XML,我们也实现了基础的高亮:
fn highlight_tag(tag: &str) -> String {
// 标签名:蓝色
// 属性名:青色
// 属性值:绿色
// 注释:灰色
}
格式化模块负责将所有信息以美观的方式呈现给用户:
impl Formatter {
pub fn print_response(
request: &ParsedRequest,
response: &Response,
options: &FormatOptions,
) {
// 静默模式:只输出响应体
if options.quiet {
Self::print_body(&response.body, Self::get_content_type(&response.headers));
return;
}
// 详细模式:先显示请求信息
if options.verbose {
Self::print_request(request);
println!();
}
// 状态行(带颜色)
Self::print_status_line(response);
// 耗时
Self::print_duration(response);
// 响应头
Self::print_response_headers(&response.headers);
// 响应体(带语法高亮)
Self::print_body(&response.body, Self::get_content_type(&response.headers));
}
}状态码的颜色编码让用户一眼就能看出请求是否成功:
pub fn status_color(status: u16) -> &'static str {
match status {
200..=299 => "green", // 成功:绿色
300..=399 => "yellow", // 重定向:黄色
400..=599 => "red", // 错误:红色
_ => "white",
}
}最后是命令行入口,使用 clap 库来解析参数:
#[derive(ClapParser, Debug)]
#[command(name = "h")]
struct Cli {
/// HTTP 方法或 URL
#[arg(required = true)]
method_or_url: String,
/// URL(如果第一个参数是方法)或第一个参数
#[arg()]
url_or_param: Option<String>,
/// 请求参数
#[arg(trailing_var_arg = true)]
params: Vec<String>,
/// 详细模式
#[arg(short, long)]
verbose: bool,
/// 静默模式
#[arg(short, long)]
quiet: bool,
}这里有个巧妙的设计:第一个参数可以是方法也可以是 URL。如果是方法(如 g、p、POST 等),则第二个参数是 URL;如果不是方法,则第一个参数就是 URL,默认使用 GET 方法。
除了常规的单元测试,我们还使用 proptest 库进行属性测试。属性测试的思想是:定义系统应该满足的属性,然后让测试框架自动生成大量随机输入来验证这些属性。
例如,URL 解析的属性测试:
proptest! {
#![proptest_config(ProptestConfig::with_cases(100))]
// 属性1:以 :port 开头的 URL 应该解析为 localhost
#[test]
fn prop_port_shortcut_resolves_to_localhost(port in 1u16..65535u16) {
let input = format!(":{}", port);
let result = UrlResolver::resolve(&input).unwrap();
prop_assert!(result.starts_with("http://localhost:"));
prop_assert!(result.contains(&port.to_string()));
}
// 属性2:已有 http:// 协议的 URL 应该原样返回
#[test]
fn prop_http_url_unchanged(host in "[a-z]{3,10}") {
let input = format!("http://{}.com", host);
let result = UrlResolver::resolve(&input).unwrap();
prop_assert_eq!(result, input);
}
}参数解析的属性测试确保了解析逻辑的正确性:
proptest! {
// key=value 应该被解析为 JSON 数据
#[test]
fn prop_json_data_parsing(key in valid_key(), value in valid_value()) {
let input = format!("{}={}", key, value);
let result = Parser::parse_param(&input).unwrap();
prop_assert_eq!(result, ParamType::JsonData(key, value));
}
// key:value 应该被解析为请求头
#[test]
fn prop_header_parsing(key in valid_key(), value in valid_value()) {
let input = format!("{}:{}", key, value);
let result = Parser::parse_param(&input).unwrap();
prop_assert_eq!(result, ParamType::Header(key, value));
}
// key==value 应该被解析为查询参数
#[test]
fn prop_query_param_parsing(key in valid_key(), value in valid_value()) {
let input = format!("{}=={}", key, value);
let result = Parser::parse_param(&input).unwrap();
prop_assert_eq!(result, ParamType::QueryParam(key, value));
}
}
属性测试帮助我们发现了一些边界情况的 bug,比如空键名、特殊字符等。每个属性测试运行 100 次,大大提高了代码的可靠性。
让我们看看这个工具的实际使用效果:
# 最简形式
h :8080/api/users
# 带查询参数
h api.example.com/users page==1 limit==10
# 带自定义请求头
h api.example.com/users Authorization:"Bearer token123"
# 发送 JSON 数据
h p api.example.com/users name=test email=test@example.com
# 等价于
curl -X POST -H "Content-Type: application/json" \
-d '{"name":"test","email":"test@example.com"}' \
https://api.example.com/users
# PUT 请求
h u api.example.com/users/1 name=updated
# DELETE 请求
h d api.example.com/users/1
# PATCH 请求
h pa api.example.com/users/1 status=active# 详细模式:显示请求详情
h -v api.example.com/users
# 静默模式:只输出响应体(适合管道处理)
h -q api.example.com/users | jq '.data'通过这个项目,我们实现了一个功能完整、使用简便的 HTTP 客户端命令行工具。主要特点包括:
Rust 语言的类型系统和模式匹配让代码既安全又优雅。reqwest 和 clap 等优秀的库让我们可以专注于业务逻辑,而不用重复造轮子。
这个项目的代码量不大(约 1000 行),但涵盖了 Rust 开发的很多实践:模块化设计、错误处理、异步编程、命令行解析、属性测试等。希望这篇文章能对你有所帮助!
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。