请求
route 属性和函数签名共同指定了关于请求的哪些条件必须成立,以便调用路由的处理程序。你已经看到了一个实际的例子:
#[get("/world")]fn handler() { }
这个路由表示它只匹配 /world 路径的 GET 请求。Rocket 在调用 handler 之前确保这一点。当然,除了指定请求的方法和路径外,你还可以要求 Rocket 自动验证:
-
动态路径段的类型。 -
多个动态路径段的类型。 -
传入正文数据的类型。 -
查询字符串、表单和表单值的类型。 -
请求的预期传入或传出格式。 -
任意用户定义的安全或验证策略。
路由属性和函数签名协同工作以描述这些验证。Rocket 的代码生成负责实际验证这些属性。本节描述如何要求 Rocket 验证所有这些属性以及更多。
方法
Rocket 路由属性可以是 get、put、post、delete、head、patch 或 options 之一,每个对应于要匹配的 HTTP 方法。例如,以下属性将匹配根路径的 POST 请求:
这些属性的语法在 route API 文档中有正式定义。
HEAD 请求
当存在匹配的 GET 路由时,Rocket 会自动处理 HEAD 请求。它通过剥离响应体(如果有)来实现。你也可以通过为 HEAD 声明路由来专门处理它;Rocket 不会干扰应用程序显式处理的 HEAD 请求。
重新解释
由于 Web 浏览器仅支持提交 HTML 表单为 GET 或 POST 请求,Rocket 在某些条件下会重新解释请求方法。如果 POST 请求包含 Content-Type: application/x-www-form-urlencoded 的正文,并且表单的第一个字段名为 _method 且其值为有效的 HTTP 方法名(如 "PUT"),则该字段的值用作传入请求的方法。这允许 Rocket 应用程序提交非 POST 表单。待办事项示例利用此功能从 Web 表单提交 PUT 和 DELETE 请求。
动态路径
你可以在路由的路径中使用尖括号包围变量名来声明路径段为动态的。例如,如果我们想向任何事物问好,不仅仅是世界,我们可以声明如下路由:
#[get("/hello/<name>")]fn hello(name: &str) -> String {format!("Hello, {}!", name)}
如果我们在根路径挂载路径(.mount("/", routes![hello])),则任何具有两个非空段的路径请求,其中第一段是 hello,都将被分派到 hello 路由。例如,如果我们访问 /hello/John,应用程序将响应 Hello, John!。
允许任意数量的动态路径段。路径段可以是任何类型,包括你自己的类型,只要该类型实现了 FromParam trait。我们称这些类型为参数守卫。Rocket 为许多标准库类型以及一些特殊的 Rocket 类型实现了 FromParam。有关提供的实现的完整列表,请参见 FromParam API 文档。以下是一个更完整的路由示例,说明了不同的用法:
#[get("/hello/<name>/<age>/<cool>")]fn hello(name: &str, age: u8, cool: bool) -> String {if cool {format!("You're a cool {} year old, {}!", age, name)} else {format!("{}, we need to talk about your coolness.", name)}}
多个段
你还可以使用 <param..> 在路由路径中匹配多个段。此类参数的类型,称为段守卫,必须实现 FromSegments。段守卫必须是路径的最后一个组件:段守卫后的任何文本都会导致编译时错误。
作为示例,以下路由匹配所有以 /page 开头的路径:
use std::path::PathBuf;#[get("/page/<path..>")]fn get_page(path: PathBuf) { }
在 /page/ 之后的路径将在 path 参数中可用,对于简单为 /page、/page/、/page// 等路径,该参数可能为空。PathBuf 的 FromSegments 实现确保 path 不会导致路径遍历攻击。借此,可以在仅 4 行内实现一个安全可靠的静态文件服务器:
use std::path::{Path, PathBuf};use rocket::fs::NamedFile;#[get("/<file..>")]async fn files(file: PathBuf) -> Option<NamedFile> {NamedFile::open(Path::new("static/").join(file)).await.ok()}
Rocket 甚至使提供静态文件变得更容易!
如果你需要从 Rocket 应用程序提供静态文件,可以考虑使用 FileServer,它非常简单:
use rocket::fs::FileServer;#[launch]fn rocket() -> _ {rocket::build().mount("/public", FileServer::from("/www/static"))}
忽略的段
通过使用 <_> 可以完全忽略路由的一个组件,通过使用 <_..> 可以忽略多个组件。换句话说,通配符名称 _ 是一个忽略该动态参数的动态参数名称。被忽略的参数不得出现在函数参数列表中。声明为 <_> 的段匹配任何单段,而声明为 <_..> 的段匹配任意数量的段,无条件。
作为示例,以下 foo_bar 路由匹配任何以 /foo/ 开头且以 /bar 结尾的 3 段 URI 的 GET 请求。以下 everything 路由匹配每个 GET 请求。
#[get("/foo/<_>/bar")]fn foo_bar() -> &'static str {"Foo _____ bar!"}#[get("/<_..>")]fn everything() -> &'static str {"Hey, you're here."}
转发
让我们更仔细地看看之前示例中的这个路由属性和签名对:
#[get("/hello/<name>/<age>/<cool>")]fn hello(name: &str, age: u8, cool: bool) { }
如果 cool 不是 bool 会怎样?或者,如果 age 不是 u8 会怎样?当参数类型不匹配时,Rocket 将请求转发到下一个匹配的路由(如果有)。这会持续直到路由成功或失败,或者没有其他匹配的路由可尝试。当没有剩余路由时,将调用与最后转发守卫设置的状态相关联的错误捕获器。
路由按递增的等级顺序尝试。每个路由都有关联的等级。如果没有指定,Rocket 会根据路由的颜色选择一个默认等级,以避免冲突,这将在下一节中详细说明,但路由的等级也可以通过 rank 属性手动设置。为了说明,考虑以下路由:
#[get("/user/<id>")]fn user(id: usize) { }#[get("/user/<id>", rank = 2)]fn user_int(id: isize) { }#[get("/user/<id>", rank = 3)]fn user_str(id: &str) { }#[launch]fn rocket() -> _ {rocket::build().mount("/", routes![user, user_int, user_str])}
注意 user_int 和 user_str 中的 rank 参数。如果我们在根路径挂载此应用程序,如上在 rocket() 中所做的,对 /user/<id>(如 /user/123、/user/Bob 等)的请求将按以下方式路由:
-
user路由首先匹配。如果在<id>位置的字符串是无符号整数,则调用user处理程序。如果不是,则请求被转发到下一个匹配的路由:user_int。 -
user_int路由接下来匹配。如果<id>是有符号整数,则调用user_int。否则,请求被转发。 -
user_str路由最后匹配。由于<id>总是字符串,路由总是匹配。调用user_str处理程序。
路由的等级出现在启动期间的**[方括号]**中。
你还会在应用程序启动期间的括号中找到记录的路由等级:GET /user/<id> [3] (user_str)。
转发可以被 Result 或 Option 类型捕获。例如,如果 user 函数中的 id 类型是 Result<usize, &str>,则 user 永远不会转发。Ok 变量表示 <id> 是有效的 usize,而 Err 表示 <id> 不是 usize。Err 的值将包含未能解析为 usize 的字符串。
不仅仅是转发可以被捕获!
通常,当任何守卫因任何原因失败时,包括参数守卫,你可以在其位置使用 Option 或 Result 类型来捕获错误。
顺便说一下,如果你省略 user_str 或 user_int 路由中的 rank 参数,Rocket 会发出错误并中止启动,表明路由冲突,或者可以匹配类似的传入请求。rank 参数解决了这种冲突。
默认排名
如果没有显式指定等级,Rocket 会给路由分配一个默认等级。默认等级范围从 -12 到 -1。这与可以在路由属性中手动分配的等级不同,后者必须是正数。默认等级在路径和查询中更偏向于静态段而不是动态段:路由的路径和查询越静态,其优先级越高。
每个路径和查询可以分配有三种“颜色”:
-
static,意味着所有组件都是静态的 -
partial,意味着至少一个组件是动态的 -
wild,意味着所有组件都是动态的
此外,查询可能没有颜色(none),如果没有查询。
静态路径比静态查询更重要。对于部分和通配路径也是如此。这导致了以下默认排名表:
| 路径颜色 | 查询颜色 | 默认等级 |
|---|---|---|
| static | static | -12 |
| static | partial | -11 |
| static | wild | -10 |
| static | none | -9 |
| partial | static | -8 |
| partial | partial | -7 |
| partial | wild | -6 |
| partial | none | -5 |
| wild | static | -4 |
| wild | partial | -3 |
| wild | wild | -2 |
| wild | none | -1 |
回想一下,较低的等级具有较高的优先级。例如,考虑之前的这个应用程序:
#[get("/foo/<_>/bar")]fn foo_bar() { }#[get("/<_..>")]fn everything() { }
默认排名确保具有“部分”路径颜色的 foo_bar 比具有“通配”路径颜色的 everything 具有更高的优先级。这个默认排名防止了原本会发生的 routing 冲突。
请求守卫
请求守卫是 Rocket 最强大的工具之一。正如名字可能暗示的,请求守卫根据传入请求中包含的信息保护处理程序不被错误调用。更具体地说,请求守卫是表示任意验证策略的类型。验证策略通过 FromRequest trait 实现。每个实现 FromRequest 的类型都是请求守卫。
请求守卫作为输入出现在处理程序中。任意数量的请求守卫可以作为参数出现在路由处理程序中。Rocket 会在调用处理程序之前自动调用请求守卫的 FromRequest 实现。只有当所有守卫都通过时,Rocket 才会将请求分派给处理程序。
例如,以下虚拟处理程序使用了三个请求守卫 A、B 和 C。如果输入未在路由属性中命名,则可以被识别为请求守卫。
#[get("/<param>")]fn index(param: isize, a: A, b: B, c: C) { }
请求守卫总是按从左到右的声明顺序触发。在上面的示例中,顺序将是 A 后跟 B 后跟 C。错误是短路的;如果一个守卫失败,剩余的不会尝试。要了解更多关于请求守卫及其实现的信息,请参见 FromRequest 文档。
自定义守卫
你可以为自己的类型实现 FromRequest。例如,为了保护 sensitive 路由不运行,除非请求头中存在 ApiKey,你可以创建一个实现 FromRequest 的 ApiKey 类型,然后将其用作请求守卫:
#[get("/sensitive")]fn sensitive(key: ApiKey) { }
你还可以为 AdminUser 类型实现 FromRequest,该类型使用传入的 cookie 对管理员进行身份验证。然后,任何在其参数列表中具有 AdminUser 或 ApiKey 类型的处理程序都确保仅在满足适当条件时才被调用。请求守卫集中策略,从而实现更简单、更安全、更安全的应用程序。
守卫透明性
当请求守卫类型只能通过其 FromRequest 实现创建,并且该类型不是 Copy 时,请求守卫值的存在提供了当前请求已针对任意策略验证的类型级别证明。这提供了强大的手段,通过要求数据访问方法通过请求守卫见证授权证明来保护应用程序免受访问控制违规。我们使用请求守卫作为见证的概念称为守卫透明性。
作为一个具体示例,以下应用程序有一个函数 health_records,它返回数据库中的所有健康记录。由于健康记录是敏感信息,它们应该只能被超级用户访问。SuperUser 请求守卫对超级用户进行身份验证和授权,其 FromRequest 实现是构造 SuperUser 的唯一手段。通过如下声明 health_records 函数,可以确保在编译时防止对健康记录的访问控制违规:
fn health_records(user: &SuperUser) -> Records { }
推理如下:
-
health_records函数需要&SuperUser类型。 -
SuperUser类型的唯一构造函数是FromRequest。 -
只有 Rocket 可以提供活动的 &Request以通过FromRequest构造。 -
因此,必须有一个 Request授权SuperUser调用health_records。
以守卫类型的生命周期参数为代价,可以通过将传递给 FromRequest 的 Request 的生命周期与请求守卫关联起来,使保证更强,确保守卫值总是对应于一个活动请求。
我们建议利用请求透明性来所有数据访问。
转发守卫
请求守卫和转发是执行策略的强大组合。为了说明,我们考虑如何使用这些机制实现一个简单的授权系统。
我们从两个请求守卫开始:
-
User:一个普通的、经过身份验证的用户。User的FromRequest实现检查 cookie 是否识别用户,如果是,则返回User值。如果无法验证任何用户,守卫将以 401 未授权状态转发。 -
AdminUser:被身份验证为管理员的用户。AdminUser的FromRequest实现检查 cookie 是否识别管理用户,如果是,则返回AdminUser值。如果无法验证任何用户,守卫将以 401 未授权状态转发。
我们现在结合使用这两个守卫和转发来实现以下三个路由,每个路由都通向 /admin 的管理控制面板:
use rocket::response::Redirect;#[get("/login")]fn login() -> Template { }#[get("/admin")]fn admin_panel(admin: AdminUser) -> &'static str {"Hello, administrator. This is the admin panel!"}#[get("/admin", rank = 2)]fn admin_panel_user(user: User) -> &'static str {"Sorry, you must be an administrator to access this page."}#[get("/admin", rank = 3)]fn admin_panel_redirect() -> Redirect {Redirect::to(uri!(login))}
上面的三个路由编码了身份验证和授权。admin_panel 路由仅在管理员登录时才成功。只有这样,才会显示管理面板。如果用户不是管理员,AdminUser 守卫将转发。由于 admin_panel_user 路由的排名次高,因此会尝试此路由。如果有任何用户登录,则此路由成功,并显示授权错误消息。最后,如果用户未登录,则尝试 admin_panel_redirect 路由。由于此路由没有守卫,因此总是成功。用户将被重定向到登录页面。
可失败守卫
失败或转发的守卫可以在处理程序中被“捕获”,防止其失败或转发,通过 Option<T> 和 Result<T, E> 守卫。当守卫 T 失败或转发时,Option<T> 将为 None。如果守卫 T 以错误 E 失败,Result<T, E> 将为 Err(E)。
例如,对于上面的 User 守卫,我们可能不想允许守卫在 admin_panel_user 中转发,而是想检测它并动态处理:
use rocket::response::Redirect;#[get("/admin", rank = 2)]fn admin_panel_user(user: Option<User>) -> Result<&'static str, Redirect> {let user = user.ok_or_else(|| Redirect::to(uri!(login)))?;Ok("Sorry, you must be an administrator to access this page.")}
如果 User 守卫转发或失败,Option 将为 None。如果它成功,它将为 Some(User)。
对于可能失败(不仅仅是转发)的守卫,Result<T, E> 守卫允许在出错时检索错误类型 E。例如,当 mtls::Certificate 类型失败时,它会在 mtls::Error 类型中报告原因。可以通过使用 Result<Certificate, Error> 守卫在处理程序中检索该值:
use rocket::mtls;#[get("/login")]fn login(cert: Result<mtls::Certificate, mtls::Error>) {match cert {Ok(cert) => {},Err(e) => {},}}
需要注意的是,Result<T, E> 在 T 转发时也会转发。然而,你可以链接两个捕获响应者来确定守卫 T 是否转发或失败,并在失败时检索错误,使用 Option<Result<T, E>>:
use rocket::mtls;#[get("/login")]fn login(cert: Option<Result<mtls::Certificate, mtls::Error>>) {match cert {Some(Ok(cert)) => {},Some(Err(e)) => {},None => {},}}
Cookie
CookieJar 的引用是一个重要的内置请求守卫:它允许你获取、设置和删除 cookie。由于 &CookieJar 是一个请求守卫,其类型的参数可以简单地添加到处理程序:
use rocket::http::CookieJar;#[get("/")]fn index(cookies: &CookieJar<'_>) -> Option<String> {cookies.get("message").map(|crumb| format!("Message: {}", crumb.value()))}
这导致可以从处理程序访问传入请求的 cookie。上面的示例检索名为 message 的 cookie。还可以使用 CookieJar 守卫设置和删除 cookie。GitHub 上的 cookie 示例进一步说明了 CookieJar 类型的使用,而 CookieJar 文档包含完整的使用信息。
私有 Cookie
通过 CookieJar::add() 方法添加的 cookie 会以明文设置。换句话说,设置的值对客户端是可见的。对于敏感数据,Rocket 提供了私有 cookie。私有 cookie 与常规 cookie 类似,只是它们使用经过身份验证的加密进行加密,这种加密同时提供机密性、完整性和真实性。因此,客户端无法检查、篡改或制造私有 cookie。如果你愿意,可以将私有 cookie 视为已签名和加密。
必须通过 secrets crate 功能手动启用对私有 cookie 的支持:
rocket = { version = "0.5.1", features = ["secrets"] }
用于检索、添加和删除私有 cookie 的 API 与常规 cookie 相同,只是大多数方法的后缀为 _private。这些方法是:get_private、add_private 和 remove_private。以下是它们的用法示例:
use rocket::http::{Cookie, CookieJar};use rocket::response::{Flash, Redirect};#[get("/user_id")]fn user_id(cookies: &CookieJar<'_>) -> Option<String> {cookies.get_private("user_id").map(|crumb| format!("User ID: {}", crumb.value()))}#[post("/logout")]fn logout(cookies: &CookieJar<'_>) -> Flash<Redirect> {cookies.remove_private("user_id");Flash::success(Redirect::to("/"), "Successfully logged out.")}
密钥
为了加密私有 cookie,Rocket 使用在 secret_key 配置参数中指定的 256 位密钥。在调试模式下编译时,会自动生成一个新密钥。在发布模式下,如果启用了 secrets 功能,Rocket 要求你设置一个密钥。未能这样做会导致启动时出现硬错误。参数的值可以是 256 位 base64 或十六进制字符串,或 32 字节切片。
生成适合用作 secret_key 配置值的字符串通常是通过 openssl 等工具完成的。使用 openssl,可以使用命令 openssl rand -base64 32 生成 256 位 base64 密钥。
有关配置的更多信息,请参见指南的配置部分。
格式
路由可以通过使用 format 路由参数来指定它愿意接受或响应的数据格式。参数的值是一个标识 HTTP 媒体类型的字符串或简写变体。例如,对于 JSON 数据,可以使用字符串 application/json 或简单的 json。
当路由指示支持有效负载的方法(PUT、POST、DELETE 和 PATCH)时,format 路由参数指示 Rocket 检查传入请求的 Content-Type 头。只有那些 Content-Type 头与 format 参数匹配的请求才会匹配到路由。
作为一个示例,考虑以下路由:
#[post("/user", format = "application/json", data = "<user>")]fn new_user(user: User) { }
post 属性中的 format 参数声明只有那些 Content-Type: application/json 的传入请求才会匹配 new_user。(data 参数在下一节中描述。)对于最常见的 format 参数,也支持简写。你可以使用简写如 format = "json" 而不是完整的 Content-Type format = "application/json"。有关可用简写的完整列表,请参见 ContentType::parse_flexible() 文档。
当路由指示不支持有效负载的方法(GET、HEAD、OPTIONS)时,format 路由参数指示 Rocket 检查传入请求的 Accept 头。只有那些 Accept 头中的首选媒体类型与 format 参数匹配的请求才会匹配到路由。
作为一个示例,考虑以下路由:
#[get("/user/<id>", format = "json")]fn user(id: usize) -> User { }
get 属性中的 format 参数声明只有那些 Accept 头中的首选媒体类型为 application/json 的传入请求才会匹配 user。如果路由被声明为 post,Rocket 会将 format 与传入响应的 Content-Type 头匹配。
正文数据
正文数据处理与 Rocket 的大部分一样,是类型导向的。为了指示处理程序期望正文数据,请使用 data = "<param>" 进行注释,其中 param 是处理程序中的一个参数。参数的类型必须实现 FromData trait。它看起来像这样,其中 T 被假定为实现 FromData:
#[post("/", data = "<input>")]fn new(input: T) { }
任何实现 FromData 的类型也被称为数据守卫。
JSON
Json<T> 守卫将正文数据反序列化为 JSON。唯一的条件是泛型类型 T 实现了 serde 中的 Deserialize trait。
use rocket::serde::{Deserialize, json::Json};#[derive(Deserialize)]#[serde(crate = "rocket::serde")]struct Task<'r> {description: &'r str,complete: bool}#[post("/todo", data = "<task>")]fn new(task: Json<Task<'_>>) { }
使用 Rocket 的 serde 派生重导出需要多一点努力。
为了方便,Rocket 从 rocket::serde 重导出 serde 的 Serialize 和 Deserialize trait 以及派生宏。然而,由于 Rust 对派生宏重导出的支持有限,使用重导出的派生宏需要给结构体标注 #[serde(crate = "rocket::serde")]。如果你希望避免这个额外的注释,你必须直接通过你的 crate 的 Cargo.toml 依赖 serde:
serde = { version = "1.0", features = ["derive"] }
我们总是在指南中使用额外的注释,但你可能更喜欢替代方案。
GitHub 上的 JSON 示例提供了一个完整的示例。
JSON 支持需要启用 Rocket 的 json 功能标志。
Rocket 有意将 JSON 支持以及其他数据格式和功能支持放在功能标志之后。有关可用功能的列表,请参见 api 文档。可以在 Cargo.toml 中启用 json 功能:
rocket = { version = "0.5.1", features = ["json"] }
临时文件
TempFile 数据守卫直接将数据流式传输到临时文件,然后可以将其持久化。它使接受文件上传变得微不足道:
use rocket::fs::TempFile;#[post("/upload", format = "plain", data = "<file>")]async fn upload(mut file: TempFile<'_>) -> std::io::Result<()> {file.persist_to(permanent_location).await}
流式处理
有时你只是想要直接处理传入数据。例如,你可能希望将传入数据流式传输到某些接收器。Rocket 通过 Data 类型使这尽可能简单:
use rocket::tokio;use rocket::data::{Data, ToByteUnit};#[post("/debug", data = "<data>")]async fn debug(data: Data<'_>) -> std::io::Result<()> {data.open(512.kibibytes()).stream_to(tokio::io::stdout()).await?;Ok(())}
上面的路由接受任何对 /debug 路径的 POST 请求。最多 512KiB 的传入数据被流式传输到 stdout。如果上传失败,将返回错误响应。上面的处理程序是完整的。它真的就是这么简单!
Rocket 在读取传入数据时需要设置限制。
为了帮助防止 DoS 攻击,Rocket 要求你以 ByteUnit 指定,当 open 一个数据流时,你愿意接受来自客户端的数据量。ToByteUnit trait 使得指定这样的值像 128.kibibytes() 一样地道。
表单
表单是 Web 应用程序中最常处理的数据类型之一,Rocket 使处理它们变得容易。Rocket 开箱即用地支持 multipart 和 x-www-form-urlencoded 表单,通过 Form 数据守卫和可派生的 FromForm trait 启用。
假设你的应用程序正在处理一个新的待办事项 Task 的表单提交。表单包含两个字段:complete,一个复选框;和 type,一个文本字段。你可以在 Rocket 中轻松处理表单请求,如下所示:
use rocket::form::Form;#[derive(FromForm)]struct Task<'r> {complete: bool,r#type: &'r str,}#[post("/todo", data = "<task>")]fn new(task: Form<Task<'_>>) { }
只要其泛型参数实现 FromForm trait,Form 就是数据守卫。在示例中,我们自动为 Task 派生了 FromForm trait。可以为任何结构体派生 FromForm,其字段实现 FromForm,或等效地,FromFormField。
如果 POST /todo 请求到达,表单数据将自动解析为 Task 结构体。如果到达的数据不是正确的 Content-Type,请求将被转发。如果数据无法解析或只是无效,将返回可自定义的错误。如前所述,可以使用 Option 和 Result 类型来捕获转发或错误:
#[post("/todo", data = "<task>")]fn new(task: Option<Form<Task<'_>>>) { }
多部分
多部分表单被透明地处理,无需额外努力。大多数 FromForm 类型都可以从传入数据流中解析自身。例如,这里有一个表单和路由,使用 TempFile 接受多部分文件上传:
use rocket::form::Form;use rocket::fs::TempFile;#[derive(FromForm)]struct Upload<'r> {save: bool,换句话说,设置的值对客户端是可见的。对于敏感数据,Rocket 提供了**私有** cookie。私有 cookie 与常规 cookie 类似,不同之处在于它们使用经过身份验证的加密进行加密,这种加密方式同时提供机密性、完整性和真实性。因此,客户端无法检查、篡改或制造私有 cookie。如果你愿意,可以将私有 cookie 视为已签名和加密。必须通过 `secrets` crate 功能手动启用对私有 cookie 的支持:```tomlrocket = { version = "0.5.1", features = ["secrets"] }
用于检索、添加和删除私有 cookie 的 API 与常规 cookie 相同,只是大多数方法都有 _private 后缀。这些方法是:get_private、add_private 和 remove_private。以下是它们的使用示例:
use rocket::http::{Cookie, CookieJar};use rocket::response::{Flash, Redirect};#[get("/user_id")]fn user_id(cookies: &CookieJar<'_>) -> Option<String> {cookies.get_private("user_id").map(|crumb| format!("User ID: {}", crumb.value()))}#[post("/logout")]fn logout(cookies: &CookieJar<'_>) -> Flash<Redirect> {cookies.remove_private("user_id");Flash::success(Redirect::to("/"), "Successfully logged out.")}
密钥
为了加密私有 cookie,Rocket 使用了 secret_key 配置参数中指定的 256 位密钥。在调试模式下编译时,会自动生成一个新密钥。在发布模式下,如果启用了 secrets 功能,Rocket 要求你设置一个密钥。如果不这样做,将在启动时出现硬错误。参数的值可以是 256 位 base64 或十六进制字符串,也可以是 32 字节切片。
生成适合用作 secret_key 配置值的字符串通常通过 openssl 等工具完成。使用 openssl,可以使用命令 openssl rand -base64 32 生成 256 位 base64 密钥。
有关配置的更多信息,请参见指南的配置部分。
格式
路由可以指定它愿意接受或响应的数据格式,通过使用 format 路由参数。参数的值是一个字符串,标识 HTTP 媒体类型或简写变体。例如,对于 JSON 数据,可以使用字符串 application/json 或简写 json。
当路由指示支持有效负载的方法(PUT、POST、DELETE 和 PATCH)时,format 路由参数指示 Rocket 检查传入请求的 Content-Type 头。只有 Content-Type 头与 format 参数匹配的请求才会匹配到路由。
例如,考虑以下路由:
#[post("/user", format = "application/json", data = "<user>")]fn new_user(user: User) { }
post 属性中的 format 参数声明只有具有 Content-Type: application/json 的传入请求才会匹配 new_user。(data 参数在下一节中描述。)对于最常见的 format 参数,也支持简写。你可以使用 format = "json" 等简写,而不是使用完整的 Content-Type format = "application/json"。有关可用简写的完整列表,请参见 ContentType::parse_flexible() 文档。
当路由指示不支持有效负载的方法(GET、HEAD、OPTIONS)时,format 路由参数指示 Rocket 检查传入请求的 Accept 头。只有 Accept 头中首选媒体类型与 format 参数匹配的请求才会匹配到路由。
例如,考虑以下路由:
#[get("/user/<id>", format = "json")]fn user(id: usize) -> User { }
get 属性中的 format 参数声明只有 Accept 头中首选媒体类型为 application/json 的传入请求才会匹配 user。如果路由被声明为 post,Rocket 会将 format 与传入响应的 Content-Type 头匹配。
正文数据
正文数据处理与 Rocket 的许多其他功能一样,是类型导向的。为了指示处理程序期望正文数据,请使用 data = "<param>" 对其进行注释,其中 param 是处理程序中的一个参数。参数的类型必须实现 FromData trait。它看起来像这样,其中 T 被假定为实现 FromData:
#[post("/", data = "<input>")]fn new(input: T) { }
任何实现 FromData 的类型也被称为数据守卫。
JSON
Json<T> 守卫将正文数据反序列化为 JSON。唯一的条件是泛型类型 T 实现了 serde 中的 Deserialize trait。
use rocket::serde::{Deserialize, json::Json};#[derive(Deserialize)]#[serde(crate = "rocket::serde")]struct Task<'r> {description: &'r str,complete: bool}#[post("/todo", data = "<task>")]fn new(task: Json<Task<'_>>) { }
使用 Rocket 的 serde 派生重导出需要多一点努力。
为了方便,Rocket 从 rocket::serde 重导出了 serde 的 Serialize 和 Deserialize trait 以及派生宏。然而,由于 Rust 对派生宏重导出的支持有限,使用重导出的派生宏需要用 #[serde(crate = "rocket::serde")] 对结构进行注释。如果你希望避免这个额外的注释,你必须直接通过你的 crate 的 Cargo.toml 依赖 serde:
serde = { version = "1.0", features = ["derive"] }
我们总是在指南中使用额外的注释,但你可能更喜欢替代方案。
参见 GitHub 上的 JSON 示例以获取完整示例。
JSON 支持需要启用 Rocket 的 json 功能标志。
Rocket 有意将 JSON 支持以及其他数据格式和功能的支持放在功能标志之后。参见 api 文档以获取可用功能列表。json 功能可以在 Cargo.toml 中启用:
rocket = { version = "0.5.1", features = ["json"] }
临时文件
TempFile 数据守卫直接将数据流式传输到临时文件,然后可以将其持久化。它使接受文件上传变得微不足道:
use rocket::fs::TempFile;#[post("/upload", format = "plain", data = "<file>")]async fn upload(mut file: TempFile<'_>) -> std::io::Result<()> {file.persist_to(permanent_location).await}
流式传输
有时你只是想要直接处理传入数据。例如,你可能想要将传入数据流式传输到某个接收器。Rocket 通过 Data 类型使其尽可能简单:
use rocket::tokio;use rocket::data::{Data, ToByteUnit};#[post("/debug", data = "<data>")]async fn debug(data: Data<'_>) -> std::io::Result<()> {data.open(512.kibibytes()).stream_to(tokio::io::stdout()).await?;Ok(())}
上面的路由接受任何对 /debug 路径的 POST 请求。最多 512KiB 的传入数据被流式传输到 stdout。如果上传失败,将返回错误响应。上面的处理程序是完整的。它真的就是这么简单!
Rocket 在读取传入数据时需要设置限制。
为了帮助防止 DoS 攻击,Rocket 要求你指定一个 ByteUnit,表示在 open 数据流时你愿意接受客户端的数据量。ToByteUnit trait 使得指定这样的值像 128.kibibytes() 一样地道。
表单
表单是 Web 应用程序中最常处理的数据类型之一,Rocket 使其处理变得简单。Rocket 支持开箱即用的 multipart 和 x-www-form-urlencoded 表单,通过 Form 数据守卫和可派生的 FromForm trait 启用。
假设你的应用程序正在处理一个新的待办事项 Task 的表单提交。表单包含两个字段:complete,一个复选框,和 type,一个文本字段。你可以在 Rocket 中轻松处理表单请求,如下所示:
use rocket::form::Form;#[derive(FromForm)]struct Task<'r> {complete: bool,r#type: &'r str,}#[post("/todo", data = "<task>")]fn new(task: Form<Task<'_>>) { }
Form 是数据守卫,只要其泛型参数实现 FromForm trait。在示例中,我们自动为 Task 派生了 FromForm trait。FromForm 可以为任何结构派生,其字段实现 FromForm,或等效地,FromFormField。
如果 POST /todo 请求到达,表单数据将自动解析为 Task 结构。如果到达的数据不是正确的 Content-Type,请求将被转发。如果数据无法解析或只是无效,将返回可自定义的错误。如前所述,可以通过使用 Option 和 Result 类型来捕获转发或错误:
#[post("/todo", data = "<task>")]fn new(task: Option<Form<Task<'_>>>) { }
Multipart
Multipart 表单是透明处理的,无需额外努力。大多数 FromForm 类型可以从传入数据流中解析自己。例如,这里有一个表单和路由,使用 TempFile 接受 multipart 文件上传:
use rocket::form::Form;use rocket::fs::TempFile;#[derive(FromForm)]struct Upload<'r> {save: bool,file: TempFile<'r>,}#[post("/upload", data = "<upload>")]fn upload_form(upload: Form<Upload<'_>>) { }
解析策略
Rocket 的 FromForm 解析默认是宽松的:即使传入表单包含额外的、重复的或缺失的字段,Form<T> 也会成功解析。额外的或重复的字段会被忽略——不会对这些字段进行验证或解析——并且当可用时,会用默认值填充缺失的字段。要更改此行为并使表单解析严格,请使用 Form<Strict<T>> 数据类型,如果存在任何额外或缺失的字段,无论默认值如何,都会发出错误。
你可以在任何使用 Form<T> 的地方使用 Form<Strict<T>>。其泛型参数也需要实现 FromForm。例如,我们可以简单地用 Form<Strict<T>> 替换 Form<T> 以上来获得严格解析:
use rocket::form::{Form, Strict};#[post("/todo", data = "<task>")]fn new(task: Form<Strict<Task<'_>>>) { }
Strict 也可以用于使单个字段严格,同时保持整体结构和剩余字段宽松:
#[derive(FromForm)]struct Input {required: Strict<bool>,uses_default: bool}#[post("/", data = "<input>")]fn new(input: Form<Input>) { }
Lenient 是 Strict 的宽松类比,它强制解析为宽松。Form 默认是宽松的,所以 Form<Lenient<T>> 是冗余的,但 Lenient 可以用于将严格解析覆盖为宽松:Option<Lenient<T>>。
默认值
表单守卫可以指定一个默认值,在字段缺失时使用。默认值仅在解析为宽松时使用。当为严格时,所有错误,包括缺失字段,都会直接传播。
一些具有默认值的类型包括 bool,默认值为 false,对复选框很有用,Option<T>,默认值为 None,以及 form::Result,默认值为 Err(Missing) 或否则在 Err 中收集错误。可以使用默认守卫就像任何其他表单守卫一样:
use rocket::form::{self, Errors};#[derive(FromForm)]struct MyForm<'v> {maybe_string: Option<&'v str>,ok_or_error: form::Result<'v, Vec<&'v str>>,here_or_false: bool,}
可以使用 #[field(default = expr)] 字段属性覆盖或取消设置默认值。如果 expr 不是字面上的 None,则参数将字段的默认值设置为 expr.into()。如果 expr是 None,则参数取消设置字段的默认值(如果有)。
#[derive(FromForm)]struct MyForm {#[field(default = "hello")]greeting: String,#[field(default = None)]is_friendly: bool,}
有关 default 属性参数的完整详细信息,以及更具表现力的 default_with 参数选项的文档,请参见 FromForm 派生文档。
字段重命名
默认情况下,Rocket 将传入表单字段的名称与结构字段的名称匹配。虽然这种行为是典型的,但也可能需要为表单字段和结构字段使用不同的名称,同时仍然按预期解析。你可以通过使用一个或多个 #[field(name = "name")] 或 #[field(name = uncased("name")] 字段注释来要求 Rocket 为给定结构字段查找不同的表单字段。uncased 变体不区分大小写地匹配字段名称。
例如,假设你正在编写一个从外部服务接收数据的应用程序。外部服务 POST 一个带有名为 first-Name 字段的表单,你希望在 Rust 中将其写为 first_name。这样的表单结构可以写为:
#[derive(FromForm)]struct External<'r> {#[field(name = "first-Name")]first_name: &'r str}
如果你想同时接受 firstName 不区分大小写以及 first_name 区分大小写,你需要使用两个注释:
#[derive(FromForm)]struct External<'r> {#[field(name = uncased("firstName"))]#[field(name = "first_name")]first_name: &'r str}
这将匹配 firstName 的任何大小写,包括 FirstName、firstname、FIRSTname 等,但仅在 first_name 上完全匹配。
相反,如果你想匹配 first-name、first_name 或 firstName 中的任何一个,在每种情况下都不区分大小写,你会写:
#[derive(FromForm)]struct External<'r> {#[field(name = uncased("first-name"))]#[field(name = uncased("first_name"))]#[field(name = uncased("firstname"))]first_name: &'r str}
可以混合和匹配大小写和不区分大小写的重命名,并且允许任意数量的重命名。如果字段名称冲突,Rocket 将在编译时发出错误,防止运行时出现歧义解析。
临时验证
可以通过 #[field(validate)] 属性轻松地对表单字段进行临时验证。例如,考虑一个表单字段 age: u16,我们希望确保它大于 21。以下结构实现了这一点:
#[derive(FromForm)]struct Person {#[field(validate = range(21..))]age: u16}
表达式 range(21..) 是对 form::validate::range 的调用。Rocket 将属性字段的借用,这里是 self.age,作为第一个参数传递给函数调用。表达式的其余字段按书面形式传递。
可以调用 form::validate 模块中的任何函数,并且可以通过使用 self.$field 传递表单的其他字段,其中 $field 是结构中字段的名称。你也可以通过使用多个属性对字段应用多个验证。例如,以下表单验证字段 confirm 的值等于字段 value 的值,并且不包含 no:
#[derive(FromForm)]struct Password<'r> {#[field(name = "password")]value: &'r str,#[field(validate = eq(self.value))]#[field(validate = omits("no"))]confirm: &'r str,}
实际上,validate = 之后的表达式可以是任何表达式,只要它评估为 Result<(), Errors<'_>> 类型的值(由 form::Result 别名),其中 Ok 值表示验证成功,而 Err 的 Errors<'_> 表示发生的错误。例如,如果你想为类似信用卡的数字实现临时 Luhn 验证器,你可能会写:
use rocket::time::Date;use rocket::form::{self, Error};#[derive(FromForm)]struct CreditCard {#[field(validate = luhn(self.cvv, &self.expiration))]number: u64,#[field(validate = range(..9999))]cvv: u16,expiration: Date,}fn luhn<'v>(number: &u64, cvv: u16, exp: &Date) -> form::Result<'v, ()> {if !valid {Err(Error::validation("invalid credit card number"))?;}Ok(())}
如果字段的验证不依赖于其他字段(验证是局部的),则在依赖于其他字段的字段之前进行验证。对于 CreditCard,cvv 和 expiration 将在 number 之前进行验证。
包装验证器
如果在多个地方应用特定验证,请优先创建封装并表示验证值的类型。例如,如果你的应用程序经常验证 age 字段,请考虑创建一个自定义的 Age 表单守卫,该守卫始终应用验证:
#[derive(FromForm)]#[field(validate = range(18..150))]struct Age(u16);
当自定义验证器已经以其他形式存在时,此方法也很有用。例如,以下示例利用 try_with 和 Token 类型上现有的 FromStr 实现来验证字符串:
use std::str::FromStr;#[derive(FromForm)]#[field(validate = try_with(|s| Token::from_str(s)))]struct Token<'r>(&'r str);
集合
Rocket 的表单支持允许你的应用程序表达任何结构,具有任何级别的嵌套和集合,超越了任何其他 Web 框架提供的表达能力。为了解析这些结构,Rocket 通过分隔符 . 和 [] 将字段的名称分成“键”,每个键又通过 : 分成“索引”。换句话说,一个名称有键,一个键有索引,每个索引都是其父项的严格子集。这在下面的示例中描绘了两个表单字段:
food.bart[bar:foo].blam[0_0][1000]=some-value&another_field=another_val|-------------------------------| name|--| |--| |-----| |--| |-| |--| keys|--| |--| |-| |-| |--| |-| |--| indices
Rocket 在表单字段到达时推送表单字段到 FromForm 类型。该类型然后对一个键(及其所有索引)进行操作,并移动到下一个 key,从左到右,在调用任何其他 FromForm 类型之前。一个移动编码了一个嵌套结构,而索引允许需要多于一个值的结构允许索引。
[] 后的 . 是可选的。
表单字段名称 a[b]c 完全等价于 a[b].c。同样,表单字段名称 .a 等价于 a。
嵌套
表单结构可以嵌套:
use rocket::form::FromForm;#[derive(FromForm)]struct MyForm<'r> {owner: Person<'r>,pet: Pet<'r>,}#[derive(FromForm)]struct Person<'r> {name: &'r str}#[derive(FromForm)]struct Pet<'r> {name: &'r str,#[field(validate = eq(true))]good_pet: bool,}
为了解析成 MyForm,必须提交具有以下字段的表单:
-
owner.name- 字符串 -
pet.name- 字符串 -
pet.good_pet- 布尔值
这样的表单,URL 编码后,可能看起来像这样:
"owner.name=Bob&pet.name=Sally&pet.good_pet=on",MyForm {owner: Person {name: "Bob".into()},pet: Pet {name: "Sally".into(),good_pet: true,}}
注意 . 用于分隔每个字段。同样,[] 可以用于替代或附加到 .:
"owner[name]=Bob&pet[name]=Sally&pet[good_pet]=on","owner[name]=Bob&pet[name]=Sally&pet.good_pet=on","owner.name=Bob&pet[name]=Sally&pet.good_pet=on","pet[name]=Sally&owner.name=Bob&pet.good_pet=on",MyForm {owner: Person {name: "Bob".into()},pet: Pet {name: "Sally".into(),good_pet: true,}}
允许任意级别的嵌套。
向量
表单还可以包含序列:
#[derive(FromForm)]struct MyForm {numbers: Vec<usize>,}
为了解析成 MyForm,必须提交具有以下字段的表单:
-
numbers[$k]- usize(或等价地,numbers.$k)
...其中 $k 是用于确定是将字段的其余部分推送到向量的最后一个元素还是创建新元素的“键”。如果键与向量看到的前一个键相同,则字段的值将推送到最后一个元素。否则,将创建一个新元素。$k 的实际值是无关紧要的:它仅用于比较,没有语义含义,并且不会被 Vec 记住。特殊的空白键永远不会等于任何其他键。
考虑以下示例。
"numbers[]=1&numbers[]=2&numbers[]=3","numbers[a]=1&numbers[b]=2&numbers[c]=3","numbers[a]=1&numbers[b]=2&numbers[a]=3","numbers[]=1&numbers[b]=2&numbers[c]=3","numbers.0=1&numbers.1=2&numbers[c]=3","numbers=1&numbers=2&numbers=3",MyForm {numbers: vec![1 ,2, 3]}"numbers[0]=1&numbers[0]=2&numbers[]=3","numbers[]=1&numbers[b]=3&numbers[b]=2",MyForm {numbers: vec![1, 3]}
你可能会对最后一个例子 "numbers=1&numbers=2&numbers=3" 感到惊讶,它在第一个列表中。这等价于前面的例子,因为 Vec 看到的“键”(numbers 之后的所有内容)是空的。因此,Vec 为每个字段推送到一个新的 usize。像所有实现 FromFormField 的类型一样,usize 在宽松解析时会丢弃重复和额外的字段,只保留第一个字段。
向量中的嵌套
由于向量本身是 FromForm,它们可以出现在向量内部:
#[derive(FromForm)]struct MyForm {v: Vec<Vec<usize>>,}
规则完全相同。
"v=1&v=2&v=3" => MyForm { v: vec![vec![1], vec![2], vec![3]] },"v[][]=1&v[][]=2&v[][]=3" => MyForm { v: vec![vec![1], vec![2], vec![3]] },"v[0][]=1&v[0][]=2&v[][]=3" => MyForm { v: vec![vec![1, 2], vec![3]] },"v[][]=1&v[0][]=2&v[0][]=3" => MyForm { v: vec![vec![1], vec![2, 3]] },"v[0][]=1&v[0][]=2&v[0][]=3" => MyForm { v: vec![vec![1, 2, 3]] },"v[0][0]=1&v[0][0]=2&v[0][]=3" => MyForm { v: vec![vec![1, 3]] },"v[0][0]=1&v[0][0]=2&v[0][0]=3" => MyForm { v: vec![vec![1]] },
映射
表单还可以包含映射:
use std::collections::HashMap;#[derive(FromForm)]struct MyForm {ids: HashMap<String, usize>,}
为了解析成 MyForm,必须提交具有以下字段的表单:
-
ids[$string]- usize(或等价地,ids.$string)
...其中 $string 是用于确定将字段的其余部分推送到映射中的哪个值的“键”。与向量不同,键确实具有语义含义并且确实被记住,因此字段的顺序是无关紧要的:给定的字符串 $string 总是映射到同一个元素。
例如,以下内容是等价的,并且都解析为 { "a" => 1, "b" => 2 }:
"ids[a]=1&ids[b]=2","ids[b]=2&ids[a]=1","ids[a]=1&ids[a]=2&ids[b]=2","ids.a=1&ids.b=2",MyForm {ids: map! {"a" => 1usize,"b" => 2usize,}}
HashMap 的键和值都可以是实现 FromForm 的任何类型。考虑一个表示另一个结构的值:
#[derive(FromForm)]struct MyForm {ids: HashMap<usize, Person>,}#[derive(FromForm)]struct Person {name: String,age: usize}
为了解析成 MyForm,必须提交具有以下字段的表单:
-
ids[$usize].name- 字符串 -
ids[$usize].age- usize
示例包括:
"ids[0]name=Bob&ids[0]age=3&ids[1]name=Sally&ids[1]age=10","ids[0]name=Bob&ids[1]age=10&ids[1]name=Sally&ids[0]age=3","ids[0]name=Bob&ids[1]name=Sally&ids[0]age=3&ids[1]age=10",MyForm {ids: map! {0usize => Person { name: "Bob".into(), age: 3 },1usize => Person { name: "Sally".into(), age: 10 },}}
现在考虑以下结构,其中键和值都表示结构:
#[derive(FromForm)]struct MyForm {m: HashMap<Person, Pet>,}#[derive(FromForm, PartialEq, Eq, Hash)]struct Person {name: String,age: usize}#[derive(FromForm)]struct Pet {wags: bool}
HashMap 键类型,此处为 Person,必须实现 Eq + Hash。
由于键是一个集合,此处为 Person,它必须由多个字段构建。这需要能够通过表单字段名称指定字段的值对应于映射中的键。这是通过语法 k:$key 完成的,该语法表示字段对应于名为 $key 的键。因此,为了解析成 MyForm,必须提交具有以下字段的表单:
-
m[k:$key].name- 字符串 -
m[k:$key].age- usize -
m[$key].wags或m[v:$key].wags- 布尔值
语法 v:$key 也存在。
速记 m[$key] 等价于 m[v:$key]。
注意,$key 可以是任何内容:它只是映射中键/值对的符号标识符,与实际将解析到映射中的值无关。
示例包括:
"m[k:alice]name=Alice&m[k:alice]age=30&m[v:alice].wags=no","m[k:alice]name=Alice&m[k:alice]age=30&m[alice].wags=no","m[k:123]name=Alice&m[k:123]age=30&m[123].wags=no",MyForm {m: map! {Person { name: "Alice".into(), age: 30 } => Pet { wags: false }}}"m[k:a]name=Alice&m[k:a]age=40&m[a].wags=no&\m[k:b]name=Bob&m[k:b]age=72&m[b]wags=yes&\m[k:cat]name=Katie&m[k:cat]age=12&m[cat]wags=yes",MyForm {m: map! {Person { name: "Alice".into(), age: 40 } => Pet { wags: false },Person { name: "Bob".into(), age: 72 } => Pet { wags: true },Person { name: "Katie".into(), age: 12 } => Pet { wags: true },}}
任意集合
任何集合都可以用任意级别的嵌套、映射和序列来表达。考虑以下 extravagant 类型:
use std::collections::{BTreeMap, HashMap};#[derive(FromForm, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]struct Person {name: String,age: usize}HashMap<Vec<BTreeMap<Person, usize>>, HashMap<usize, Person>>|-[k:$k1]-----------|------|------| |-[$k1]-----------------||---[$i]-------|------|------| |-[k:$j]*||-[k:$k2]|------| ~~[$j]~~|name*||-name*| ~~[$j]~~|age-*||-age*-||~~~~~~~~~~~~~~~|v:$k2*|
BTreeMap 键类型,此处为 Person,必须实现 Ord。
如上所示,用 * 标记终端,我们需要以下表单字段来实现此结构:
-
[k:$k1][$i][k:$k2]name- 字符串 -
[k:$k1][$i][k:$k2]age- usize -
[k:$k1][$i][$k2]- usize -
[$k1][k:$j]- usize -
[$k1][$j]name- 字符串 -
[$k1][$j]age- 字符串
其中我们有以下符号键:
-
$k1:顶级键的符号名称 -
$i:向量索引的符号名称 -
$k2:子级(BTreeMap)键的符号名称 -
$j:顶级值键的符号名称和/或值
type Foo = HashMap<Vec<BTreeMap<Person, usize>>, HashMap<usize, Person>>;"[k:top_key][i][k:sub_key]name=Bobert&\[k:top_key][i][k:sub_key]age=22&\[k:top_key][i][sub_key]=1337&\[top_key][7]name=Builder&\[top_key][7]age=99",map! {vec![bmap! {Person { name: "Bobert".into(), age: 22 } => 1337usize,}]=>map! {7usize => Person { name: "Builder".into(), age: 99 }}}
上下文
Contextual 表单守卫充当任何其他表单守卫的代理,记录所有提交的表单值和产生的错误,并将它们与相应的字段名称关联。Contextual 对于渲染带有先前提交值和与表单输入关联的错误的表单特别有用。
要检索表单的上下文,请使用 Form<Contextual<'_, T>> 作为数据守卫,其中 T 实现 FromForm。context 字段包含表单的 Context:
use rocket::form::{Form, Contextual};#[post("/submit", data = "<form>")]fn submit(form: Form<Contextual<'_, T>>) {if let Some(ref value) = form.value {// ...}let raw_id_value = form.context.field_value("id");let id_errors = form.context.field_errors("id");}
Context 对于错误是嵌套感知的。当查询字段 foo.bar 的错误时,Context 返回前缀为 foo.bar 的字段的错误,即 foo 和 foo.bar。同样,如果查询字段 foo.bar.baz 的错误,将返回字段 foo、foo.bar 和 foo.bar.baz 的错误。
Context 序列化为映射,因此它可以在需要 Serialize 类型的模板中渲染。请参见 Context 以了解其序列化格式的详细信息。表单示例也使用了表单上下文,以及所有其他表单功能。
查询字符串
查询字符串是出现在请求 URL 中的 URL 编码表单。查询参数像路径参数一样声明,但除此之外,像常规 URL 编码表单字段一样处理。下表总结了类比:
| 路径语法 | 查询语法 | 路径类型绑定 | 查询类型绑定 |
|---|---|---|---|
<param> |
<param> |
FromParam |
FromForm |
<param..> |
<param..> |
FromSegments |
FromForm |
static |
static |
N/A | N/A |
由于动态参数是表单类型,它们可以是单个值、集合、嵌套集合,或介于两者之间的任何值,就像任何其他表单字段一样。
静态参数
请求匹配路由当且仅当其查询字符串包含路由查询字符串中的所有静态参数。具有静态参数 param(任何 UTF-8 文本字符串)的路由将仅匹配查询字符串中具有该确切路径段的请求。
这确实是一个当且仅当!
只有查询路由字符串中的静态参数影响路由。默认情况下,动态参数可以缺失。
例如,以下路由将匹配路径为 / 且至少具有查询段 hello 和 cat=♥ 的请求:
#[get("/?hello&cat=♥")]fn cats() -> &'static str {"Hello, kittens!"}"/?cat=%E2%99%A5&hello""/?hello&cat=%E2%99%A5""/?dogs=amazing&hello&there&cat=%E2%99%A5"
动态参数
单个动态参数 <param> 的行为与声明为 param 的表单字段相同。特别是,Rocket 期望查询表单包含一个带有键 param 的字段,并将移动的字段推送到 param 类型。与表单一样,当解析失败时使用默认值。以下示例用单个值 name、集合 color、嵌套表单 person 以及将默认为 None 的 other 值说明了这一点:
#[derive(Debug, PartialEq, FromFormField)]enum Color {Red,Blue,Green}#[derive(Debug, PartialEq, FromForm)]struct Pet<'r> {name: &'r str,age: usize,}#[derive(Debug, PartialEq, FromForm)]struct Person<'r> {pet: Pet<'r>,}#[get("/?<name>&<color>&<person>&<other>")]fn hello(name: &str, color: Vec<Color>, person: Person<'_>, other: Option<usize>) {assert_eq!(name, "George");assert_eq!(color, [Color::Red, Color::Green, Color::Green, Color::Blue]);assert_eq!(other, None);assert_eq!(person, Person {pet: Pet { name: "Fi Fo Alex", age: 1 }});}name=George&\color=red&\color=green&\person.pet.name=Fi+Fo+Alex&\color=green&\person.pet.age=1&\color=blue&\extra=yes
注意,与表单一样,解析是字段顺序不敏感和宽松的。
尾部参数
尾部动态参数 <param..> 收集所有不匹配声明的静态或动态参数的查询段。换句话说,否则不匹配的段会被不移动地推送到 <param..> 类型:
use rocket::form::Form;#[derive(FromForm)]struct User<'r> {name: &'r str,active: bool,}#[get("/?hello&<id>&<user..>")]fn user(id: usize, user: User<'_>) {assert_eq!(id, 1337);assert_eq!(user.name, "Bob Smith");assert_eq!(user.active, true);}hello&\name=Bob+Smith&\id=1337&\active=yes
错误捕获器
应用程序处理是不可避免的。错误来自以下源:
-
失败的守卫。 -
失败的响应器。 -
路由失败。
如果发生任何这些情况,Rocket 会向客户端返回错误。为了生成错误,Rocket 会调用与错误状态码和作用域对应的捕获器。捕获器类似于路由,除了在以下方面:
-
捕获器仅在错误条件下调用。 -
捕获器用 catch属性声明。 -
捕获器通过 register()注册,而不是mount()。 -
在调用捕获器之前,对 cookie 的任何修改都会被清除。 -
错误捕获器不能调用守卫。 -
错误捕获器不应未能产生响应。 -
捕获器作用于路径前缀。
要为给定的状态码声明捕获器,请使用 catch 属性,它接受一个整数,对应于要捕获的 HTTP 状态码。例如,要声明一个用于 404 Not Found 错误的捕获器,你会写:
use rocket::Request;#[catch(404)]fn not_found(req: &Request) { }
捕获器可以带有零个、一个或两个参数。如果捕获器带有一个参数,它必须是 &Request 类型。如果它带有两个参数,它们必须是 Status 和 &Request 类型,按此顺序。与路由一样,返回类型必须实现 Responder。一个具体的实现可能看起来像这样:
#[catch(404)]fn not_found(req: &Request) -> String {format!("Sorry, '{}' is not a valid path.", req.uri())}
也与路由一样,Rocket 需要在使用捕获器处理错误之前知道它。这个过程被称为“注册”捕获器,类似于挂载路由:通过 catchers! 宏调用 register() 方法,并传入捕获器列表。添加上述声明的 404 捕获器的调用看起来像这样:
fn main() {rocket::build().register("/", catchers![not_found]);}
作用域
register() 的第一个参数是路径前缀,用于作用域捕获器,称为捕获器的基础。捕获器的基决定了它将处理哪些请求的错误。具体来说,捕获器的基必须是错误请求的前缀,才能被调用。当可以调用多个捕获器时,具有最长基的捕获器优先。
例如,考虑以下应用程序:
#[catch(404)]fn general_not_found() -> &'static str {"General 404"}#[catch(404)]fn foo_not_found() -> &'static str {"Foo 404"}#[launch]fn rocket() -> _ {rocket::build().register("/", catchers![general_not_found]).register("/foo", catchers![foo_not_found])}
由于没有挂载路由,所有请求都会 404。任何以 /foo 开头的路径(即 GET /foo、GET /foo/bar 等)的请求都将由 foo_not_found 捕获器处理,而所有其他请求将由 general_not_found 捕获器处理。
默认捕获器
默认捕获器是处理所有状态码的捕获器。如果没有为给定错误注册特定于状态码的捕获器,则它们会被作为后备调用。使用 #[catch(default)] 声明默认捕获器,并且同样必须通过 register() 注册:
use rocket::Request;use rocket::http::Status;#[catch(default)]fn default_catcher(status: Status, request: &Request) { }#[launch]fn rocket() -> _ {rocket::build().register("/", catchers![default_catcher])}
具有较长基的捕获器优先,即使在有特定于状态码的捕获器时也是如此。换句话说,具有较长匹配基的默认捕获器优先于特定于状态码的捕获器。
内置捕获器
Rocket 提供了一个内置的默认捕获器。它根据 Accept 头的值生成 HTML 或 JSON。因此,自定义捕获器只需要为自定义错误处理注册。
错误处理示例完整说明了捕获器的使用,而 Catcher API 文档提供了更多详细信息。

