1. Responder 是什么
Responder 的职责是:把一个 Rust 值转成真正的 HTTP 响应 Response。一个 Response 通常包括:
- HTTP Status(状态码)
- Headers(响应头)
- Body(响应体,可能是固定大小,也可能是流式)
实现 Responder 的类型可以根据 incoming Request 动态调整响应。例如同一个 responder 可以根据 Accept 头返回不同格式。
一个典型的例子是 String:它会生成 Content-Type: text/plain,并把字符串作为"固定大小的 body"返回。而像文件类型(例如 NamedFile)通常是"流式响应"。
2. "包装型 responder":用组合拼出你要的响应
Rocket 特别鼓励你用"包装(wrapping)"的方式构造响应:
rust
struct WrappingResponder<R>(R); // R: Responder
包装 responder 做的事通常是:先让内部 responder 生成响应,再修改 status 或 header,然后返回。
2.1 改状态码:status 模块
例如 status::Accepted 会把状态码固定成 202:
rust
use rocket::response::status;
#[post("/<id>")]
fn new(id: usize) -> status::Accepted<String> {
status::Accepted(format!("id: '{}'", id))
}
2.2 改 Content-Type:content 模块
例如 content::RawJson 可以把内容标记为 JSON(注意这不是 Json<T> 序列化那个):
rust
use rocket::http::Status;
use rocket::response::{content, status};
#[get("/")]
fn json() -> status::Custom<content::RawJson<&'static str>> {
status::Custom(Status::ImATeapot, content::RawJson("{ \"hi\": \"world\" }"))
}
2.3 直接用 tuple responder 覆写 Status / Content-Type
Rocket 内置了 (Status, R) 和 (ContentType, R) 这样的 responder(R: Responder),用起来很顺手:
rust
use rocket::http::{Status, ContentType};
#[get("/")]
fn json() -> (Status, (ContentType, &'static str)) {
(Status::ImATeapot, (ContentType::JSON, "{ \"hi\": \"world\" }"))
}
2.4 写一个"可复用的响应类型":derive Responder
如果你在项目里经常要返回某种固定格式(比如固定 status + content-type),建议封装成自己的 responder:
rust
#[derive(Responder)]
#[response(status = 418, content_type = "json")]
struct RawTeapotJson(&'static str);
#[get("/")]
fn json() -> RawTeapotJson {
RawTeapotJson("{ \"hi\": \"world\" }")
}
这种写法非常适合做"统一响应封装",比如 ApiOk<T>、ApiErr、Created<T> 等。
3. Responder 也可能失败:错误 catcher 会接管
Responder 不一定总能生成响应,它可以返回 Err(Status) 表示"我失败了"。Rocket 遇到这种情况会:
- 将请求交给对应状态码的 error catcher
- 如果没有注册 catcher,使用默认 catcher(通常返回 HTML 错误页或根据 Accept 返回 JSON)
- 如果是自定义状态码且无 catcher,通常会落到 500 catcher
3.1 直接返回 Status 也能触发 catcher(不推荐但可用)
你甚至可以让 handler 直接返回 Status:
rust
use rocket::http::Status;
#[get("/")]
fn just_fail() -> Status {
Status::NotAcceptable
}
规则大致是:
- 400--599:转发到对应 catcher
- 100 与 200--205:返回空 body + 对应状态
- 其他:视为无效,转到 500 catcher
实战里更建议用 Result<T, E> 或 status::Custom 显式表达你的意图。
4. 自定义 Responder:手写 impl vs derive
4.1 手写实现(你很少需要)
文档里给了 String 的实现例子,本质上就是 build 一个 Response:
- 设置 header
- 设置 body(sized 或 stream)
- 返回
绝大多数业务项目不需要自己手写 impl,除非你要做非常底层的响应控制。
4.2 derive Responder:最常用的方式
Rocket 的 derive 很强,特别适合"包装已有 responder + 追加 header + 固定 status/content-type"。
rust
use rocket::http::{Header, ContentType};
#[derive(Responder)]
#[response(status = 500, content_type = "json")]
struct MyResponder {
inner: OtherResponder,
header: ContentType, // 作为 header 覆盖 content_type
more: Header<'static>,
#[response(ignore)]
unrelated: MyType, // 不参与响应构造
}
关键规则:
- 第一个字段会被当成 inner responder,用它来"完成 body"
- 其余字段(除非 ignore)会被当成 headers 加到 response 上
ContentType本身也是 header,所以你可以用字段动态设置 content-type- 想动态设置状态码?把 inner 做成
(Status, R):
rust
use rocket::http::{Header, Status};
#[derive(Responder)]
#[response(content_type = "json")]
struct MyResponder {
inner: (Status, OtherResponder),
some_header: Header<'static>,
}
4.3 enum 也能 derive:动态选择响应分支
这对"统一错误返回"非常实用:
rust
use rocket::http::{ContentType, Header, Status};
use rocket::fs::NamedFile;
#[derive(Responder)]
enum Error {
#[response(status = 500, content_type = "json")]
A(String),
#[response(status = 404)]
B(NamedFile, ContentType),
C {
inner: (Status, Option<String>),
header: ContentType,
}
}
你可以用一个 enum ApiResponse 把"成功、参数错误、权限错误、资源不存在、内部错误"等全部统一起来,业务层只返回这个 enum。
5. 标准库里最常用的 Responder:String、Option、Result
5.1 &str / String
- body:固定大小
- Content-Type:
text/plain
所以你可以直接这么写:
rust
#[get("/string")]
fn handler() -> &'static str {
"Hello there! I'm a string!"
}
5.2 Option<T>:None 自动变 404
Option<T> 是一个包装 responder:
Some(v):用v响应None:返回 404 Not Found
这对"找不到资源就 404"特别自然,例如文件服务:
rust
use rocket::fs::NamedFile;
use std::path::{Path, PathBuf};
#[get("/<file..>")]
async fn files(file: PathBuf) -> Option<NamedFile> {
NamedFile::open(Path::new("static/").join(file)).await.ok()
}
5.3 Result<T, E>:Ok/Err 各走各的 responder
Result<T, E> 允许你在运行时选择两套不同的响应:
rust
use rocket::fs::NamedFile;
use rocket::response::status::NotFound;
use std::path::{Path, PathBuf};
#[get("/<file..>")]
async fn files(file: PathBuf) -> Result<NamedFile, NotFound<String>> {
let path = Path::new("static/").join(file);
NamedFile::open(&path).await.map_err(|e| NotFound(e.to_string()))
}
这就比 Option 更强:不仅能 404,还能携带错误信息或自定义结构。
实战建议:
- "有/无资源"这种二元场景用
Option<T> - "失败要给出原因或不同错误形态"用
Result<T, E>
6. Rocket 常用内置 Responders 一览
你会在项目里频繁用到这些:
NamedFile:流式返回文件,自动根据扩展名设置 Content-TypeRedirect:重定向到另一个 URI(强烈建议配合uri!)content::*:覆写 Content-Typestatus::*:覆写 status codeFlash:设置一次性 flash cookie(被读取后移除)Json<T>:序列化结构体为 JSON(需要features = ["json"])MsgPack:序列化为 MessagePack(需要对应 feature)Template:渲染模板(来自 rocket_dyn_templates)
6.1 Json responder(序列化)
rust
use rocket::serde::{Serialize, json::Json};
#[derive(Serialize)]
#[serde(crate = "rocket::serde")]
struct Task { /* .. */ }
#[get("/todo")]
fn todo() -> Json<Task> {
Json(Task { /* .. */ })
}
特点:
- 自动设置 Content-Type 为 JSON
- body 是固定大小
- 序列化失败会返回 500
6.2 Template responder(模板渲染)
rust
use rocket_dyn_templates::Template;
#[get("/")]
fn index() -> Template {
Template::render("index", context! { foo: 123 })
}
#[launch]
fn rocket() -> _ {
rocket::build()
.mount("/", routes![index])
.attach(Template::fairing())
}
Rocket 会根据模板文件后缀选择引擎(.hbs 用 Handlebars,.tera 用 Tera),debug 下支持热重载。
7. Async Streams:SSE / 无限流输出
Rocket 支持把 async stream 当成响应体,适合做 SSE、日志流、进度推送等"单向实时通信"。
7.1 ReaderStream:从 AsyncRead 变成响应流
rust
use std::io;
use std::net::SocketAddr;
use rocket::tokio::net::TcpStream;
use rocket::response::stream::ReaderStream;
#[get("/stream")]
async fn stream() -> io::Result<ReaderStream![TcpStream]> {
let addr = SocketAddr::from(([127, 0, 0, 1], 9999));
let stream = TcpStream::connect(addr).await?;
Ok(ReaderStream::one(stream))
}
7.2 TextStream:generator 风格无限输出
rust
use rocket::tokio::time::{Duration, interval};
use rocket::response::stream::TextStream;
#[get("/infinite-hellos")]
fn hello() -> TextStream![&'static str] {
TextStream! {
let mut interval = interval(Duration::from_secs(1));
loop {
yield "hello";
interval.tick().await;
}
}
}
实践提醒:
- async handler 里不要阻塞线程,避免把 tokio worker 卡死
- SSE 场景建议关注客户端断开与优雅关闭(文档里有相关说明)
8. WebSockets:rocket_ws 提供一等支持
Rocket 通过 HTTP upgrade 支持 WebSocket,官方推荐用 rocket_ws crate。
最简单的 echo:
rust
use rocket_ws::{WebSocket, Stream};
#[get("/echo")]
fn echo_compose(ws: WebSocket) -> Stream!['static] {
ws.stream(|io| io)
}
或者用 generator 写法:
rust
use rocket_ws::{WebSocket, Stream};
#[get("/echo")]
fn echo_stream(ws: WebSocket) -> Stream!['static] {
Stream! { ws =>
for await message in ws {
yield message?;
}
}
}
实战里 WebSocket 常见组合是:
- 请求守卫做鉴权(Cookie/JWT/Header)
- 通过
Stream!循环处理消息 - 与广播通道(tokio::sync::broadcast)结合做聊天室
9. Typed URIs:用 uri! 生成类型安全链接
uri! 是 Rocket 非常值得用的能力:它能在编译期检查你的 URI 构造是否与路由声明匹配,并保证生成出来的 URI 是合法编码后的 URI。
给定路由:
rust
#[get("/<id>/<name>?<age>")]
fn person(id: Option<usize>, name: &str, age: Option<u8>) { /* .. */ }
生成 URI:
rust
let mike = uri!(person(101, "Mike Smith", Some(28)));
assert_eq!(mike.to_string(), "/101/Mike%20Smith?age=28");
支持命名参数,顺序无关:
rust
let mike = uri!(person(name = "Mike", id = 101, age = Some(28)));
assert_eq!(mike.to_string(), "/101/Mike?age=28");
支持指定 mount point:
rust
let mike = uri!("/api", person(id = 101, name = "Mike", age = Some(28)));
assert_eq!(mike.to_string(), "/api/101/Mike?age=28");
query 参数可忽略(用 _),但 path 参数不可忽略:
rust
let mike = uri!(person(101, "Mike", _));
assert_eq!(mike.to_string(), "/101/Mike");
如果参数数量或类型不匹配,直接编译报错,这点对"重构路由"非常友好。
实战建议:项目里构造内部链接、重定向、Location header,尽量只用 uri!,别手拼字符串。
10. UriDisplay 与 FromUriParam:让自定义类型也能进 uri!
如果你的自定义类型想出现在 URI 的 path 或 query 里,需要实现/派生 UriDisplay:
- 出现在 path:派生
UriDisplayPath - 出现在 query:派生
UriDisplayQuery
例如:
rust
use rocket::form::Form;
#[derive(FromForm, UriDisplayQuery)]
struct UserDetails<'r> {
age: Option<usize>,
nickname: &'r str,
}
#[post("/user/<id>?<details..>")]
fn add_user(id: usize, details: UserDetails) { /* .. */ }
这样你就能:
rust
let link = uri!(add_user(120, UserDetails { age: Some(20), nickname: "Bob".into() }));
assert_eq!(link.to_string(), "/user/120?age=20&nickname=Bob");
Rocket 还通过 FromUriParam 支持大量"可自动转换"的类型(而且转换是可传递的),比如:
&str -> String&str -> PathBufT -> Option<T>(仅 path 部分)- query 部分 Option/Result 之间的一些互转
T -> Form<T>等等
这就是为什么你常常能在 uri! 里直接塞 &str,即便路由参数声明的是 PathBuf 或 String。
11. 一些项目级最佳实践
-
用自己的
ApiResponseenum 统一返回把
Json<T>、status::Custom、错误结构体、headers 都封装进去,业务层只关心"返回哪个分支"。 -
认真区分 Option 与 Result
Option<T>表达"存在/不存在",天然 404Result<T, E>表达"成功/失败",更适合携带错误细节与多种错误形态
-
JSON 响应尽量用
Json<T>,不要手写 RawJson
RawJson适合快速返回一段已构造好的 JSON 字符串,但长期维护更推荐结构体 +Json<T>,类型更稳。 -
Redirect 一律配合
uri!路由改了路径但你忘记改重定向字符串,这是线上常见坑,
uri!能把它变成编译期错误。 -
Streaming/SSE/WebSocket 场景里避免阻塞
任何阻塞 IO 都要用
spawn_blocking或改 async 版本,否则会把吞吐压得很难看。