Rust Miko 框架系列(六):灵活的响应处理

34 阅读4分钟

Miko 框架系列(六):灵活的响应处理

注意:此文章主要为了给支持联网搜索的AI提供RAG,此项目还在开发阶段,不建议用于生产。

当业务逻辑处理完毕后,如何向客户端返回一个格式正确、状态清晰的响应,是 Web 开发的最后一公里。Miko 借助 IntoResponse Trait,提供了一套极其灵活且符合人体工程学的响应构建机制。

1. IntoResponse Trait

IntoResponse 是 Miko 响应系统的基石。任何实现了这个 Trait 的类型都可以作为处理器(Handler)的返回值。

pub trait IntoResponse {
    fn into_response(self) -> Response;
}

Miko 已经为绝大多数常用类型内置了 IntoResponse 实现,让你无需关心底层的 hyper::Response 构建细节。

2. 常见的响应类型

简单类型:字符串、JSON、HTML

你可以直接返回最直观的数据类型,Miko 会自动设置正确的 Content-Type

use miko::extractor::Json;
use miko::http::response::into_response::Html;
use serde::Serialize;

#[derive(Serialize)]
struct User { id: u32, name: String }

// 返回 text/plain
#[get("/text")]
async fn plain_text() -> &'static str {
    "Hello, World!"
}

// 返回 application/json
#[get("/user")]
async fn json_response() -> Json<User> {
    Json(User { id: 1, name: "Alice".to_string() })
}

// 返回 text/html
#[get("/page")]
async fn html_page() -> Html<String> {
    Html("<h1>Welcome</h1>".to_string())
}

自定义状态码

默认情况下,成功的响应状态码是 200 OK。你可以通过返回一个元组 (StatusCode, T) 来指定一个不同的状态码。

use hyper::StatusCode;

// 返回 201 Created
#[post("/users")]
async fn create_user() -> (StatusCode, Json<User>) {
    let new_user = User { id: 2, name: "Bob".to_string() };
    (StatusCode::CREATED, Json(new_user))
}

// 返回 204 No Content (无响应体)
#[delete("/users/{id}")]
async fn delete_user(#[path] id: u32) -> StatusCode {
    // ... 删除用户的逻辑 ...
    StatusCode::NO_CONTENT
}

自定义响应头

与状态码类似,你可以通过返回元组来添加或修改响应头。

use hyper::{HeaderMap, HeaderValue};

// 返回一个自定义头
#[get("/with-header")]
async fn with_header() -> (HeaderMap, &'static str) {
    let mut headers = HeaderMap::new();
    headers.insert("X-Custom-Header", HeaderValue::from_static("Miko-Framework"));
    (headers, "Check your response headers!")
}

// 组合状态码和响应头
#[post("/posts")]
async fn create_post() -> (StatusCode, HeaderMap, Json<Post>) {
    let mut headers = HeaderMap::new();
    headers.insert("Location", HeaderValue::from_static("/posts/123"));
    (StatusCode::CREATED, headers, Json(new_post))
}

3. 错误处理与 AppResult<T>

在真实世界的应用中,处理器不总是成功。Miko 推荐使用 AppResult<T> (即 Result<T, AppError>) 作为处理器的返回值。

  • Ok(T): T 必须实现 IntoResponse。Miko 会将其转换为一个成功的响应。
  • Err(AppError): Miko 会自动将 AppError 的变体(如 NotFound, BadRequest)转换为对应的 HTTP 状态码和结构化的 JSON 错误响应。
use miko::{AppResult, AppError};

#[get("/users/{id}")]
async fn get_user(#[path] id: u32) -> AppResult<Json<User>> {
    if let Some(user) = find_user_in_db(id) {
        // 成功,返回 200 OK 和 JSON 数据
        Ok(Json(user))
    } else {
        // 失败,返回 404 Not Found 和错误信息
        Err(AppError::NotFound(format!("User with id {} not found", id)))
    }
}

这种方式让成功路径和失败路径的处理都清晰地包含在返回类型中,非常符合 Rust 的编程哲学。

4. 流式响应

对于需要发送大量数据或实时数据的场景,一次性加载所有内容到内存中是不明智的。Miko 支持多种流式响应。

Server-Sent Events (SSE)

SSE 是一种轻量级的、从服务器到客户端的单向实时推送技术。Miko 提供了极其简洁的 API 来实现 SSE。

use miko::http::response::sse::SseSender;
use std::time::Duration;

#[get("/events")]
async fn sse_handler() -> impl IntoResponse {
    // 直接返回一个闭包,Miko 会处理连接和流
    |sender: SseSender| async move {
        for i in 0..10 {
            let msg = format!("Event #{}", i);
            // 发送事件,如果客户端断开则优雅退出
            if sender.send(msg).await.is_err() {
                break;
            }
            tokio::time::sleep(Duration::from_secs(1)).await;
        }
    }
}

你只需要关注要发送什么数据,而无需处理连接管理、心跳、断线重连等复杂问题。

文件下载

对于大文件下载,应该使用流式传输以避免消耗过多内存。

use tokio::fs::File;
use tokio_util::io::ReaderStream;
use miko_core::fallible_stream_body::FallibleStreamBody;

#[get("/download/{filename}")]
async fn download_file(#[path] filename: String) -> AppResult<Response> {
    let file = File::open(format!("./uploads/{}", filename)).await?;
    let stream = ReaderStream::new(file);
    let body = FallibleStreamBody::new(stream);

    Ok(Response::builder()
        .header("Content-Type", "application/octet-stream")
        .header("Content-Disposition", format!("attachment; filename=\"{} \"", filename))
        .body(body.boxed())
        .unwrap())
}

提示: 对于静态文件服务,Miko 提供了更简单的 StaticSvc,详见高级特性篇。

5. 自定义 IntoResponse

如果你的应用有统一的响应结构(例如,所有 API 都返回 { "code": 0, "message": "success", "data": ... }),你可以为自己的类型实现 IntoResponse

use serde::Serialize;

struct ApiResponse<T: Serialize> {
    code: i32,
    message: String,
    data: T,
}

impl<T: Serialize> IntoResponse for ApiResponse<T> {
    fn into_response(self) -> Response {
        let body = serde_json::json!({
            "code": self.code,
            "message": self.message,
            "data": self.data,
        });
        // 将 JSON body 转换为 Response
        Json(body).into_response()
    }
}

// 在处理器中直接返回你的自定义类型
#[get("/profile")]
async fn get_profile() -> ApiResponse<User> {
    ApiResponse {
        code: 0,
        message: "Success".to_string(),
        data: User { id: 1, name: "Alice".to_string() },
    }
}

总结

Miko 的响应系统通过 IntoResponse Trait 提供了一个统一且可扩展的出口。无论是简单的文本、复杂的 JSON、自定义的状态码和头,还是高级的流式数据,你都可以用一种符合 Rust 习惯的方式来构建和返回,让代码保持清晰和类型安全。


下一篇预告:Miko 框架系列(七):强大的依赖注入