1、一个最常用的 Fairing:RequestId + 耗时 + 访问日志
目标效果:
- 每个请求生成一个唯一 RequestId(一次请求内只生成一次)
- 输出访问日志:方法、路径、状态码、耗时、request_id
- 在响应头里注入
X-Request-Id、X-Response-Time-ms
代码(直接可用):
rust
#[macro_use] extern crate rocket;
use rocket::{Request, Data, Response};
use rocket::fairing::{Fairing, Info, Kind};
use rocket::http::Header;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::time::Instant;
static REQ_SEQ: AtomicUsize = AtomicUsize::new(1);
struct ReqMeta {
id: usize,
start: Instant,
}
struct TraceAndLog;
#[rocket::async_trait]
impl Fairing for TraceAndLog {
fn info(&self) -> Info {
Info { name: "Trace + AccessLog", kind: Kind::Request | Kind::Response }
}
async fn on_request(&self, req: &mut Request<'_>, _: &mut Data<'_>) {
// request-local state:每个请求只生成一次
let meta = req.local_cache(|| ReqMeta {
id: REQ_SEQ.fetch_add(1, Ordering::Relaxed),
start: Instant::now(),
});
// 这里可以只打"入站日志",也可以不打,留到 on_response 打完整日志
let ip = req.client_ip().map(|x| x.to_string()).unwrap_or_else(|| "-".into());
println!("[IN ] id={} ip={} {} {}", meta.id, ip, req.method(), req.uri());
}
async fn on_response<'r>(&self, req: &'r Request<'_>, res: &mut Response<'r>) {
let meta = req.local_cache(|| ReqMeta {
id: REQ_SEQ.fetch_add(1, Ordering::Relaxed),
start: Instant::now(),
});
let elapsed_ms = meta.start.elapsed().as_millis();
res.set_header(Header::new("X-Request-Id", meta.id.to_string()));
res.set_header(Header::new("X-Response-Time-ms", elapsed_ms.to_string()));
println!(
"[OUT] id={} {} {} -> {} ({}ms)",
meta.id,
req.method(),
req.uri(),
res.status(),
elapsed_ms
);
}
}
#[get("/")]
fn index() -> &'static str {
"ok"
}
#[launch]
fn rocket() -> _ {
rocket::build()
.attach(TraceAndLog)
.mount("/", routes![index])
}
要点:
local_cache是关键:它让你在同一个请求生命周期里,多次取到同一个对象(避免重复生成 id/重复计时)。- Fairing 的执行顺序按 attach 顺序,多个 fairing 叠加时要注意"谁先注入,谁后覆盖"。
2、统一安全响应头:全站加固的"最小集合"
典型要加的头(按需选):
X-Content-Type-Options: nosniffX-Frame-Options: DENY或SAMEORIGINReferrer-PolicyContent-Security-Policy(CSP 要结合前端资源策略)Strict-Transport-Security(HSTS 只建议在 HTTPS 场景开启)
用 response fairing 一次性注入:
rust
use rocket::{Request, Response};
use rocket::fairing::{Fairing, Info, Kind};
use rocket::http::Header;
struct SecurityHeaders;
#[rocket::async_trait]
impl Fairing for SecurityHeaders {
fn info(&self) -> Info {
Info { name: "Security Headers", kind: Kind::Response }
}
async fn on_response<'r>(&self, _req: &'r Request<'_>, res: &mut Response<'r>) {
res.set_header(Header::new("X-Content-Type-Options", "nosniff"));
res.set_header(Header::new("X-Frame-Options", "DENY"));
res.set_header(Header::new("Referrer-Policy", "no-referrer"));
// CSP 示例:实际要按你站点资源加载策略调整
res.set_header(Header::new(
"Content-Security-Policy",
"default-src 'self'; object-src 'none'; frame-ancestors 'none'; base-uri 'self'"
));
// HSTS:只有你的站点稳定跑在 HTTPS 上才建议开启
// res.set_header(Header::new("Strict-Transport-Security", "max-age=31536000; includeSubDomains"));
}
}
3、CORS:Fairing 负责"注入头",OPTIONS 由路由负责"响应"
CORS 的坑点:浏览器的预检请求是 OPTIONS,如果你只在 Fairing 里加头但没有对应 OPTIONS 路由,还是会 404。
做法:
- Response Fairing:对所有响应加 CORS 头
- 一个兜底 OPTIONS 路由:专门处理预检
rust
#[macro_use] extern crate rocket;
use rocket::{Request, Response};
use rocket::fairing::{Fairing, Info, Kind};
use rocket::http::{Header, Status};
struct Cors;
#[rocket::async_trait]
impl Fairing for Cors {
fn info(&self) -> Info {
Info { name: "CORS", kind: Kind::Response }
}
async fn on_response<'r>(&self, _req: &'r Request<'_>, res: &mut Response<'r>) {
res.set_header(Header::new("Access-Control-Allow-Origin", "*"));
res.set_header(Header::new("Access-Control-Allow-Methods", "GET,POST,PUT,DELETE,OPTIONS"));
res.set_header(Header::new("Access-Control-Allow-Headers", "Content-Type,Authorization"));
res.set_header(Header::new("Access-Control-Max-Age", "86400"));
}
}
// 兜底预检:让所有 OPTIONS 都返回 204
#[options("/<_..>")]
fn all_options() -> Status {
Status::NoContent
}
#[get("/ping")]
fn ping() -> &'static str { "pong" }
#[launch]
fn rocket() -> _ {
rocket::build()
.attach(Cors)
.mount("/", routes![ping, all_options])
}
4、404 重写:给 SPA(前端路由)一个"始终返回 index.html"的后门
很多前端单页应用(React/Vue)刷新 /app/settings 会命中后端 404。你想要的其实是:后端把 404 重写成 index.html,让前端路由接管。
这类逻辑非常适合写在 response fairing 里:
rust
use rocket::{Request, Response};
use rocket::fairing::{Fairing, Info, Kind};
use rocket::http::{Status, ContentType};
use std::io::Cursor;
struct SpaFallback;
#[rocket::async_trait]
impl Fairing for SpaFallback {
fn info(&self) -> Info {
Info { name: "SPA 404 Fallback", kind: Kind::Response }
}
async fn on_response<'r>(&self, req: &'r Request<'_>, res: &mut Response<'r>) {
if res.status() != Status::NotFound {
return;
}
// 只对 GET 做 fallback,避免把 API 的 404 也吞掉
if req.method() != rocket::http::Method::Get {
return;
}
// 简单规则:/api 开头不重写(你可按需调整)
if req.uri().path().starts_with("/api") {
return;
}
let index_html = r#"<!doctype html><html><body><div id="app"></div></body></html>"#;
res.set_status(Status::Ok);
res.set_header(ContentType::HTML);
res.set_sized_body(index_html.len(), Cursor::new(index_html));
}
}
生产建议:index_html 真实项目里不要硬编码,应该用 NamedFile 或构建产物目录读取。
5、启动阶段 on_ignite:校验配置 + 注入 Managed State
Ignite 阶段是"应用还没真正启动监听端口"的时候,你可以:
- 从配置里取值
- 做合法性校验,不合法就阻止启动
- 把解析后的结果塞进
manage()的状态里供业务使用
用 AdHoc 写法最省事:
rust
use rocket::fairing::AdHoc;
#[derive(Clone)]
struct MyConfig {
service_name: String,
}
fn config_fairing() -> AdHoc {
AdHoc::on_ignite("Config Validator", |rocket| Box::pin(async move {
// 这里演示从环境变量读取,实际也可以用 figment extract
let service_name = std::env::var("SERVICE_NAME").unwrap_or_else(|_| "rocket-app".into());
if service_name.trim().is_empty() {
panic!("SERVICE_NAME is empty, abort launch");
}
rocket.manage(MyConfig { service_name })
}))
}
挂载:
rust
rocket::build().attach(config_fairing())
这种写法很适合"必须要有某个配置才能启动"的场景:数据库 DSN、外部依赖地址、加密密钥等。
6、liftoff / shutdown:启动伴生任务与优雅收尾
你经常会在服务启动后做一些事:
- 打印启动信息
- 启动一个后台协程(例如定时刷新缓存、订阅消息)
- 停机时通知外部系统、flush 日志等
AdHoc 版:
rust
use rocket::fairing::AdHoc;
fn liftoff_and_shutdown() -> AdHoc {
AdHoc::on_liftoff("Liftoff Hook", |_| Box::pin(async move {
println!("Rocket launched, start background workers...");
}))
}
再加一个 shutdown:
rust
fn shutdown_hook() -> AdHoc {
AdHoc::on_shutdown("Shutdown Hook", |_| Box::pin(async move {
println!("Shutdown started, do cleanup...");
}))
}
注意:shutdown fairings 会并发执行,Rocket 会等待它们完成再继续关停流程。
7、别用 Fairing 做鉴权:给你一个"正确的混合姿势"
你可能想:能不能在 on_request 里检查 token,没有就拒绝?
不行,Fairing 不能直接响应或终止请求。
正确姿势是:
- Fairing:注入 request_id、打点、统一头
- Guard:做鉴权/授权并决定 forward/deny
- Handler:只写业务
这样你的鉴权还能做到"只对某些路由生效",而不是全站硬塞。、