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 框架系列(七):强大的依赖注入