核心内容摘要
日本护土体内谢精事件持续发酵引发社会广泛关注
Responder 是什么Responder的职责是把一个 Rust 值转成真正的 HTTP 响应Response。
一个Response通常包括HTTP Status状态码Headers响应头Body响应体可能是固定大小也可能是流式实现Responder的类型可以根据 incomingRequest动态调整响应。
例如同一个 responder 可以根据Accept头返回不同格式。
一个典型的例子是String它会生成Content-Type: text/plain并把字符串作为“固定大小的 body”返回。
而像文件类型例如NamedFile通常是“流式响应”。
“包装型 responder”用组合拼出你要的响应Rocket 特别鼓励你用“包装wrapping”的方式构造响应structWrappingResponderR(R);// R: Responder包装 responder 做的事通常是先让内部 responder 生成响应再修改 status 或 header然后返回。
1 改状态码status 模块例如status::Accepted会把状态码固定成 202userocket::response::status;#[post(/id)]fnnew(id:usize)-status::AcceptedString{status::Accepted(format!(id: {},id))}
2 改 Content-Typecontent 模块例如content::RawJson可以把内容标记为 JSON注意这不是JsonT序列化那个userocket::http::Status;userocket::response::{content,status};#[get(/)]fnjson()-status::Customcontent::RawJsonstaticstr{status::Custom(Status::ImATeapot,content::RawJson({ \hi\: \world\ }))}
3 直接用 tuple responder 覆写 Status / Content-TypeRocket 内置了(Status, R)和(ContentType, R)这样的 responderR: Responder用起来很顺手userocket::http::{Status,ContentType};#[get(/)]fnjson()-(Status,(ContentType,staticstr)){(Status::ImATeapot,(ContentType::JSON,{ \hi\: \world\ }))}
4 写一个“可复用的响应类型”derive Responder如果你在项目里经常要返回某种固定格式比如固定 status content-type建议封装成自己的 responder#[derive(Responder)]#[response(status 418, content_type json)]structRawTeapotJson(staticstr);#[get(/)]fnjson()-RawTeapotJson{RawTeapotJson({ \hi\: \world\ })}这种写法非常适合做“统一响应封装”比如ApiOkT、ApiErr、CreatedT等。
Responder 也可能失败错误 catcher 会接管Responder 不一定总能生成响应它可以返回Err(Status)表示“我失败了”。
Rocket 遇到这种情况会将请求交给对应状态码的 error catcher如果没有注册 catcher使用默认 catcher通常返回 HTML 错误页或根据 Accept 返回 JSON如果是自定义状态码且无 catcher通常会落到 500 catcher
1 直接返回 Status 也能触发 catcher不推荐但可用你甚至可以让 handler 直接返回Statususerocket::http::Status;#[get(/)]fnjust_fail()-Status{Status::NotAcceptable}规则大致是400–599转发到对应 catcher100 与 200–205返回空 body 对应状态其他视为无效转到 500 catcher实战里更建议用ResultT, E或status::Custom显式表达你的意图。
自定义 Responder手写 impl vs derive
1 手写实现你很少需要文档里给了String的实现例子本质上就是 build 一个Response设置 header设置 bodysized 或 stream返回绝大多数业务项目不需要自己手写 impl除非你要做非常底层的响应控制。
2 derive Responder最常用的方式Rocket 的 derive 很强特别适合“包装已有 responder 追加 header 固定 status/content-type”。
userocket::http::{Header,ContentType};#[derive(Responder)]#[response(status 500, content_type json)]structMyResponder{inner:OtherResponder,header:ContentType,// 作为 header 覆盖 content_typemore:Headerstatic,#[response(ignore)]unrelated:MyType,// 不参与响应构造}关键规则第一个字段会被当成 inner responder用它来“完成 body”其余字段除非 ignore会被当成 headers 加到 response 上ContentType本身也是 header所以你可以用字段动态设置 content-type想动态设置状态码把 inner 做成(Status, R)userocket::http::{Header,Status};#[derive(Responder)]#[response(content_type json)]structMyResponder{inner:(Status,OtherResponder),some_header:Headerstatic,}
3 enum 也能 derive动态选择响应分支这对“统一错误返回”非常实用userocket::http::{ContentType,Header,Status};userocket::fs::NamedFile;#[derive(Responder)]enumError{#[response(status 500, content_type json)]A(String),#[response(status
]B(NamedFile,ContentType),C{inner:(Status,OptionString),header:ContentType,}}你可以用一个enum ApiResponse把“成功、参数错误、权限错误、资源不存在、内部错误”等全部统一起来业务层只返回这个 enum。
标准库里最常用的 ResponderString、Option、Result
1str/Stringbody固定大小Content-Typetext/plain所以你可以直接这么写#[get(/string)]fnhandler()-staticstr{Hello there! Im a string!}
2OptionTNone 自动变 404OptionT是一个包装 responderSome(v)用v响应None返回 404 Not Found这对“找不到资源就 404”特别自然例如文件服务userocket::fs::NamedFile;usestd::path::{Path,PathBuf};#[get(/file..)]asyncfnfiles(file:PathBuf)-OptionNamedFile{NamedFile::open(Path::new(static/).join(file)).await.ok()}
3ResultT, EOk/Err 各走各的 responderResultT, E允许你在运行时选择两套不同的响应userocket::fs::NamedFile;userocket::response::status::NotFound;usestd::path::{Path,PathBuf};#[get(/file..)]asyncfnfiles(file:PathBuf)-ResultNamedFile,NotFoundString{letpathPath::new(static/).join(file);NamedFile::open(path).await.map_err(|e|NotFound(e.to_string()))}这就比Option更强不仅能 404还能携带错误信息或自定义结构。
实战建议“有/无资源”这种二元场景用OptionT“失败要给出原因或不同错误形态”用ResultT, E
Rocket 常用内置 Responders 一览你会在项目里频繁用到这些NamedFile流式返回文件自动根据扩展名设置 Content-TypeRedirect重定向到另一个 URI强烈建议配合uri!content::*覆写 Content-Typestatus::*覆写 status codeFlash设置一次性 flash cookie被读取后移除JsonT序列化结构体为 JSON需要features [json]MsgPack序列化为 MessagePack需要对应 featureTemplate渲染模板来自 rocket_dyn_templates
1 Json responder序列化userocket::serde::{Serialize,json::Json};#[derive(Serialize)]#[serde(crate rocket::serde)]structTask{/* .. */}#[get(/todo)]fntodo()-JsonTask{Json(Task{/* .. */})}特点自动设置 Content-Type 为 JSONbody 是固定大小序列化失败会返回
5
2 Template responder模板渲染userocket_dyn_templates::Template;#[get(/)]fnindex()-Template{Template::render(index,context!{foo:123})}#[launch]fnrocket()-_{rocket::build().mount(/,routes![index]).attach(Template::fairing())}Rocket 会根据模板文件后缀选择引擎.hbs用 Handlebars.tera用 Teradebug 下支持热重载。
Async StreamsSSE / 无限流输出Rocket 支持把 async stream 当成响应体适合做 SSE、日志流、进度推送等“单向实时通信”。
1 ReaderStream从 AsyncRead 变成响应流usestd::io;usestd::net::SocketAddr;userocket::tokio::net::TcpStream;userocket::response::stream::ReaderStream;#[get(/stream)]asyncfnstream()-io::ResultReaderStream![TcpStream]{letaddrSocketAddr::from(([127,0,0,1],
);letstreamTcpStream::connect(addr).await?;Ok(ReaderStream::one(stream))}
2 TextStreamgenerator 风格无限输出userocket::tokio::time::{Duration,interval};userocket::response::stream::TextStream;#[get(/infinite-hellos)]fnhello()-TextStream![staticstr]{TextStream!{letmutintervalinterval(Duration::from_secs(
);loop{yieldhello;interval.tick().await;}}}实践提醒async handler 里不要阻塞线程避免把 tokio worker 卡死SSE 场景建议关注客户端断开与优雅关闭文档里有相关说明
WebSocketsrocket_ws 提供一等支持Rocket 通过 HTTP upgrade 支持 WebSocket官方推荐用rocket_wscrate。
最简单的 echouserocket_ws::{WebSocket,Stream};#[get(/echo)]fnecho_compose(ws:WebSocket)-Stream![static]{ws.stream(|io|io)}或者用 generator 写法userocket_ws::{WebSocket,Stream};#[get(/echo)]fnecho_stream(ws:WebSocket)-Stream![static]{Stream!{wsforawaitmessageinws{yieldmessage?;}}}实战里 WebSocket 常见组合是请求守卫做鉴权Cookie/JWT/Header通过Stream!循环处理消息与广播通道tokio::sync::broadcast结合做聊天室
Typed URIs用 uri! 生成类型安全链接uri!是 Rocket 非常值得用的能力它能在编译期检查你的 URI 构造是否与路由声明匹配并保证生成出来的 URI 是合法编码后的 URI。
给定路由#[get(/id/name?age)]fnperson(id:Optionusize,name:str,age:Optionu
{/* .. */}生成 URIletmikeuri!(person(101,Mike Smith,Some(
));assert_eq!(mike.to_string(),/101/Mike%20Smith?age
;支持命名参数顺序无关letmikeuri!(person(nameMike,id101,ageSome(
));assert_eq!(mike.to_string(),/101/Mike?age
;支持指定 mount pointletmikeuri!(/api,person(id101,nameMike,ageSome(
));assert_eq!(mike.to_string(),/api/101/Mike?age
;query 参数可忽略用_但 path 参数不可忽略letmikeuri!(person(101,Mike,_));assert_eq!(mike.to_string(),/101/Mike);如果参数数量或类型不匹配直接编译报错这点对“重构路由”非常友好。
实战建议项目里构造内部链接、重定向、Location header尽量只用uri!别手拼字符串。
UriDisplay 与 FromUriParam让自定义类型也能进 uri!如果你的自定义类型想出现在 URI 的 path 或 query 里需要实现/派生UriDisplay出现在 path派生UriDisplayPath出现在 query派生UriDisplayQuery例如userocket::form::Form;#[derive(FromForm, UriDisplayQuery)]structUserDetailsr{age:Optionusize,nickname:rstr,}#[post(/user/id?details..)]fnadd_user(id:usize,details:UserDetails){/* .. */}这样你就能letlinkuri!(add_user(120,UserDetails{age:Some(
,nickname:Bob.into()}));assert_eq!(link.to_string(),/user/120?age20nicknameBob);Rocket 还通过FromUriParam支持大量“可自动转换”的类型而且转换是可传递的比如str - Stringstr - PathBufT - OptionT仅 path 部分query 部分 Option/Result 之间的一些互转T - FormT等等这就是为什么你常常能在uri!里直接塞str即便路由参数声明的是PathBuf或String。
一些项目级最佳实践用自己的ApiResponseenum 统一返回把JsonT、status::Custom、错误结构体、headers 都封装进去业务层只关心“返回哪个分支”。
认真区分 Option 与 ResultOptionT表达“存在/不存在”天然 404ResultT, E表达“成功/失败”更适合携带错误细节与多种错误形态JSON 响应尽量用JsonT不要手写 RawJsonRawJson适合快速返回一段已构造好的 JSON 字符串但长期维护更推荐结构体 JsonT类型更稳。
Redirect 一律配合uri!路由改了路径但你忘记改重定向字符串这是线上常见坑uri!能把它变成编译期错误。
Streaming/SSE/WebSocket 场景里避免阻塞任何阻塞 IO 都要用spawn_blocking或改 async 版本否则会把吞吐压得很难看。