← [ BACK_TO_NOTES ]

构建 Rust HTTP 服务

从标准能力与生态出发搭建可测试 HTTP 服务骨架:路由、状态、错误与运行时选择。

#howto #growing #dev #rust

[!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 或关闭超时。

结合场景再看三个关注点

  1. 框架选择可替换,异步模型相同
  2. handler 保持瘦,业务进 service 层
  3. 测试可用 tower 服务测试或起端口

核心骨架

层职责
router路径与方法
extractor解析参数/JSON
app service用例
domain规则
infraDB/HTTP 客户端

工程实践

  1. 显式配置超时与请求体限制,核对中间件实际覆盖范围
  2. 统一错误类型 → 状态码
  3. 优雅关闭
  4. 指标与 tracing
  5. 配置十二因子

本节总结

  • HTTP 服务 = async 运行时 + 框架 + 分层
  • 健康检查先通
  • 与运维能力同步建设

自测题

  1. 为什么 handler 不该塞 SQL?
  2. 健康检查应检查什么边界?
参考答案
  1. 难测、难替换、违反分层。
  2. 进程存活 vs 依赖就绪要分开(liveness/readiness)。

延伸阅读与资料来源

资料类型支撑内容
axum生态常用框架
hyper生态HTTP 库
Tokio生态运行时
创建于 2026/7/15 更新于 2026/10/7
Digital Biome
Access

键盘导航

j / k / ↑ / ↓
选择对象
Enter / o
打开选择
/ 或 Ctrl+K
全站搜索
i / f / 1–4
筛选当前列表
g → h / l / p / t / a
首页 / 笔记 / 项目 / 工具 / About
r → 1–9
打开所选对象的关联
u
返回上一页
Esc
关闭浮层 / 取消命令;输入框内清空文本