哈喽,大家好呀,我是呼噜噜,对于我们程序员来说,curl 这样的工具 是我们经常使用的,常常用于发送 HTTP 请求并从服务器获取响应。无论是用于测试接口、下载文件,还是调试 Web 应用等等
curl有很多替代品,其中以HTTPie最为出名,它是Python 实现的工具,功能很强大,而且输出结果也比 curl 漂亮多了。
我想用rust来重写HTTPie,查了一下目前这方面资料也比较多,Github上已经有了rust版的xh,它的star数目前高达7.3k,本文参考"Rust 编程第一课"上相关的例子,进行一定的迭代与更新,用rust来实现一个类似于 HTTPie 的命令行工具,帮助大家更好地入门rust
HTTPie由于功能非常的完善,我们先从简单的开始实现,本文要实现哪些主要功能:
- 因为是
cli工具,肯定得有命令行解析,处理子命令和各种参数,验证用户的输入。我这边使用clap库,非常有名的一个库 - 我们使用
reqwest来发送一个HTTP请求,并且能成功获得响应。 - 将响应结果更美观输出给用户。我们使用
colored库,来美化终端
创建项目
我们先检查一下本地的rust开发环境
$cargo --version
rustc 1.88.0 (6b00bc388 2025-06-23)所以本文基于rust1.88.0,其他版本也行
接着我们创建新项目:
cargo new rust_httpie_xh添加依赖
[dependencies]
anyhow = "1.0.99" # 错误处理
clap = { version = "4.5.52", features = ["derive"] } # 命令行
colored = "3.0.0" # 命令终端 颜色
jsonxf = "1.1.1" # json格式化
mime = "0.3.17" # 处理 mime 类型
reqwest = { version = "0.12.24", features = ["json"] } # http 请求
tokio = { version = "1.47.2", features = ["rt", "rt-multi-thread", "macros"] } # 异步
serde_json = "1.0.147" # json 解析我们主要引入这些库,来帮助我们更有效率地开发,这些库的版本,我也都选的比较新
这个项目的核心库是Clap + Tokio + Reqwest,这也是目前 Rust 写CLI 工具的“御三家”。基本上各种cli小工具都会用得到
引入Anyhow来帮我们简化错误处理,避免再去折腾自定义 Error 类型,更详细的rust错误处理,可以去看呼噜噜之前的文章👉深入浅出Rust错误处理:写出更健壮的Rust代码
最后通过Jsonxf + Colored,来美化终端,输出结果必须要漂亮。黑底白字的JSON 谁看得下去?

命令行定义
既然我们要复刻 HTTPie,那得先把交互的“门面”搭起来
我们这里先支持HTTP 协议中最常用的 4 个子命令GET、POST、PUT、DELETE
下面是命令行最经典的定义:
/// cli.rs
use clap::{Parser, Subcommand};
#[derive(Parser)]
#[command(name = "httprs_cli",version = "1.0.0", author="xiaoniuhululu.com", about = "httpie-rs mini", long_about = None)] // 设置名称、版本、作者、简介等元数据
pub struct Cli { // 定义主 CLI 结构体,自动生成 help 信息
#[command(subcommand)]//设置子命令
pub cmd: Command,
}
#[derive(Subcommand)]
pub enum Command {//定义支持的子命令列表
Get(RequestArgs),
Post(RequestArgs),
Put(RequestArgs),
Delete(RequestArgs),
}
#[derive(Parser)]
pub struct RequestArgs {
pub url: String, // 位置参数:URL
pub items: Vec<String>, // 位置参数:后续所有的 kv 对,收集为一个字符串数组
}这段代码比较简单,只要把这个结构体定义好,参数解析、错误提示、--help文档生成这些脏活累活,clap已经在底层帮我们全包圆了。
接下来,我们只需要专注于处理业务逻辑即可
参数解析
搭建好cli入口后,我们需要对用户输入的参数进行解析
一个标准的 HTTP 请求,除去 URL 和 Method,核心其实就剩下“三块骨头”:
Headers: 比如Content-Type、User-AgentQuery Params: 也就是URL ?后面的那一串东西Body: 通常是JSON数据

现在的难点在于,用户在命令行里敲下一堆键值对(比如 a=1 b:2),程序怎么知道哪个是 Header,哪个是 Body?

我们这里借用HTTPie的规则,沿用它的语法约定,通过分隔符来区分:
Header用**:**(例如Content-Type:json)Query用**==**(例如search==rust)JSON String用**=**(例如name=xiaoniu,值会被处理为字符串)JSON Value用**:=**(例如age:=18或is_admin:=true,值会被当作原始JSON类型,支持布尔值、数字甚至嵌套结构)
我们这里定义KV枚举来统一承载这些类型:
/// kv.rs
use anyhow::{anyhow, Result};
use std::str::FromStr;
/// 定义核心数据模型,kv键值对
#[derive(Debug, Clone)]
pub enum Kv {
Header(String, String), // 对应 header:value
Query(String, String), // 对应 query==value
Json(String, serde_json::Value), // 对应 json=value 或 json:=value
}
/// 实现 FromStr trait
/// 解析键值对字符串,将其转换为强类型的 Kv 枚举
impl FromStr for Kv {
type Err = anyhow::Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
// 优先级很重要:先判断复杂的,再判断简单的
if let Some((k, v)) = s.split_once(":=") {// 处理 JSON 原生类型 (key:=value)
Ok(Kv::Json(k.into(), serde_json::from_str(v)?))
} else if let Some((k, v)) = s.split_once("==") { // 处理 Query 参数 (key==value)
Ok(Kv::Query(k.into(), v.into()))
} else if let Some((k, v)) = s.split_once(':') { // 处理 Header (key:value)
Ok(Kv::Header(k.into(), v.into()))
} else if let Some((k, v)) = s.split_once('=') { // 处理普通 JSON 字符串 (key=value)
Ok(Kv::Json(k.into(), serde_json::Value::String(v.into())))
} else {
Err(anyhow!("invalid argument: {}, expected format like key=value", s))
}
}
}这里我们利用 Rust 强大的 FromStr trait,把解析逻辑封装在结构体内部,这样可以让我们的代码看起来干净又舒服。
另外需要注意这里,判断的优先级不能乱。比如 := 必须在 : 之前判断,否则 age:=18 可能会被误判为 Header。
使用 split_once 既高效又语义清晰,比写正则匹配要舒服得多
请求构建
我们现在已经把用户输入的各种参数Headers、Query、Body都统一解析成了 KV 枚举,接下来把这些零散的参数组装成真正的 HTTP 请求了
request.rs 的实现:
/// request.rs
use reqwest::{Client, RequestBuilder};
use serde_json::{Map, Value};
use reqwest::header::{HeaderMap, HeaderName, HeaderValue}; // 显式引入 header 相关类型
use crate::kv::Kv;
pub fn build_request(
client: &Client,
method: reqwest::Method,
url: &str,
items: Vec<Kv>,
) -> anyhow::Result<RequestBuilder> {
let mut headers = HeaderMap::new();
let mut query = Vec::new();
let mut json = Map::new();
for item in items {
match item {
Kv::Header(k, v) => {
//这里需要显式指定解析的目标类型HeaderName,HeaderValue;因为rust编译器的类型推导能力有限
headers.insert(k.parse::<HeaderName>()?, v.parse::<HeaderValue>()?);
}
Kv::Query(k, v) => query.push((k, v)),
Kv::Json(k, v) => {
json.insert(k, v);
}
}
}
// 初始化请求构建器
let mut req = client.request(method, url).headers(headers);
if !query.is_empty() {
req = req.query(&query);
}
// 如果有 JSON 字段,序列化为 Body
if !json.is_empty() {
req = req.json(&Value::Object(json));
}
Ok(req)
}这步工作看似简单,其实是个“分拣”的过程。我们需要把混在一起的 KV 列表,分门别类地塞进三个不同的“桶”里:HeaderMap、Query 数组和 JSON 对象
注意下headers.insert(k.parse::<HeaderName>()?, v.parse::<HeaderValue>()?)这段代码,这里rust编译器的类型无法自动推导出来,需要显式指定解析的目标类型HeaderName、HeaderValue
在 Python 或 JS 里,Header 可能就是个字符串字典,但在 Rust 里不行,HeaderName 和 HeaderValue 都有严格的校验规则,它能确保你传进去的 HTTP 头是合规的,否则直接抛错,把问题拦截在发送请求之前
我们还使用了 serde_json::Map 来收集 JSON 字段。这样无论用户输入了多少个 key=value,最后都会被合并成一个完整的 JSON 对象,再通过 req.json()一次性序列化。
响应打印
当请求发出去了,服务器回包了。
如果直接把服务器返回的原始字符串拍在终端上,那一坨压缩后的 JSON 能把人看瞎。既然是对标 HTTPie,我们的输出必须得带颜色、带格式化,看起来得赏心悦目
我们先来了解一下http的响应报文,响应报文包括四部分,分别为响应首行(Status Line)、响应头(Headers)、空行(Blank Line)、响应体(Response Body),如下图所示

这里我们主要关注三个部分的处理:状态行、Header、Body,打印并美化响应:
/// response.rs
use colored::Colorize;
use mime::Mime;
use reqwest::Response;
pub async fn render(resp: Response) -> anyhow::Result<()> {
//打印状态行:协议版本 + 状态码
println!(
"{} {}\n",
format!("{:?}", resp.version()).blue(),
resp.status().to_string().blue()
);
//打印 Header,为了便于区分,把 Key 染成绿色
for (k, v) in resp.headers() {
println!("{}: {:?}", k.to_string().green(), v);
}
// Header 和 Body 之间空一行,符合 HTTP 协议视觉习惯
println!();
//处理 Body
let mime = resp
.headers()
.get(reqwest::header::CONTENT_TYPE)
.and_then(|v| v.to_str().ok())
.and_then(|v| v.parse::<Mime>().ok());
// 获取原始 Body 文本
let body = resp.text().await?;
// 如果是 JSON,则进行格式化输出
if let Some(m) = mime {
if m.type_() == mime::APPLICATION && m.subtype() == mime::JSON {
if let Ok(pretty) = jsonxf::pretty_print(&body) {
println!("{}", pretty.cyan());
return Ok(());
}
}
}
// 如果不是 JSON 或者格式化失败,就原样打印
println!("{}", body);
Ok(())
}美化响应报文,我用了 colored 库给不同区域上了色。状态码用蓝色,Header 的 Key 用绿色,JSON Body 用青色
这里我们还用了mime::Mime来精准识别HTTP 头里的 Content-Type,而不是把它当成普通字符串处理,避免了很多麻烦
main.rs
前面我们完成了 cli、kv、request、response这些"零件",现在我就通过 main.rs 把它们组装起来
拧紧螺丝,点火启动!
话不多说,直接看最终的组装代码:
/// main.rs
mod cli;
mod kv;
mod request;
mod response;
use clap::Parser;
use reqwest::{Client, Method};
#[tokio::main]
async fn main() -> anyhow::Result<()> {
// 解析命令行参数
// 如果参数格式不对,clap 会在这里直接打印 help 信息并退出,不用我们操心
let cli = cli::Cli::parse();
// 初始化 HTTP 客户端
let client = Client::new();
// 匹配子命令,确定 HTTP 方法和参数
let (method, args) = match cli.cmd {
cli::Command::Get(a) => (Method::GET, a),
cli::Command::Post(a) => (Method::POST, a),
cli::Command::Put(a) => (Method::PUT, a),
cli::Command::Delete(a) => (Method::DELETE, a),
};
// 解析 kv 参数
let items = args
.items
.iter()
.map(|s| s.parse())
.collect::<Result<Vec<_>, _>>()?;
// 构建请求
let req = request::build_request(&client, method, &args.url, items)?;
// 发送请求
let resp = req.send().await?;
// 渲染响应
response::render(resp).await
}这个main函数的逻辑非常直白,就解析命令行 -> 解析 kv 参数 -> 构建请求 -> 发送请求 -> 渲染响应
代码写完了,是骡子是马得拉出来溜溜~
在项目根目录下执行:
#========GET=========
# get基础访问
cargo run get https://httpbin.org/get
# 带查询参数 (Result: ?a=1&b=2)
cargo run get https://httpbin.org/get a==1 name==xiaoniuhululu
# 带请求头 (Result: Headers 中包含 X-Api-Key 和 User-Agent)
cargo run get https://httpbin.org/get X-Api-Key:abc-123 User-Agent:HttpRs/1.0
# 混合:参数 + 请求头
cargo run get https://httpbin.org/get search==rust lang==cn Authorization:BearerToken
#========POST=========
# 纯字符串字段 (Result: {"greeting": "wx", "name": "xiaoniuhululu"})
cargo run post https://httpbin.org/post greeting=wx name=xiaoniuhululu
# 混合类型:字符串 + 数字 + 布尔值 (注意 := 的用法)
cargo run post https://httpbin.org/post name=xiaoniuhululu id:=100 is_admin:=true
# 测试覆盖:带浮点数和 null
cargo run post https://httpbin.org/post score:=99.5 parent:=null
#========PUT=========
# PUT
cargo run put https://httpbin.org/put name=jack age:=18
#========DELETE=========
# 常见用法:通过 URL Query 删除
cargo run delete https://httpbin.org/delete id==1
# 特殊用法:部分 API 要求在 DELETE Body 中带数据
cargo run delete https://httpbin.org/delete ids:='[10, 20]'上述测试用例均使用了 httpbin.org,它是一个开源的 HTTP 测试服务,它会把接收到的请求内容(Headers, Body, Args)原样返回给你

单元测试
我们这里再补充一些单元测试,来测试边界情况、将测试用例,作为活的文档,方便后续重构优化:
/// kv.rs
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parse_header() {
let kv = "Authorization:token".parse().unwrap();
matches!(kv, Kv::Header(_, _));
}
#[test]
fn parse_query() {
let kv: Kv = "page==1".parse().unwrap();
assert!(matches!(kv, Kv::Query(k, v) if k == "page" && v == "1"));
}
#[test]
fn parse_json_string() {
let kv: Kv = "name=jack".parse().unwrap();
assert!(matches!(kv, Kv::Json(k, v) if k == "name" && v == "jack"));
}
#[test]
fn parse_json_raw() {
let kv: Kv = "age:=18".parse().unwrap();
// 检查 json 的数字类型
assert!(matches!(kv, Kv::Json(k, v) if k == "age" && v == 18));
}
#[test]
fn parse_json_obj() {
// 测试复杂 JSON 对象
let kv: Kv = r#"user:={"name":"rose"}"#.parse().unwrap();
if let Kv::Json(k, v) = kv {
assert_eq!(k, "user");
assert_eq!(v["name"], "rose");
} else {
panic!("expected json obj");
}
}
#[test]
fn invalid_kv() {
let result = "xxx".parse::<Kv>();
assert!(result.is_err());
}
}
///request.rs
#[cfg(test)]
mod tests {
use super::*;
use reqwest::{Client, Method};
#[test]
fn build_post_request() {
let client = Client::new();
let items = vec![
Kv::Json("name".into(), "jack".into()),
Kv::Header("X-Test".into(), "1".into()),
];
let req = build_request(&client, Method::POST, "http://example.com", items).unwrap();
let req = req.build().unwrap();
assert_eq!(req.method(), Method::POST);
assert!(req.headers().contains_key("x-test"));
}
#[test]
fn build_delete_with_body() {
let client = Client::new();
let items = vec![Kv::Json("id".into(), 1.into())];
let req = build_request(&client, Method::DELETE, "http://example.com", items).unwrap();
let req = req.build().unwrap();
assert_eq!(req.method(), Method::DELETE);
}
}Rust 支持条件编译,这里 #[cfg(test)] 表明整个 mod tests 都只在 cargo test 时才编译
我们只需运行 cargo test,就能一次性跑完这些单元测试

最后我们无论发布给别人用或还是自用,追求极致的性能,更小的体积,更快的运行速度的话,可以使用cargo build --release来打个包

大小仅3.4M!

本文到这里就结束啦,这是一个非常适合新手练手的项目,复刻了 Python 著名的 HTTP 客户端工具 httpie。代码清爽简洁,不得不感谢Rust 强大的生态系统
关注我的公众号【小牛呼噜噜】,获取更多干货,下期我们再见!
扫码到公众号中,回复关键字:rust_httpie_xh,即可获取本文完整代码示例和练习项目
作者:小牛呼噜噜
本文到这里就结束啦,感谢阅读,关注同名公众号:小牛呼噜噜,防失联+获取更多技术干货