Rocket 0.5 响应体系Responder、流式输出、WebSocket 与 uri! 类型安全 URI

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>ApiErrCreated<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-Type
  • Redirect:重定向到另一个 URI(强烈建议配合 uri!
  • content::*:覆写 Content-Type
  • status::*:覆写 status code
  • Flash:设置一次性 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 -> PathBuf
  • T -> Option<T>(仅 path 部分)
  • query 部分 Option/Result 之间的一些互转
  • T -> Form<T> 等等

这就是为什么你常常能在 uri! 里直接塞 &str,即便路由参数声明的是 PathBufString

11. 一些项目级最佳实践

  1. 用自己的 ApiResponse enum 统一返回

    Json<T>status::Custom、错误结构体、headers 都封装进去,业务层只关心"返回哪个分支"。

  2. 认真区分 Option 与 Result

  • Option<T> 表达"存在/不存在",天然 404
  • Result<T, E> 表达"成功/失败",更适合携带错误细节与多种错误形态
  1. JSON 响应尽量用 Json<T>,不要手写 RawJson
    RawJson 适合快速返回一段已构造好的 JSON 字符串,但长期维护更推荐结构体 + Json<T>,类型更稳。

  2. Redirect 一律配合 uri!

    路由改了路径但你忘记改重定向字符串,这是线上常见坑,uri! 能把它变成编译期错误。

  3. Streaming/SSE/WebSocket 场景里避免阻塞

    任何阻塞 IO 都要用 spawn_blocking 或改 async 版本,否则会把吞吐压得很难看。

相关推荐
JoySSLLian2 小时前
IP SSL证书是什么?为何它是保障IP通信安全的关键?
网络协议·tcp/ip·https·ssl
阿钱真强道2 小时前
11 JetLinks MQTT 直连设备功能调用完整流程与 Python 实现
服务器·开发语言·网络·python·物联网·网络协议
shuangti2 小时前
告别食堂拥堵,爽提带来秩序新解
安全·美食·外卖
FreeBuf_3 小时前
黑客利用React Native CLI漏洞(CVE-2025-11953)在公开披露前部署Rust恶意软件
react native·react.js·rust
zt1985q3 小时前
本地部署静态网站生成工具 Vuepress 并实现外部访问
运维·服务器·网络·数据库·网络协议
手动阀行3 小时前
守护发布的最后一道防线:将自动化红队测试深度嵌入 CI/CD 流水线,筑牢 MCP 应用持续交付的安全底座
安全·ci/cd·自动化
Hubianji_093 小时前
[IOS]2026年网络安全、通信技术与计算机科学国际会议(ACCTCS 2026)
计算机网络·安全·web安全·ios·国际会议·国际期刊
鸿乃江边鸟3 小时前
Spark Datafusion Comet 向量化Rust Native--Native算子(CometNativeExec)怎么串联执行
大数据·rust·spark·native
mit6.8243 小时前
[]try catch no | result yes
rust