92 KiB
obj | website | rev |
---|---|---|
concept | https://rocket.rs | 2024-06-19 |
Rocket.rs
Rocket provides primitives to build web servers and applications with Rust.
Lifecycle
Rocket's main task is to listen for incoming web requests, dispatch the request to the application code, and return a response to the client. We call the process that goes from request to response the "lifecycle". We summarize the lifecycle as the following sequence of steps:
- Routing
Rocket parses an incoming HTTP request into native structures that your code operates on indirectly. Rocket determines which request handler to invoke by matching against route attributes declared in your application. - Validation
Rocket validates the incoming request against types and guards present in the matched route. If validation fails, Rocket forwards the request to the next matching route or calls an error handler. - Processing
The request handler associated with the route is invoked with validated arguments. This is the main business logic of an application. Processing completes by returning aResponse
. - Response
The returnedResponse
is processed. Rocket generates the appropriate HTTP response and sends it to the client. This completes the lifecycle. Rocket continues listening for requests, restarting the lifecycle for each incoming request.
Routing
Rocket applications are centered around routes and handlers. A route is a combination of:
- A set of parameters to match an incoming request against.
- A handler to process the request and return a response.
A handler is simply a function that takes an arbitrary number of arguments and returns any arbitrary type.
The parameters to match against include static paths, dynamic paths, path segments, forms, query strings, request format specifiers, and body data. Rocket uses attributes, which look like function decorators in other languages, to make declaring routes easy. Routes are declared by annotating a function, the handler, with the set of parameters to match against. A complete route declaration looks like this:
#[get("/world")] // <- route attribute
fn world() -> &'static str { // <- request handler
"hello, world!"
}
Mounting
Before Rocket can dispatch requests to a route, the route needs to be mounted:
rocket::build().mount("/hello", routes![world]);
The mount
method takes as input:
- A base path to namespace a list of routes under, here,
/hello
. - A list of routes via the
routes!
macro: here,routes![world]
, with multiple routes:routes![a, b, c]
.
This creates a new Rocket
instance via the build
function and mounts the world
route to the /hello
base path, making Rocket aware of the route. GET
requests to /hello/world
will be directed to the world
function.
The mount
method, like all other builder methods on Rocket
, can be chained any number of times, and routes can be reused by mount points:
rocket::build()
.mount("/hello", routes![world])
.mount("/hi", routes![world]);
By mounting world
to both /hello
and /hi
, requests to "/hello/world"
and "/hi/world"
will be directed to the world
function.
> Note: In many cases, the base path will simply be "/"
.
Launching
Rocket begins serving requests after being launched, which starts a multi-threaded asynchronous server and dispatches requests to matching routes as they arrive.
There are two mechanisms by which a Rocket
can be launched. The first and preferred approach is via the #[launch]
route attribute, which generates a main
function that sets up an async runtime and starts the server. With #[launch]
, our complete Hello, world! application looks like:
#[macro_use] extern crate rocket;
#[get("/world")]
fn world() -> &'static str {
"Hello, world!"
}
#[launch]
fn rocket() -> _ {
rocket::build().mount("/hello", routes![world])
}
Requests
Together, a route
attribute and function signature specify what must be true about a request in order for the route's handler to be called. You've already seen an example of this in action:
#[get("/world")]
fn handler() { /* .. */ }
This route indicates that it only matches against GET
requests to the /world
route. Rocket ensures that this is the case before handler
is called. Of course, you can do much more than specify the method and path of a request. Among other things, you can ask Rocket to automatically validate:
- The type of a dynamic path segment.
- The type of several dynamic path segments.
- The type of incoming body data.
- The types of query strings, forms, and form values.
- The expected incoming or outgoing format of a request.
- Any arbitrary, user-defined security or validation policies.
The route attribute and function signature work in tandem to describe these validations. Rocket's code generation takes care of actually validating the properties. This section describes how to ask Rocket to validate against all of these properties and more.
Methods
A Rocket route attribute can be any one of get
, put
, post
, delete
, head
, patch
, or options
, each corresponding to the HTTP method to match against. For example, the following attribute will match against POST
requests to the root path:
#[post("/")]
The grammar for these attributes is defined formally in the route
API docs.
HEAD Requests
Rocket handles HEAD
requests automatically when there exists a GET
route that would otherwise match. It does this by stripping the body from the response, if there is one. You can also specialize the handling of a HEAD
request by declaring a route for it; Rocket won't interfere with HEAD
requests your application explicitly handles.
Dynamic Paths
You can declare path segments as dynamic by using angle brackets around variable names in a route's path. For example, if we want to say Hello! to anything, not just the world, we can declare a route like so:
#[get("/hello/<name>")]
fn hello(name: &str) -> String {
format!("Hello, {}!", name)
}
If we were to mount the path at the root (.mount("/", routes![hello])
), then any request to a path with two non-empty segments, where the first segment is hello
, will be dispatched to the hello
route. For example, if we were to visit /hello/John
, the application would respond with Hello, John!
.
Any number of dynamic path segments are allowed. A path segment can be of any type, including your own, as long as the type implements the FromParam
trait. We call these types parameter guards. Rocket implements FromParam
for many of the standard library types, as well as a few special Rocket types. For the full list of provided implementations, see the FromParam
API docs. Here's a more complete route to illustrate varied usage:
#[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)
}
}
Multiple Segments
You can also match against multiple segments by using <param..>
in a route path. The type of such parameters, known as segments guards, must implement FromSegments
. A segments guard must be the final component of a path: any text after a segments guard will result in a compile-time error.
As an example, the following route matches against all paths that begin with /page
:
use std::path::PathBuf;
#[get("/page/<path..>")]
fn get_page(path: PathBuf) { /* ... */ }
The path after /page/
will be available in the path
parameter, which may be empty for paths that are simply /page
, /page/
, /page//
, and so on. The FromSegments
implementation for PathBuf
ensures that path
cannot lead to path traversal attacks. With this, a safe and secure static file server can be implemented in just 4 lines:
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 makes it even easier to serve static files!
If you need to serve static files from your Rocket application, consider using FileServer
, which makes it as simple as:
use rocket::fs::FileServer;
#[launch]
fn rocket() -> _ {
rocket::build()
// serve files from `/www/static` at path `/public`
.mount("/public", FileServer::from("/www/static"))
}
Ignored Segments
A component of a route can be fully ignored by using <_>
, and multiple components can be ignored by using <_..>
. In other words, the wildcard name _
is a dynamic parameter name that ignores that dynamic parameter. An ignored parameter must not appear in the function argument list. A segment declared as <_>
matches anything in a single segment while segments declared as <_..>
match any number of segments with no conditions.
As an example, the foo_bar
route below matches any GET
request with a 3-segment URI that starts with /foo/
and ends with /bar
. The everything
route below matches every GET request.
#[get("/foo/<_>/bar")]
fn foo_bar() -> &'static str {
"Foo _____ bar!"
}
#[get("/<_..>")]
fn everything() -> &'static str {
"Hey, you're here."
}
Request Guards
Request guards are one of Rocket's most powerful instruments. As the name might imply, a request guard protects a handler from being called erroneously based on information contained in an incoming request. More specifically, a request guard is a type that represents an arbitrary validation policy. The validation policy is implemented through the FromRequest
trait. Every type that implements FromRequest
is a request guard.
Request guards appear as inputs to handlers. An arbitrary number of request guards can appear as arguments in a route handler. Rocket will automatically invoke the FromRequest
implementation for request guards before calling the handler. Rocket only dispatches requests to a handler when all of its guards pass.
For instance, the following dummy handler makes use of three request guards, A
, B
, and C
. An input can be identified as a request guard if it is not named in the route attribute.
#[get("/<param>")]
fn index(param: isize, a: A, b: B, c: C) { /* ... */ }
Request guards always fire in left-to-right declaration order. In the example above, the order will be A
followed by B
followed by C
. Errors are short-circuiting; if one guard fails, the remaining are not attempted. To learn more about request guards and implementing them, see the FromRequest
documentation.
Custom Guards
You can implement FromRequest
for your own types. For instance, to protect a sensitive
route from running unless an ApiKey
is present in the request headers, you might create an ApiKey
type that implements FromRequest
and then use it as a request guard:
#[get("/sensitive")]
fn sensitive(key: ApiKey) { /* .. */ }
You might also implement FromRequest
for an AdminUser
type that authenticates an administrator using incoming cookies. Then, any handler with an AdminUser
or ApiKey
type in its argument list is assured to only be invoked if the appropriate conditions are met. Request guards centralize policies, resulting in a simpler, safer, and more secure applications.
Cookies
A reference to a CookieJar
is an important, built-in request guard: it allows you to get, set, and remove cookies. Because &CookieJar
is a request guard, an argument of its type can simply be added to a handler:
use rocket::http::CookieJar;
#[get("/")]
fn index(cookies: &CookieJar<'_>) -> Option<String> {
cookies.get("message").map(|crumb| format!("Message: {}", crumb.value()))
}
This results in the incoming request's cookies being accessible from the handler. The example above retrieves a cookie named message
. Cookies can also be set and removed using the CookieJar
guard. The cookies example on GitHub illustrates further use of the CookieJar
type to get and set cookies, while the CookieJar
documentation contains complete usage information.
Private Cookies
Cookies added via the CookieJar::add()
method are set in the clear. In other words, the value set is visible to the client. For sensitive data, Rocket provides private cookies. Private cookies are similar to regular cookies except that they are encrypted using authenticated encryption, a form of encryption which simultaneously provides confidentiality, integrity, and authenticity. Thus, private cookies cannot be inspected, tampered with, or manufactured by clients. If you prefer, you can think of private cookies as being signed and encrypted.
Support for private cookies must be manually enabled via the secrets
crate feature:
## in Cargo.toml
rocket = { version = "0.5.1", features = ["secrets"] }
The API for retrieving, adding, and removing private cookies is identical except that most methods are suffixed with _private
. These methods are: get_private
, add_private
, and remove_private
. An example of their usage is below:
use rocket::http::{Cookie, CookieJar};
use rocket::response::{Flash, Redirect};
/// Retrieve the user's ID, if any.
#[get("/user_id")]
fn user_id(cookies: &CookieJar<'_>) -> Option<String> {
cookies.get_private("user_id")
.map(|crumb| format!("User ID: {}", crumb.value()))
}
/// Remove the `user_id` cookie.
#[post("/logout")]
fn logout(cookies: &CookieJar<'_>) -> Flash<Redirect> {
cookies.remove_private("user_id");
Flash::success(Redirect::to("/"), "Successfully logged out.")
}
Secret Key
To encrypt private cookies, Rocket uses the 256-bit key specified in the secret_key
configuration parameter. When compiled in debug mode, a fresh key is generated automatically. In release mode, Rocket requires you to set a secret key if the secrets
feature is enabled. Failure to do so results in a hard error at launch time. The value of the parameter may either be a 256-bit base64 or hex string or a 32-byte slice.
Generating a string suitable for use as a secret_key
configuration value is usually done through tools like openssl
. Using openssl
, a 256-bit base64 key can be generated with the command openssl rand -base64 32
.
For more information on configuration, see Configuration.
Body Data
Body data processing, like much of Rocket, is type directed. To indicate that a handler expects body data, annotate it with data = "<param>"
, where param
is an argument in the handler. The argument's type must implement the FromData
trait. It looks like this, where T
is assumed to implement FromData
:
#[post("/", data = "<input>")]
fn new(input: T) { /* .. */ }
Any type that implements FromData
is also known as a data guard.
JSON
The Json<T>
guard deserializes body data as JSON. The only condition is that the generic type T
implements the Deserialize
trait from serde
.
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<'_>>) { /* .. */ }
JSON support requires enabling Rocket's
json
feature flag.
Rocket intentionally places JSON support, as well support for other data formats and features, behind feature flags. See the api docs for a list of available features. The json
feature can be enabled in the Cargo.toml
:
rocket = { version = "0.5.1", features = ["json"] }
Temporary Files
The TempFile
data guard streams data directly to a temporary file which can then be persisted. It makes accepting file uploads trivial:
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
}
Streaming
Sometimes you just want to handle incoming data directly. For example, you might want to stream the incoming data to some sink. Rocket makes this as simple as possible via the Data
type:
use rocket::tokio;
use rocket::data::{Data, ToByteUnit};
#[post("/debug", data = "<data>")]
async fn debug(data: Data<'_>) -> std::io::Result<()> {
// Stream at most 512KiB all of the body data to stdout.
data.open(512.kibibytes())
.stream_to(tokio::io::stdout())
.await?;
Ok(())
}
The route above accepts any POST
request to the /debug
path. At most 512KiB of the incoming is streamed out to stdout
. If the upload fails, an error response is returned.
Forms
Forms are one of the most common types of data handled in web applications, and Rocket makes handling them easy. Rocket supports both multipart
and x-www-form-urlencoded
forms out of the box, enabled by the Form
data guard and derivable FromForm
trait.
Say your application is processing a form submission for a new todo Task
. The form contains two fields: complete
, a checkbox, and type
, a text field. You can easily handle the form request in Rocket as follows:
use rocket::form::Form;
#[derive(FromForm)]
struct Task<'r> {
complete: bool,
r#type: &'r str,
}
#[post("/todo", data = "<task>")]
fn new(task: Form<Task<'_>>) { /* .. */ }
Multipart
Multipart forms are handled transparently, with no additional effort. Most FromForm
types can parse themselves from the incoming data stream. For example, here's a form and route that accepts a multipart file upload using TempFile
:
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<'_>>) { /* .. */ }
Parsing Strategy
Rocket's FromForm
parsing is lenient by default: a Form<T>
will parse successfully from an incoming form even if it contains extra, duplicate, or missing fields. Extras or duplicates are ignored -- no validation or parsing of the fields occurs -- and missing fields are filled with defaults when available. To change this behavior and make form parsing strict, use the Form<Strict<T>>
data type, which emits errors if there are any extra or missing fields, irrespective of defaults.
You can use a Form<Strict<T>>
anywhere you'd use a Form<T>
. Its generic parameter is also required to implement FromForm
. For instance, we can simply replace Form<T>
with Form<Strict<T>>
above to get strict parsing:
use rocket::form::{Form, Strict};
#[post("/todo", data = "<task>")]
fn new(task: Form<Strict<Task<'_>>>) { /* .. */ }
Strict
can also be used to make individual fields strict while keeping the overall structure and remaining fields lenient:
#[derive(FromForm)]
struct Input {
required: Strict<bool>,
uses_default: bool
}
#[post("/", data = "<input>")]
fn new(input: Form<Input>) { /* .. */ }
Lenient
is the lenient analog to Strict
, which forces parsing to be lenient. Form
is lenient by default, so a Form<Lenient<T>>
is redundant, but Lenient
can be used to overwrite a strict parse as lenient: Option<Lenient<T>>
.
Defaults
A form guard may specify a default value to use when a field is missing. The default value is used only when parsing is lenient. When strict, all errors, including missing fields, are propagated directly.
Some types with defaults include bool
, which defaults to false
, useful for checkboxes, Option<T>
, which defaults to None
, and form::Result
, which defaults to Err(Missing)
or otherwise collects errors in an Err
of Errors<'_>
. Defaulting guards can be used just like any other form guard:
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,
}
The default can be overridden or unset using the #[field(default = expr)]
field attribute. If expr
is not literally None
, the parameter sets the default value of the field to be expr.into()
. If expr
is None
, the parameter unsets the default value of the field, if any.
#[derive(FromForm)]
struct MyForm {
// Set the default value to be `"hello"`.
//
// Note how an `&str` is automatically converted into a `String`.
#[field(default = "hello")]
greeting: String,
// Remove the default value of `false`, requiring all parses of `MyForm`
// to contain an `is_friendly` field.
#[field(default = None)]
is_friendly: bool,
}
See the FromForm
derive documentation for full details on the default
attribute parameter as well documentation on the more expressive default_with
parameter option.
Field Renaming
By default, Rocket matches the name of an incoming form field to the name of a structure field. While this behavior is typical, it may also be desired to use different names for form fields and struct fields while still parsing as expected. You can ask Rocket to look for a different form field for a given structure field by using one or more #[field(name = "name")]
or #[field(name = uncased("name")]
field annotation. The uncased
variant case-insensitively matches field names.
As an example, say that you're writing an application that receives data from an external service. The external service POST
s a form with a field named first-Name
which you'd like to write as first_name
in Rust. Such a form structure can be written as:
#[derive(FromForm)]
struct External<'r> {
#[field(name = "first-Name")]
first_name: &'r str
}
If you want to accept both firstName
case-insensitively as well as first_name
case-sensitively, you'll need to use two annotations:
#[derive(FromForm)]
struct External<'r> {
#[field(name = uncased("firstName"))]
#[field(name = "first_name")]
first_name: &'r str
}
This will match any casing of firstName
including FirstName
, firstname
, FIRSTname
, and so on, but only match exactly on first_name
.
If instead you wanted to match any of first-name
, first_name
or firstName
, in each instance case-insensitively, you would write:
#[derive(FromForm)]
struct External<'r> {
#[field(name = uncased("first-name"))]
#[field(name = uncased("first_name"))]
#[field(name = uncased("firstname"))]
first_name: &'r str
}
Cased and uncased renamings can be mixed and matched, and any number of renamings is allowed. Rocket will emit an error at compile-time if field names conflict, preventing ambiguous parsing at runtime.
Ad-Hoc Validation
Fields of forms can be easily ad-hoc validated via the #[field(validate)]
attribute. As an example, consider a form field age: u16
which we'd like to ensure is greater than 21
. The following structure accomplishes this:
#[derive(FromForm)]
struct Person {
#[field(validate = range(21..))]
age: u16
}
The expression range(21..)
is a call to form::validate::range
. Rocket passes a borrow of the attributed field, here self.age
, as the first parameter to the function call. The rest of the fields are pass as written in the expression.
Any function in the form::validate
module can be called, and other fields of the form can be passed in by using self.$field
where $field
is the name of the field in the structure. You can also apply more than one validation to a field by using multiple attributes. For example, the following form validates that the value of the field confirm
is equal to the value of the field value
and that it doesn't contain no
:
#[derive(FromForm)]
struct Password<'r> {
#[field(name = "password")]
value: &'r str,
#[field(validate = eq(self.value))]
#[field(validate = omits("no"))]
confirm: &'r str,
}
In reality, the expression after validate =
can be any expression as long as it evaluates to a value of type Result<(), Errors<'_>>
(aliased by form::Result
), where an Ok
value means that validation was successful while an Err
of Errors<'_>
indicates the error(s) that occurred. For instance, if you wanted to implement an ad-hoc Luhn validator for credit-card-like numbers, you might write:
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(())
}
If a field's validation doesn't depend on other fields (validation is local), it is validated prior to those fields that do. For CreditCard
, cvv
and expiration
will be validated prior to number
.
Wrapping Validators
If a particular validation is applied in more than once place, prefer creating a type that encapsulates and represents the validated value. For example, if your application often validates age
fields, consider creating a custom Age
form guard that always applies the validation:
#[derive(FromForm)]
#[field(validate = range(18..150))]
struct Age(u16);
This approach is also useful when a custom validator already exists in some other form. For instance, the following example leverages try_with
and an existing FromStr
implementation on a Token
type to validate a string:
use std::str::FromStr;
#[derive(FromForm)]
#[field(validate = try_with(|s| Token::from_str(s)))]
struct Token<'r>(&'r str);
Query Strings
Query strings are URL-encoded forms that appear in the URL of a request. Query parameters are declared like path parameters but otherwise handled like regular URL-encoded form fields.
Static Parameters
A request matches a route iff its query string contains all of the static parameters in the route's query string. A route with a static parameter param
(any UTF-8 text string) in a query will only match requests with that exact path segment in its query string.
This is truly an iff!
Only the static parameters in query route string affect routing. Dynamic parameters are allowed to be missing by default.
For example, the route below will match requests with path /
and at least the query segments hello
and cat=♥
:
#[get("/?hello&cat=♥")]
fn cats() -> &'static str {
"Hello, kittens!"
}
// The following GET requests match `cats`. `%E2%99%A5` is encoded `♥`.
"/?cat=%E2%99%A5&hello"
"/?hello&cat=%E2%99%A5"
"/?dogs=amazing&hello&there&cat=%E2%99%A5"
Dynamic Parameters
A single dynamic parameter of <param>
acts identically to a form field declared as param
. In particular, Rocket will expect the query form to contain a field with key param
and push the shifted field to the param
type. As with forms, default values are used when parsing fails. The example below illustrates this with a single value name
, a collection color
, a nested form person
, and an other
value that will default to None
:
#[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 }
});
}
// A request with these query segments matches as above.
name=George&\
color=red&\
color=green&\
person.pet.name=Fi+Fo+Alex&\
color=green&\
person.pet.age=1&\
color=blue&\
extra=yes\
Note that, like forms, parsing is field-ordering insensitive and lenient by default.
Trailing Parameter
A trailing dynamic parameter of <param..>
collects all of the query segments that don't otherwise match a declared static or dynamic parameter. In other words, the otherwise unmatched segments are pushed, unshifted, to the <param..>
type:
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);
}
// A request with these query segments matches as above.
hello&\
name=Bob+Smith&\
id=1337&\
active=yes\
Error Catchers
Application processing is fallible. Errors arise from the following sources:
- A failing guard.
- A failing responder.
- A routing failure.
If any of these occur, Rocket returns an error to the client. To generate the error, Rocket invokes the catcher corresponding to the error's status code and scope. Catchers are similar to routes except in that:
- Catchers are only invoked on error conditions.
- Catchers are declared with the
catch
attribute. - Catchers are registered with
register()
instead ofmount()
. - Any modifications to cookies are cleared before a catcher is invoked.
- Error catchers cannot invoke guards.
- Error catchers should not fail to produce a response.
- Catchers are scoped to a path prefix.
To declare a catcher for a given status code, use the catch
attribute, which takes a single integer corresponding to the HTTP status code to catch. For instance, to declare a catcher for 404 Not Found
errors, you'd write:
use rocket::Request;
#[catch(404)]
fn not_found(req: &Request) { /* .. */ }
Catchers may take zero, one, or two arguments. If the catcher takes one argument, it must be of type &Request
. It it takes two, they must be of type Status
and &Request
, in that order. As with routes, the return type must implement Responder
. A concrete implementation may look like:
#[catch(404)]
fn not_found(req: &Request) -> String {
format!("Sorry, '{}' is not a valid path.", req.uri())
}
Also as with routes, Rocket needs to know about a catcher before it is used to handle errors. The process, known as "registering" a catcher, is similar to mounting a route: call the register()
method with a list of catchers via the catchers!
macro. The invocation to add the 404 catcher declared above looks like:
fn main() {
rocket::build().register("/", catchers![not_found]);
}
Scoping
The first argument to register()
is a path to scope the catcher under called the catcher's base. A catcher's base determines which requests it will handle errors for. Specifically, a catcher's base must be a prefix of the erroring request for it to be invoked. When multiple catchers can be invoked, the catcher with the longest base takes precedence.
As an example, consider the following application:
#[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])
}
Since there are no mounted routes, all requests will 404
. Any request whose path begins with /foo
(i.e, GET /foo
, GET /foo/bar
, etc) will be handled by the foo_not_found
catcher while all other requests will be handled by the general_not_found
catcher.
Default Catchers
A default catcher is a catcher that handles all status codes. They are invoked as a fallback if no status-specific catcher is registered for a given error. Declaring a default catcher is done with #[catch(default)]
and must similarly be registered with 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])
}
Catchers with longer bases are preferred, even when there is a status-specific catcher. In other words, a default catcher with a longer matching base than a status-specific catcher takes precedence.
Built-In Catcher
Rocket provides a built-in default catcher. It produces HTML or JSON, depending on the value of the Accept
header. As such, custom catchers only need to be registered for custom error handling.
Responses
You may have noticed that the return type of a handler appears to be arbitrary, and that's because it is! A value of any type that implements the Responder
trait can be returned, including your own. In this section, we describe the Responder
trait as well as several useful Responder
s provided by Rocket. We'll also briefly discuss how to implement your own Responder
.
Responder
Types that implement Responder
know how to generate a Response
from their values. A Response
includes an HTTP status, headers, and body. The body may either be fixed-sized or streaming. The given Responder
implementation decides which to use. For instance, String
uses a fixed-sized body, while File
uses a streamed response. Responders may dynamically adjust their responses according to the incoming Request
they are responding to.
Wrapping
Before we describe a few responders, we note that it is typical for responders to wrap other responders. That is, responders can be of the following form, where R
is some type that implements Responder
:
struct WrappingResponder<R>(R);
A wrapping responder modifies the response returned by R
before responding with that same response. For instance, Rocket provides Responder
s in the status
module that override the status code of the wrapped Responder
. As an example, the Accepted
type sets the status to 202 - Accepted
. It can be used as follows:
use rocket::response::status;
#[post("/<id>")]
fn new(id: usize) -> status::Accepted<String> {
status::Accepted(format!("id: '{}'", id))
}
Similarly, the types in the content
module can be used to override the Content-Type of a response. For instance, to set the Content-Type of &'static str
to JSON, as well as setting the status code to an arbitrary one like 418 I'm a teapot
, combine [content::RawJson
] with status::Custom
:
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\" }"))
}
Errors
Responders may fail instead of generating a response by returning an Err
with a status code. When this happens, Rocket forwards the request to the error catcher for that status code.
If an error catcher has been registered for the given status code, Rocket will invoke it. The catcher creates and returns a response to the client. If no error catcher has been registered and the error status code is one of the standard HTTP status code, a default error catcher will be used. Default error catchers return an HTML page with the status code and description. If there is no catcher for a custom status code, Rocket uses the 500 error catcher to return a response.
Custom Responders
The Responder
trait documentation details how to implement your own custom responders by explicitly implementing the trait. For most use cases, however, Rocket makes it possible to automatically derive an implementation of Responder
. In particular, if your custom responder wraps an existing responder, headers, or sets a custom status or content-type, Responder
can be automatically derived:
use rocket::http::{Header, ContentType};
#[derive(Responder)]
#[response(status = 500, content_type = "json")]
struct MyResponder {
inner: OtherResponder,
// Override the Content-Type declared above.
header: ContentType,
more: Header<'static>,
#[response(ignore)]
unrelated: MyType,
}
For the example above, Rocket generates a Responder
implementation that:
- Set the response's status to
500: Internal Server Error
. - Sets the Content-Type to
application/json
. - Adds the headers
self.header
andself.more
to the response. - Completes the response using
self.inner
.
Note that the first field is used as the inner responder while all remaining fields (unless ignored with #[response(ignore)]
) are added as headers to the response. The optional #[response]
attribute can be used to customize the status and content-type of the response. Because ContentType
is itself a header, you can also dynamically set a content-type by simply including a field of type ContentType
. To set an HTTP status dynamically, leverage the (Status, R: Responder)
responder:
use rocket::http::{Header, Status};
#[derive(Responder)]
#[response(content_type = "json")]
struct MyResponder {
inner: (Status, OtherResponder),
some_header: Header<'static>,
}
You can also use derive Responder
for enum
s, allowing dynamic selection of a responder:
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,
}
}
For more on using the Responder
derive, including details on how to use the derive to define generic responders, see the Responder
derive documentation.
Implementations
Rocket implements Responder
for many types in Rust's standard library including String
, &str
, File
, Option
, and Result
. The Responder
documentation describes these in detail, but we briefly cover a few here.
Strings
The Responder
implementations for &str
and String
are straight-forward: the string is used as a sized body, and the Content-Type of the response is set to text/plain
. To get a taste for what such a Responder
implementation looks like, here's the implementation for String
:
use std::io::Cursor;
use rocket::request::Request;
use rocket::response::{self, Response, Responder};
use rocket::http::ContentType;
#[rocket::async_trait]
impl<'r> Responder<'r, 'static> for String {
fn respond_to(self, _: &'r Request<'_>) -> response::Result<'static> {
Response::build()
.header(ContentType::Plain)
.sized_body(self.len(), Cursor::new(self))
.ok()
}
}
Because of these implementations, you can directly return an &str
or String
type from a handler:
#[get("/string")]
fn handler() -> &'static str {
"Hello there! I'm a string!"
}
Option
Option
is a wrapping responder: an Option<T>
can only be returned when T
implements Responder
. If the Option
is Some
, the wrapped responder is used to respond to the client. Otherwise, an error of 404 - Not Found is returned to the client.
This implementation makes Option
a convenient type to return when it is not known until process-time whether content exists. For example, because of Option
, we can implement a file server that returns a 200
when a file is found and a 404
when a file is not found in just 4, idiomatic lines:
use rocket::fs::NamedFile;
#[get("/<file..>")]
async fn files(file: PathBuf) -> Option<NamedFile> {
NamedFile::open(Path::new("static/").join(file)).await.ok()
}
Result
Result
is another wrapping responder: a Result<T, E>
can only be returned when T
implements Responder
and E
implements Responder
.
The wrapped Responder
in Ok
or Err
, whichever it might be, is used to respond to the client. This means that the responder can be chosen dynamically at run-time, and two different kinds of responses can be used depending on the circumstances. Revisiting our file server, for instance, we might wish to provide more feedback to the user when a file isn't found. We might do this as follows:
use rocket::fs::NamedFile;
use rocket::response::status::NotFound;
#[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()))
}
Rocket Responders
Some of Rocket's best features are implemented through responders. Among these are:
NamedFile
- Streams a file to the client; automatically sets the Content-Type based on the file's extension.Redirect
- Redirects the client to a different URI.content
- Contains types that override the Content-Type of a response.status
- Contains types that override the status code of a response.Flash
- Sets a "flash" cookie that is removed when accessed.Json
- Automatically serializes values into JSON.MsgPack
- Automatically serializes values into MessagePack.Template
- Renders a dynamic template using handlebars or Tera.
Async Streams
The stream
responders allow serving potentially infinite async Stream
s. A stream can be created from any async Stream
or AsyncRead
type, or via generator syntax using the stream!
macro and its typed equivalents. Streams are the building blocks for unidirectional real-time communication. For instance, the chat
example uses an EventStream
to implement a real-time, multi-room chat application using Server-Sent Events (SSE).
The simplest version creates a ReaderStream
from a single AsyncRead
type. For example, to stream from a TCP connection, we might write:
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))
}
Streams can also be created using generator syntax. The following example returns an infinite TextStream
that produces one "hello"
every second:
use rocket::tokio::time::{Duration, interval};
use rocket::response::stream::TextStream;
/// Produce an infinite series of `"hello"`s, one per second.
#[get("/infinite-hellos")]
fn hello() -> TextStream![&'static str] {
TextStream! {
let mut interval = interval(Duration::from_secs(1));
loop {
yield "hello";
interval.tick().await;
}
}
}
See the stream
docs for full details on creating streams including notes on how to detect and handle graceful shutdown requests.
WebSockets
Enabled by Rocket's support for HTTP connection upgrades, the official rocket_ws
crate implements first-class support for WebSockets. Working with rocket_ws
to implement an echo server looks like this:
use rocket_ws::{WebSocket, Stream};
#[get("/echo")]
fn echo_compose(ws: WebSocket) -> Stream!['static] {
ws.stream(|io| io)
}
As with async
streams, rocket_ws
also supports using generator syntax for WebSocket messages:
use rocket_ws::{WebSocket, Stream};
#[get("/echo")]
fn echo_stream(ws: WebSocket) -> Stream!['static] {
Stream! { ws =>
for await message in ws {
yield message?;
}
}
}
For complete usage details, see the rocket_ws
documentation.
JSON
The Json
responder in allows you to easily respond with well-formed JSON data: simply return a value of type Json<T>
where T
is the type of a structure to serialize into JSON. The type T
must implement the Serialize
trait from serde
, which can be automatically derived.
As an example, to respond with the JSON value of a Task
structure, we might write:
use rocket::serde::{Serialize, json::Json};
#[derive(Serialize)]
#[serde(crate = "rocket::serde")]
struct Task { /* .. */ }
#[get("/todo")]
fn todo() -> Json<Task> {
Json(Task { /* .. */ })
}
You must enable Rocket's
json
crate feature to use theJson
type.
Templates
Rocket has first-class templating support that works largely through a Template
responder in the rocket_dyn_templates
contrib library. To render a template named "index", for instance, you might return a value of type Template
as follows:
use rocket_dyn_templates::Template;
#[get("/")]
fn index() -> Template {
let context = /* object-like value */;
Template::render("index", &context)
}
Templates are rendered with the render
method. The method takes in the name of a template and a context to render the template with. The context can be any type that implements Serialize
and serializes into an Object
value, such as structs, HashMaps
, and others.
You can also use context!
to create ad-hoc templating contexts without defining a new type:
use rocket_dyn_templates::Template;
#[get("/")]
fn index() -> Template {
Template::render("index", context! {
foo: 123,
})
}
To render a template, it must first be registered. The Template
fairing automatically registers all discoverable templates when attached. The Fairings sections of the guide provides more information on fairings. To attach the template fairing, simply call .attach(Template::fairing())
on an instance of Rocket
as follows:
use rocket_dyn_templates::Template;
#[launch]
fn rocket() -> _ {
rocket::build()
.mount("/", routes![/* .. */])
.attach(Template::fairing())
}
Rocket discovers templates in the configurable template_dir
directory. Templating support in Rocket is engine agnostic. The engine used to render a template depends on the template file's extension. For example, if a file ends with .hbs
, Handlebars is used, while if a file ends with .tera
, Tera is used.
Live Reloading
When your application is compiled in debug
mode (without the --release
flag passed to cargo
), templates are automatically reloaded when they are modified on supported platforms. This means that you don't need to rebuild your application to observe template changes: simply refresh! In release builds, reloading is disabled.
Typed URIs
Rocket's uri!
macro allows you to build URIs to routes in your application in a robust, type-safe, and URI-safe manner. Type or route parameter mismatches are caught at compile-time, and changes to route URIs are automatically reflected in the generated URIs.
The uri!
macro returns an Origin
structure with the URI of the supplied route interpolated with the given values. Each value passed into uri!
is rendered in its appropriate place in the URI using the UriDisplay
implementation for the value's type. The UriDisplay
implementation ensures that the rendered value is URI-safe.
Note that Origin
implements Into<Uri>
(and by extension, TryInto<Uri>
), so it can be converted into a Uri
using .into()
as needed and passed into methods such as Redirect::to()
.
For example, given the following route:
#[get("/<id>/<name>?<age>")]
fn person(id: Option<usize>, name: &str, age: Option<u8>) { /* .. */ }
URIs to person
can be created as follows:
// with unnamed parameters, in route path declaration order
let mike = uri!(person(101, "Mike Smith", Some(28)));
assert_eq!(mike.to_string(), "/101/Mike%20Smith?age=28");
// with named parameters, order irrelevant
let mike = uri!(person(name = "Mike", id = 101, age = Some(28)));
assert_eq!(mike.to_string(), "/101/Mike?age=28");
let mike = uri!(person(id = 101, age = Some(28), name = "Mike"));
assert_eq!(mike.to_string(), "/101/Mike?age=28");
// with a specific mount-point
let mike = uri!("/api", person(id = 101, name = "Mike", age = Some(28)));
assert_eq!(mike.to_string(), "/api/101/Mike?age=28");
// with optional (defaultable) query parameters ignored
let mike = uri!(person(101, "Mike", _));
assert_eq!(mike.to_string(), "/101/Mike");
let mike = uri!(person(id = 101, name = "Mike", age = _));
assert_eq!(mike.to_string(), "/101/Mike");
Rocket informs you of any mismatched parameters at compile-time:
error: `person` route uri expects 3 parameters but 1 was supplied
--> examples/uri/main.rs:7:26
|
7 | let x = uri!(person("Mike Smith"));
| ^^^^^^^^^^^^
|
= note: expected parameters: id: Option <usize>, name: &str, age: Option <u8>
Rocket also informs you of any type errors at compile-time:
--> examples/uri/src/main.rs:7:31
|
7 | let x = uri!(person(id = "10", name = "Mike Smith", age = Some(10)));
| ^^^^ `FromUriParam<Path, &str>` is not implemented for `usize`
We recommend that you use uri!
exclusively when constructing URIs to your routes.
Ignorables
As illustrated in the previous above, query parameters can be ignored using _
in place of an expression in a uri!
invocation. The corresponding type in the route URI must implement Ignorable
. Ignored parameters are not interpolated into the resulting Origin
. Path parameters are not ignorable.
DerivingUriDisplay
The UriDisplay
trait can be derived for custom types. For types that appear in the path part of a URI, derive using UriDisplayPath
; for types that appear in the query part of a URI, derive using UriDisplayQuery
.
As an example, consider the following form structure and route:
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) { /* .. */ }
By deriving using UriDisplayQuery
, an implementation of UriDisplay<Query>
is automatically generated, allowing for URIs to add_user
to be generated using uri!
:
let link = uri!(add_user(120, UserDetails { age: Some(20), nickname: "Bob".into() }));
assert_eq!(link.to_string(), "/user/120?age=20&nickname=Bob");
State
Many web applications have a need to maintain state. This can be as simple as maintaining a counter for the number of visits or as complex as needing to access job queues and multiple databases. Rocket provides the tools to enable these kinds of interactions in a safe and simple manner.
Managed State
The enabling feature for maintaining state is managed state. Managed state, as the name implies, is state that Rocket manages for your application. The state is managed on a per-type basis: Rocket will manage at most one value of a given type.
The process for using managed state is simple:
- Call
manage
on theRocket
instance corresponding to your application with the initial value of the state. - Add a
&State<T>
type to any request handler, whereT
is the type of the value passed intomanage
.
All managed state must be thread-safe.
Because Rocket automatically parallelizes your application, handlers can concurrently access managed state. As a result, managed state must be thread-safe. Thanks to Rust, this condition is checked at compile-time by ensuring that the type of values you store in managed state implement Send
+ Sync
.
Adding State
To instruct Rocket to manage state for your application, call the manage
method on an instance of Rocket
. For example, to ask Rocket to manage a HitCount
structure with an internal AtomicUsize
with an initial value of 0
, we can write the following:
use std::sync::atomic::AtomicUsize;
struct HitCount {
count: AtomicUsize
}
rocket::build().manage(HitCount { count: AtomicUsize::new(0) });
The manage
method can be called any number of times as long as each call refers to a value of a different type. For instance, to have Rocket manage both a HitCount
value and a Config
value, we can write:
rocket::build()
.manage(HitCount { count: AtomicUsize::new(0) })
.manage(Config::from(user_input));
Retrieving State
State that is being managed by Rocket can be retrieved via the &State
type: a request guard for managed state. To use the request guard, add a &State<T>
type to any request handler, where T
is the type of the managed state. For example, we can retrieve and respond with the current HitCount
in a count
route as follows:
use rocket::State;
#[get("/count")]
fn count(hit_count: &State<HitCount>) -> String {
let current_count = hit_count.count.load(Ordering::Relaxed);
format!("Number of visits: {}", current_count)
}
You can retrieve more than one &State
type in a single route as well:
#[get("/state")]
fn state(hit_count: &State<HitCount>, config: &State<Config>) { /* .. */ }
If you request a
&State<T>
for aT
that is notmanaged
, Rocket will refuse to start your application. This prevents what would have been an unmanaged state runtime error. Unmanaged state is detected at runtime through sentinels, so there are limitations. If a limitation is hit, Rocket still won't call the offending route. Instead, Rocket will log an error message and return a 500 error to the client.
Within Guards
Because State
is itself a request guard, managed state can be retrieved from another request guard's implementation using either Request::guard()
or Rocket::state()
. In the following code example, the Item
request guard retrieves MyConfig
from managed state using both methods:
use rocket::State;
use rocket::request::{self, Request, FromRequest};
use rocket::outcome::IntoOutcome;
use rocket::http::Status;
struct Item<'r>(&'r str);
#[rocket::async_trait]
impl<'r> FromRequest<'r> for Item<'r> {
type Error = ();
async fn from_request(request: &'r Request<'_>) -> request::Outcome<Self, ()> {
// Using `State` as a request guard. Use `inner()` to get an `'r`.
let outcome = request.guard::<&State<MyConfig>>().await
.map(|my_config| Item(&my_config.user_val));
// Or alternatively, using `Rocket::state()`:
let outcome = request.rocket().state::<MyConfig>()
.map(|my_config| Item(&my_config.user_val))
.or_forward(Status::InternalServerError);
outcome
}
}
Fairings
Fairings are Rocket's approach to structured middleware. With fairings, your application can hook into the request lifecycle to record or rewrite information about incoming requests and outgoing responses.
Overview
Any type that implements the Fairing
trait is a fairing. Fairings hook into Rocket's request lifecycle, receiving callbacks for events such as incoming requests and outgoing responses. Rocket passes information about these events to the fairing; the fairing can do what it wants with the information. This includes rewriting requests or responses, recording information about the event, or doing nothing at all.
Rocket’s fairings are a lot like middleware from other frameworks, but they bear a few key distinctions:
- Fairings cannot terminate or respond to an incoming request directly.
- Fairings cannot inject arbitrary, non-request data into a request.
- Fairings can prevent an application from launching.
- Fairings can inspect and modify the application's configuration.
If you are familiar with middleware from other frameworks, you may find yourself reaching for fairings instinctively. Before doing so, remember that Rocket provides a rich set of mechanisms such as request guards and data guards that can be used to solve problems in a clean, composable, and robust manner.
As a general rule of thumb, only globally applicable actions should be effected through fairings. You should not use a fairing to implement authentication or authorization (preferring to use a request guard instead) unless the authentication or authorization applies to all or the overwhelming majority of the application. On the other hand, you should use a fairing to record timing and usage statistics or to enforce global security policies.
Attaching
Fairings are registered with Rocket via the attach
method on a Rocket
instance. Only when a fairing is attached will its callbacks fire. As an example, the following snippet attached two fairings, req_fairing
and res_fairing
, to a new Rocket instance:
#[launch]
fn rocket() -> _ {
rocket::build()
.attach(req_fairing)
.attach(res_fairing)
}
Fairings are executed in the order in which they are attached: the first attached fairing has its callbacks executed before all others. A fairing can be attached any number of times. Except for singleton fairings, all attached instances are polled at runtime. Fairing callbacks may not be commutative; the order in which fairings are attached may be significant.
Callbacks
There are five events for which Rocket issues fairing callbacks. Each of these events is briefly described below and in details in the Fairing
trait docs:
-
Ignite (
on_ignite
)An ignite callback is called during ignition An ignite callback can arbitrarily modify the
Rocket
instance being built. They are commonly used to parse and validate configuration values, aborting on bad configurations, and inserting the parsed value into managed state for later retrieval. -
Liftoff (
on_liftoff
)A liftoff callback is called immediately after a Rocket application has launched. A liftoff callback can inspect the
Rocket
instance being launched. A liftoff callback can be a convenient hook for launching services related to the Rocket application being launched. -
Request (
on_request
)A request callback is called just after a request is received. A request callback can modify the request at will and peek into the incoming data. It may not, however, abort or respond directly to the request; these issues are better handled via request guards or via response callbacks.
-
Response (
on_response
)A response callback is called when a response is ready to be sent to the client. A response callback can modify part or all of the response. As such, a response fairing can be used to provide a response when the greater application fails by rewriting 404 responses as desired. As another example, response fairings can also be used to inject headers into all outgoing responses.
-
Shutdown (
on_shutdown
)A shutdown callback is called when shutdown is triggered. At this point, graceful shutdown has commenced but not completed; no new requests are accepted but the application may still be actively serving existing requests. All registered shutdown fairings are run concurrently; resolution of all fairings is awaited before resuming shutdown.
Implementing
Recall that a fairing is any type that implements the Fairing
trait. A Fairing
implementation has one required method: info
, which returns an Info
structure. This structure is used by Rocket to assign a name to the fairing and determine the set of callbacks the fairing is registering for. A Fairing
can implement any of the available callbacks: on_ignite
, on_liftoff
, on_request
, on_response
, and on_shutdown
. Each callback has a default implementation that does absolutely nothing.
Requirements
A type implementing Fairing
is required to be Send + Sync + 'static
. This means that the fairing must be sendable across thread boundaries (Send
), thread-safe (Sync
), and have only static references, if any ('static
). Note that these bounds do not prohibit a Fairing
from holding state: the state need simply be thread-safe and statically available or heap allocated.
Example
As an example, we want to record the number of GET
and POST
requests that our application has received. While we could do this with request guards and managed state, it would require us to annotate every GET
and POST
request with custom types, polluting handler signatures. Instead, we can create a simple fairing that acts globally.
The code for a Counter
fairing below implements exactly this. The fairing receives a request callback, where it increments a counter on each GET
and POST
request. It also receives a response callback, where it responds to unrouted requests to the /counts
path by returning the recorded number of counts.
use std::io::Cursor;
use std::sync::atomic::{AtomicUsize, Ordering};
use rocket::{Request, Data, Response};
use rocket::fairing::{Fairing, Info, Kind};
use rocket::http::{Method, ContentType, Status};
struct Counter {
get: AtomicUsize,
post: AtomicUsize,
}
#[rocket::async_trait]
impl Fairing for Counter {
// This is a request and response fairing named "GET/POST Counter".
fn info(&self) -> Info {
Info {
name: "GET/POST Counter",
kind: Kind::Request | Kind::Response
}
}
// Increment the counter for `GET` and `POST` requests.
async fn on_request(&self, request: &mut Request<'_>, _: &mut Data<'_>) {
match request.method() {
Method::Get => self.get.fetch_add(1, Ordering::Relaxed),
Method::Post => self.post.fetch_add(1, Ordering::Relaxed),
_ => return
};
}
async fn on_response<'r>(&self, request: &'r Request<'_>, response: &mut Response<'r>) {
// Don't change a successful user's response, ever.
if response.status() != Status::NotFound {
return
}
// Rewrite the response to return the current counts.
if request.method() == Method::Get && request.uri().path() == "/counts" {
let get_count = self.get.load(Ordering::Relaxed);
let post_count = self.post.load(Ordering::Relaxed);
let body = format!("Get: {}\nPost: {}", get_count, post_count);
response.set_status(Status::Ok);
response.set_header(ContentType::Plain);
response.set_sized_body(body.len(), Cursor::new(body));
}
}
}
The complete example can be found in the Fairing
documentation.
Ad-Hoc Fairings
For simpler cases, implementing the Fairing
trait can be cumbersome. This is why Rocket provides the AdHoc
type, which creates a fairing from a simple function or closure. Using the AdHoc
type is easy: simply call the on_ignite
, on_liftoff
, on_request
, on_response
, or on_shutdown
constructors on AdHoc
to create a fairing from a function or closure.
As an example, the code below creates a Rocket
instance with two attached ad-hoc fairings. The first, a liftoff fairing named "Liftoff Printer", prints a message indicating that the application has launched. The second named "Put Rewriter", a request fairing, rewrites the method of all requests to be PUT
.
use rocket::fairing::AdHoc;
use rocket::http::Method;
rocket::build()
.attach(AdHoc::on_liftoff("Liftoff Printer", |_| Box::pin(async move {
println!("...annnddd we have liftoff!");
})))
.attach(AdHoc::on_request("Put Rewriter", |req, _| Box::pin(async move {
req.set_method(Method::Put);
})))
.attach(AdHoc::on_shutdown("Shutdown Printer", |_| Box::pin(async move {
println!("...shutdown has commenced!");
})));
Testing
Every application should be well tested and understandable. Rocket provides the tools to perform unit and integration tests. It also provides a means to inspect code generated by Rocket.
Local Dispatching
Rocket applications are tested by dispatching requests to a local instance of Rocket
. The local
module contains all of the structures necessary to do so. In particular, it contains a Client
structure that is used to create LocalRequest
structures that can be dispatched against a given Rocket
instance. Usage is straightforward:
- Construct a
Rocket
instance that represents the application.
let rocket = rocket::build();
- Construct a
Client
using theRocket
instance.
let client = Client::tracked(rocket).unwrap();
- Construct requests using the
Client
instance.
let req = client.get("/");
- Dispatch the request to retrieve the response.
let response = req.dispatch();
Validating Responses
A dispatch
of a LocalRequest
returns a LocalResponse
which can be inspected for validity. During testing, the response is usually validated against expected properties. These includes things like the response HTTP status, the inclusion of headers, and expected body data.
LocalResponse
type provides methods to ease this sort of validation. We list a few below:
status
: returns the HTTP status in the response.content_type
: returns the Content-Type header in the response.headers
: returns a map of all of the headers in the response.into_string
: reads the body data into aString
.into_bytes
: reads the body data into aVec<u8>
.into_json
: deserializes the body data on-the-fly as JSON.into_msgpack
: deserializes the body data on-the-fly as MessagePack.
These methods are typically used in combination with the assert_eq!
or assert!
macros as follows:
use rocket::http::{ContentType, Status};
let mut response = client.get(uri!(hello)).dispatch();
assert_eq!(response.status(), Status::Ok);
assert_eq!(response.content_type(), Some(ContentType::Plain));
assert!(response.headers().get_one("X-Special").is_some());
assert_eq!(response.into_string().unwrap(), "Expected Body");
Testing "Hello, world!"
To solidify an intuition for how Rocket applications are tested, we walk through how to test the "Hello, world!" application below:
#[get("/")]
fn hello() -> &'static str {
"Hello, world!"
}
#[launch]
fn rocket() -> _ {
rocket::build().mount("/", routes![hello])
}
Notice that we've separated the creation of the Rocket
instance from the launch of the instance. As you'll soon see, this makes testing our application easier, less verbose, and less error-prone.
Setting Up
First, we'll create a test
module with the proper imports:
#[cfg(test)]
mod test {
use super::rocket;
use rocket::local::blocking::Client;
use rocket::http::Status;
#[test]
fn hello_world() {
/* .. */
}
}
You can also move the body of the test
module into its own file, say tests.rs
, and then import the module into the main file using:
#[cfg(test)] mod tests;
Testing
To test our "Hello, world!" application, we create a Client
for our Rocket
instance. It's okay to use methods like expect
and unwrap
during testing: we want our tests to panic when something goes wrong.
let client = Client::tracked(rocket()).expect("valid rocket instance");
Then, we create a new GET /
request and dispatch it, getting back our application's response:
let mut response = client.get(uri!(hello)).dispatch();
Finally, we ensure that the response contains the information we expect it to. Here, we want to ensure two things:
- The status is
200 OK
. - The body is the string "Hello, world!".
We do this by checking the Response
object directly:
use rocket::http::{ContentType, Status};
assert_eq!(response.status(), Status::Ok);
assert_eq!(response.into_string(), Some("Hello, world!".into()));
That's it! Altogether, this looks like:
#[get("/")]
fn hello() -> &'static str {
"Hello, world!"
}
#[launch]
fn rocket() -> Rocket<Build> {
rocket::build().mount("/", routes![hello])
}
#[cfg(test)]
mod test {
use super::rocket;
use rocket::local::blocking::Client;
use rocket::http::Status;
#[test]
fn hello_world() {
let client = Client::tracked(rocket()).expect("valid rocket instance");
let mut response = client.get(uri!(super::hello)).dispatch();
assert_eq!(response.status(), Status::Ok);
assert_eq!(response.into_string().unwrap(), "Hello, world!");
}
}
The tests can be run with cargo test
. You can find the full source code to this example on GitHub.
Asynchronous Testing
You may have noticed the use of a "blocking
" API in these examples, even though Rocket
is an async
web framework. In most situations, the blocking
testing API is easier to use and should be preferred. However, when concurrent execution of two or more requests is required for the server to make progress, you will need the more flexible asynchronous
API; the blocking
API is not capable of dispatching multiple requests simultaneously. While synthetic, the async_required
testing
example uses an async
barrier to demonstrate such a case. For more information, see the rocket::local
and rocket::local::asynchronous
documentation.
Configuration
Rocket's configuration system is flexible. Based on Figment, it allows you to configure your application the way you want while also providing with a sensible set of defaults.
Overview
Rocket's configuration system is based on Figment's Provider
s, types which provide configuration data. Rocket's Config
and Config::figment()
, as well as Figment's Toml
and Json
, are some examples of providers. Providers can be combined into a single Figment
provider from which any configuration structure that implements Deserialize
can be extracted.
Rocket expects to be able to extract a Config
structure from the provider it is configured with. This means that no matter which configuration provider Rocket is asked to use, it must be able to read the following configuration values:
key | kind | description | debug/release default |
---|---|---|---|
address |
IpAddr |
IP address to serve on | 127.0.0.1 |
port |
u16 |
Port to serve on. | 8000 |
workers * |
usize |
Number of threads to use for executing futures. | cpu core count |
max_blocking * |
usize |
Limit on threads to start for blocking tasks. | 512 |
ident |
string , false |
If and how to identify via the Server header. |
"Rocket" |
ip_header |
string , false |
IP header to inspect to get client's real IP. | "X-Real-IP" |
keep_alive |
u32 |
Keep-alive timeout seconds; disabled when 0 . |
5 |
log_level |
LogLevel |
Max level to log. (off/normal/debug/critical) | normal /critical |
cli_colors |
bool |
Whether to use colors and emoji when logging. | true |
secret_key |
SecretKey |
Secret key for signing and encrypting values. | None |
tls |
TlsConfig |
TLS configuration, if any. | None |
limits |
Limits |
Streaming read size limits. | Limits::default() |
limits.$name |
&str /uint |
Read limit for $name . |
form = "32KiB" |
ctrlc |
bool |
Whether ctrl-c initiates a server shutdown. |
true |
shutdown * |
Shutdown |
Graceful shutdown configuration. | Shutdown::default() |
- Note: the
workers
,max_blocking
, andshutdown.force
configuration parameters are only read from the default provider.
Profiles
Configurations can be arbitrarily namespaced by Profile
s. Rocket's Config
and Config::figment()
providers automatically set the configuration profile to "debug" when compiled in "debug" mode and "release" when compiled in release mode, but you can arbitrarily name and set profiles to your desire. For example, with the default provider, you can set the selected profile via ROCKET_PROFILE
. This results in Rocket preferring the values in the ROCKET_PROFILE
profile.
In addition to any profiles you declare, there are two meta-profiles, default
and global
, which can be used to provide values that apply to all profiles. Values provided in a default
profile are used as fall-back values when the selected profile doesn't contain a requested value, while values in the global
profile supplant any values with the same name in any profile.
Default Provider
Rocket's default configuration provider is Config::figment()
; this is the provider that's used when calling rocket::build()
.
The default figment reads from and merges, at a per-key level, the following sources in ascending priority order:
Config::default()
, which provides default values for all parameters.Rocket.toml
or TOML file path inROCKET_CONFIG
environment variable.ROCKET_
prefixed environment variables.
The selected profile is the value of the ROCKET_PROFILE
environment variable, or if it is not set, "debug" when compiled in debug mode and "release" when compiled in release mode. With the exception of log_level
, which changes from normal
in debug to critical
in release, all of the default configuration values are the same in all profiles. What's more, all configuration values have defaults, so no configuration is needed to get started.
As a result of Config::figment()
, without any effort, Rocket can be configured via a Rocket.toml
file and/or via environment variables, the latter of which take precedence over the former.
Rocket.toml
Rocket searches for Rocket.toml
or the filename in a ROCKET_CONFIG
environment variable starting at the current working directory. If it is not found, the parent directory, its parent, and so on, are searched until the file is found or the root is reached. If the path set in ROCKET_CONFIG
is absolute, no such search occurs and the set path is used directly.
The file is assumed to be nested, so each top-level key declares a profile and its values the value for the profile. The following is an example of what such a file might look like:
## defaults for _all_ profiles
[default]
address = "0.0.0.0"
limits = { form = "64 kB", json = "1 MiB" }
## set only when compiled in debug mode, i.e, `cargo build`
[debug]
port = 8000
## only the `json` key from `default` will be overridden; `form` will remain
limits = { json = "10MiB" }
## set only when the `nyc` profile is selected
[nyc]
port = 9001
## set only when compiled in release mode, i.e, `cargo build --release`
[release]
port = 9999
ip_header = false
secret_key = "hPrYyЭRiMyµ5sBB1π+CMæ1køFsåqKvBiQJxBVHQk="
The following is a Rocket.toml
file with all configuration options set for demonstration purposes. You do not and should not set a value for configuration options needlessly, preferring to use the default value when sensible.
[default]
address = "127.0.0.1"
port = 8000
workers = 16
max_blocking = 512
keep_alive = 5
ident = "Rocket"
ip_header = "X-Real-IP" # set to `false` to disable
log_level = "normal"
temp_dir = "/tmp"
cli_colors = true
secret_key = "hPrYyЭRiMyµ5sBB1π+CMæ1køFsåqKvBiQJxBVHQk="
[default.limits]
form = "64 kB"
json = "1 MiB"
msgpack = "2 MiB"
"file/jpg" = "5 MiB"
[default.tls]
certs = "path/to/cert-chain.pem"
key = "path/to/key.pem"
[default.shutdown]
ctrlc = true
signals = ["term", "hup"]
grace = 5
mercy = 5
Environment Variables
Rocket reads all environment variable names prefixed with ROCKET_
using the string after the _
as the name of a configuration value as the value of the parameter as the value itself. Environment variables take precedence over values in Rocket.toml
. Values are parsed as loose form of TOML syntax. Consider the following examples:
ROCKET_FLOAT=3.14
ROCKET_ARRAY=[1,"b",3.14]
ROCKET_STRING=Hello
ROCKET_STRING="Hello There"
ROCKET_KEEP_ALIVE=1
ROCKET_IDENT=Rocket
ROCKET_IDENT="Hello Rocket"
ROCKET_IDENT=false
ROCKET_TLS={certs="abc",key="foo/bar"}
ROCKET_LIMITS={form="64 KiB"}
Deployment
In a containerization environment, you are responsible for writing a Dockerfile
or Containerfile
which you provide to an application platform.
Below you'll find an example of a Dockerfile
that:
- Builds the application with the latest stable Rust compiler.
- Uses
--mount=type=cache
to avoid recompiling dependencies. - Uses a second stage to create a slim (~100MiB), ready-to-deploy image with only what's needed.
- Bundles all of an application's assets in the container.
FROM docker.io/rust:1-slim-bookworm AS build
## cargo package name: customize here or provide via --build-arg
ARG pkg=rocket-app
WORKDIR /build
COPY . .
RUN --mount=type=cache,target=/build/target \
--mount=type=cache,target=/usr/local/cargo/registry \
--mount=type=cache,target=/usr/local/cargo/git \
set -eux; \
cargo build --release; \
objcopy --compress-debug-sections target/release/$pkg ./main
################################################################################
FROM docker.io/debian:bookworm-slim
WORKDIR /app
## copy the main binary
COPY --from=build /build/main ./
## copy runtime assets which may or may not exist
COPY --from=build /build/Rocket.tom[l] ./static
COPY --from=build /build/stati[c] ./static
COPY --from=build /build/template[s] ./templates
## ensure the container listens globally on port 8080
ENV ROCKET_ADDRESS=0.0.0.0
ENV ROCKET_PORT=8080
CMD ./main
You will need to modify the pkg
ARG
or provide it via the command-line:
docker build --build-arg pkg=cargo_package_name -t app .
You may also need to make the following changes:
- Add/remove/modify
ENV
variables as needed. - Modify the expected
target/release/$pkg
directory. - Add more assets to
COPY
to the final image.
Finally, we recommend the following .dockerignore
file to avoid copying unnecessary artifacts:
target
.cargo
**/*.sh
**/*.tar.gz