构建 Rust HTTP 服务
从标准能力与生态出发搭建可测试 HTTP 服务骨架:路由、状态、错误与运行时选择。
[!info] Related notes
构建 Rust HTTP 服务
目标
理解 Rust 服务常见栈:tokio + hyper/axum(或 actix-web 等),用 axum 实际启动健康检查与 JSON echo。下面固定一组可验证依赖;升级时应核对所用 API。
[!abstract] 一句话理解 异步运行时驱动服务;框架负责路由与提取器;需要状态时用 State 提取器和可克隆状态(可包含
Arc)共享;错误映射为 HTTP 状态。
最小可运行示例
场景:本地健康检查与 JSON echo
执行 cargo new mini-http 并进入目录。保留生成的 [package],在 Cargo.toml 中配置:
[dependencies]
axum = "=0.8.6"
tokio = { version = "=1.48.0", features = ["macros", "rt-multi-thread", "net", "signal"] }
serde_json = "=1.0.145"
[dev-dependencies]
tower = { version = "=0.5.2", features = ["util"] }
将以下代码保存到 src/main.rs,执行 cargo run。提交 Cargo.lock 可固定解析后的完整依赖集合。
use axum::{extract::DefaultBodyLimit, routing::{get, post}, Json, Router};
use serde_json::{json, Value};
use std::io;
async fn health() -> Json<Value> {
Json(json!({"status": "ok"}))
}
async fn echo(Json(value): Json<Value>) -> Json<Value> {
Json(json!({"echo": value}))
}
fn app() -> Router {
Router::new()
.route("/health", get(health))
.route("/echo", post(echo))
.layer(DefaultBodyLimit::max(64 * 1024))
}
#[tokio::main]
async fn main() -> io::Result<()> {
let listener = tokio::net::TcpListener::bind("127.0.x.x:8080").await?;
axum::serve(listener, app())
.with_graceful_shutdown(async {
if let Err(err) = tokio::signal::ctrl_c().await {
eprintln!("failed to listen for Ctrl-C: {err}");
}
})
.await
}
在另一终端验证:
curl -i http://127.0.x.x:8080/health
curl -i http://127.0.x.x:8080/echo -H 'Content-Type: application/json' -d '{"x":1}'
分别得到 200 和 JSON {"status":"ok"} / {"echo":{"x":1}}。本例 echo 接受任意 JSON 值,包括数组、数字和 null;若协议只接受对象,应更换提取器目标类型或显式校验。
框架默认行为也属于需要了解的接口边界:未知路径 404、已知路径错误方法 405、JSON 媒体类型缺失/不符 415、JSON 语法错误 400、超过本例提取器限额 413。这些默认 rejection 不保证与成功响应相同的 JSON 错误包络。
DefaultBodyLimit 约束使用该限制的提取器,并非自动覆盖所有自定义 body 读取。例中没有总请求超时、鉴权或依赖就绪检查;/health 只做存活响应。Ctrl-C 触发停止接入并等待在途请求,没有关闭期限或 SIGTERM 支持,部署时接到 优雅关闭 的完整策略。
不监听端口的路由测试
在 src/main.rs 末尾添加测试,执行 cargo test:
#[cfg(test)]
mod tests {
use super::*;
use axum::{body::{to_bytes, Body}, http::{Request, StatusCode}};
use tower::ServiceExt;
#[tokio::test]
async fn health_route() {
let response = app().oneshot(
Request::builder().uri("/health").body(Body::empty()).unwrap()
).await.unwrap();
assert_eq!(response.status(), StatusCode::OK);
assert_eq!(response.headers()["content-type"], "application/json");
let body = to_bytes(response.into_body(), 1024).await.unwrap();
assert_eq!(serde_json::from_slice::<Value>(&body).unwrap(), json!({"status":"ok"}));
}
}
这个测试验证路由和响应,不覆盖真实 TCP、TLS 或进程信号。
2026-10-07:Rust 1.90.0 下文中依赖组合与测试通过;另验证 9 类错误/echo 请求、loopback 临时端口健康请求及协作式停机。尚未验证 TLS、SIGTERM 或关闭超时。
结合场景再看三个关注点
- 框架选择可替换,异步模型相同
- handler 保持瘦,业务进 service 层
- 测试可用
tower服务测试或起端口
核心骨架
| 层 | 职责 |
|---|---|
| router | 路径与方法 |
| extractor | 解析参数/JSON |
| app service | 用例 |
| domain | 规则 |
| infra | DB/HTTP 客户端 |
工程实践
- 显式配置超时与请求体限制,核对中间件实际覆盖范围
- 统一错误类型 → 状态码
- 优雅关闭
- 指标与 tracing
- 配置十二因子
本节总结
- HTTP 服务 = async 运行时 + 框架 + 分层
- 健康检查先通
- 与运维能力同步建设
自测题
- 为什么 handler 不该塞 SQL?
- 健康检查应检查什么边界?
参考答案
- 难测、难替换、违反分层。
- 进程存活 vs 依赖就绪要分开(liveness/readiness)。
延伸阅读与资料来源
| 资料 | 类型 | 支撑内容 |
|---|---|---|
| axum | 生态 | 常用框架 |
| hyper | 生态 | HTTP 库 |
| Tokio | 生态 | 运行时 |