Set cookies even on error responses.

Fixes #1213.
This commit is contained in:
Sergio Benitez 2020-01-23 21:07:11 -08:00
parent 1ae3e68087
commit f35e3c4aca
5 changed files with 99 additions and 24 deletions

View File

@ -27,7 +27,7 @@ time = "0.2.4"
indexmap = "1.0" indexmap = "1.0"
rustls = { version = "0.16", optional = true } rustls = { version = "0.16", optional = true }
state = "0.4" state = "0.4"
cookie = { version = "0.13", features = ["percent-encode"] } cookie = { version = "0.13.1", features = ["percent-encode"] }
pear = "0.1" pear = "0.1"
unicode-xid = "0.2" unicode-xid = "0.2"

View File

@ -236,6 +236,17 @@ impl<'a> Cookies<'a> {
} }
} }
/// Removes all delta cookies.
/// WARNING: This is unstable! Do not use this method outside of Rocket!
#[inline]
#[doc(hidden)]
pub fn reset_delta(&mut self) {
match *self {
Cookies::Jarred(ref mut jar, _) => jar.reset_delta(),
Cookies::Empty(ref mut jar) => jar.reset_delta()
}
}
/// Returns an iterator over all of the cookies present in this collection. /// Returns an iterator over all of the cookies present in this collection.
/// ///
/// # Example /// # Example

View File

@ -239,15 +239,8 @@ impl Rocket {
request: &'r Request<'s>, request: &'r Request<'s>,
data: Data data: Data
) -> Response<'r> { ) -> Response<'r> {
match self.route(request, data) { let mut response = match self.route(request, data) {
Outcome::Success(mut response) => { Outcome::Success(response) => response,
// A user's route responded! Set the cookies.
for cookie in request.cookies().delta() {
response.adjoin_header(cookie);
}
response
}
Outcome::Forward(data) => { Outcome::Forward(data) => {
// There was no matching route. Autohandle `HEAD` requests. // There was no matching route. Autohandle `HEAD` requests.
if request.method() == Method::Head { if request.method() == Method::Head {
@ -255,14 +248,24 @@ impl Rocket {
// Dispatch the request again with Method `GET`. // Dispatch the request again with Method `GET`.
request._set_method(Method::Get); request._set_method(Method::Get);
self.route_and_process(request, data)
// Return early so we don't set cookies twice.
return self.route_and_process(request, data);
} else { } else {
// No match was found and it can't be autohandled. 404. // No match was found and it can't be autohandled. 404.
self.handle_error(Status::NotFound, request) self.handle_error(Status::NotFound, request)
} }
} }
Outcome::Failure(status) => self.handle_error(status, request) Outcome::Failure(status) => self.handle_error(status, request),
};
// Set the cookies. Note that error responses will only include cookies
// set by the error handler. See `handle_error` for more.
for cookie in request.cookies().delta() {
response.adjoin_header(cookie);
} }
response
} }
/// Tries to find a `Responder` for a given `request`. It does this by /// Tries to find a `Responder` for a given `request`. It does this by
@ -306,10 +309,11 @@ impl Rocket {
} }
// Finds the error catcher for the status `status` and executes it for the // Finds the error catcher for the status `status` and executes it for the
// given request `req`. If a user has registered a catcher for `status`, the // given request `req`; the cookies in `req` are reset to their original
// catcher is called. If the catcher fails to return a good response, the // state before invoking the error handler. If a user has registered a
// 500 catcher is executed. If there is no registered catcher for `status`, // catcher for `status`, the catcher is called. If the catcher fails to
// the default catcher is used. // return a good response, the 500 catcher is executed. If there is no
// registered catcher for `status`, the default catcher is used.
pub(crate) fn handle_error<'r>( pub(crate) fn handle_error<'r>(
&self, &self,
status: Status, status: Status,
@ -317,6 +321,11 @@ impl Rocket {
) -> Response<'r> { ) -> Response<'r> {
warn_!("Responding with {} catcher.", Paint::red(&status)); warn_!("Responding with {} catcher.", Paint::red(&status));
// For now, we reset the delta state to prevent any modifications from
// earlier, unsuccessful paths from being reflected in error response.
// We may wish to relax this in the future.
req.cookies().reset_delta();
// Try to get the active catcher but fallback to user's 500 catcher. // Try to get the active catcher but fallback to user's 500 catcher.
let catcher = self.catchers.get(&status.code).unwrap_or_else(|| { let catcher = self.catchers.get(&status.code).unwrap_or_else(|| {
error_!("No catcher found for {}. Using 500 catcher.", status); error_!("No catcher found for {}. Using 500 catcher.", status);

View File

@ -0,0 +1,47 @@
#![feature(proc_macro_hygiene)]
#[macro_use] extern crate rocket;
use rocket::request::Request;
use rocket::http::{Cookie, Cookies};
#[catch(404)]
fn not_found(request: &Request) -> &'static str {
request.cookies().add(Cookie::new("not_found", "hi"));
"404 - Not Found"
}
#[get("/")]
fn index(mut cookies: Cookies) -> &'static str {
cookies.add(Cookie::new("index", "hi"));
"Hello, world!"
}
mod tests {
use super::*;
use rocket::local::Client;
use rocket::fairing::AdHoc;
#[test]
fn error_catcher_sets_cookies() {
let rocket = rocket::ignite()
.mount("/", routes![index])
.register(catchers![not_found])
.attach(AdHoc::on_request("Add Fairing Cookie", |req, _| {
req.cookies().add(Cookie::new("fairing", "hi"));
}));
let client = Client::new(rocket).unwrap();
// Check that the index returns the `index` and `fairing` cookie.
let response = client.get("/").dispatch();
let cookies = response.cookies();
assert_eq!(cookies.len(), 2);
assert!(cookies.iter().find(|c| c.name() == "index").is_some());
assert!(cookies.iter().find(|c| c.name() == "fairing").is_some());
// Check that the catcher returns only the `not_found` cookie.
let response = client.get("/not-existent").dispatch();
assert_eq!(response.cookies(), vec![Cookie::new("not_found", "hi")]);
}
}

View File

@ -918,13 +918,20 @@ Routing may fail for a variety of reasons. These include:
* No routes matched. * No routes matched.
If any of these conditions occur, Rocket returns an error to the client. To do If any of these conditions occur, Rocket returns an error to the client. To do
so, Rocket invokes the _catcher_ corresponding to the error's status code. A so, Rocket invokes the _catcher_ corresponding to the error's status code.
catcher is like a route, except it only handles errors. Rocket provides default Catchers are similar to routes except in that:
catchers for all of the standard HTTP error codes. To override a default
catcher, or declare a catcher for a custom status code, use the [`catch`] 1. Catchers are only invoked on error conditions.
attribute, which takes a single integer corresponding to the HTTP status code to 2. Catchers are declared with the `catch` attribute.
catch. For instance, to declare a catcher for `404 Not Found` errors, you'd 3. Catchers are _registered_ with [`register()`] instead of [`mount()`].
write: 4. Any modifications to cookies are cleared before a catcher is invoked.
5. Error catchers cannot invoke guards of any sort.
Rocket provides default catchers for all of the standard HTTP error codes. To
override a default catcher, or declare a catcher for a custom 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:
```rust ```rust
#[catch(404)] #[catch(404)]
@ -952,10 +959,11 @@ rocket::ignite().register(catchers![not_found])
``` ```
Unlike route request handlers, catchers take exactly zero or one parameter. If Unlike route request handlers, catchers take exactly zero or one parameter. If
the catcher takes a parameter, it must be of type [`&Request`] The [error the catcher takes a parameter, it must be of type [`&Request`]. The [error
catcher example](@example/errors) on GitHub illustrates their use in full. catcher example](@example/errors) on GitHub illustrates their use in full.
[`catch`]: @api/rocket_codegen/attr.catch.html [`catch`]: @api/rocket_codegen/attr.catch.html
[`register()`]: @api/rocket/struct.Rocket.html#method.register [`register()`]: @api/rocket/struct.Rocket.html#method.register
[`mount()`]: @api/rocket/struct.Rocket.html#method.mount
[`catchers!`]: @api/rocket_codegen/macro.catchers.html [`catchers!`]: @api/rocket_codegen/macro.catchers.html
[`&Request`]: @api/rocket/struct.Request.html [`&Request`]: @api/rocket/struct.Request.html