Introduce sentinels: auto-discovered launch abort.
Sentinels resolve a long-standing usability and functional correctness
issue in Rocket: starting an application with guards and/or responders
that depend on state that isn't available. The canonical example is the
'State' guard. Prior to this commit, an application with routes that
queried unmanaged state via 'State' would fail at runtime. With this
commit, the application refuses to launch with a detailed error message.
The 'Sentinel' docs explains it as:
A sentinel, automatically run on ignition, can trigger a launch
abort should an instance fail to meet arbitrary conditions. Every
type that appears in a mounted route's type signature is eligible to
be a sentinel. Of these, those that implement 'Sentinel' have their
'abort()' method invoked automatically, immediately after ignition,
once for each unique type. Sentinels inspect the finalized instance
of 'Rocket' and can trigger a launch abort by returning 'true'.
The following types are now sentinels:
* 'contrib::databases::Connection' (any '#[database]' type)
* 'contrib::templates::Metadata'
* 'contrib::templates::Template'
* 'core::State'
The following are "specialized" sentinels, which allow sentinel
discovery even through type aliases:
* 'Option<T>', 'Debug<T>' if 'T: Sentinel'
* 'Result<T, E>', 'Either<T, E>' if 'T: Sentinel', 'E: Sentinel'
Closes #464.
2021-04-16 08:23:15 +00:00
|
|
|
use rocket::{*, error::ErrorKind::SentinelAborts};
|
|
|
|
|
|
|
|
#[get("/two")]
|
2021-05-11 13:56:35 +00:00
|
|
|
fn two_states(_one: &State<u32>, _two: &State<String>) {}
|
Introduce sentinels: auto-discovered launch abort.
Sentinels resolve a long-standing usability and functional correctness
issue in Rocket: starting an application with guards and/or responders
that depend on state that isn't available. The canonical example is the
'State' guard. Prior to this commit, an application with routes that
queried unmanaged state via 'State' would fail at runtime. With this
commit, the application refuses to launch with a detailed error message.
The 'Sentinel' docs explains it as:
A sentinel, automatically run on ignition, can trigger a launch
abort should an instance fail to meet arbitrary conditions. Every
type that appears in a mounted route's type signature is eligible to
be a sentinel. Of these, those that implement 'Sentinel' have their
'abort()' method invoked automatically, immediately after ignition,
once for each unique type. Sentinels inspect the finalized instance
of 'Rocket' and can trigger a launch abort by returning 'true'.
The following types are now sentinels:
* 'contrib::databases::Connection' (any '#[database]' type)
* 'contrib::templates::Metadata'
* 'contrib::templates::Template'
* 'core::State'
The following are "specialized" sentinels, which allow sentinel
discovery even through type aliases:
* 'Option<T>', 'Debug<T>' if 'T: Sentinel'
* 'Result<T, E>', 'Either<T, E>' if 'T: Sentinel', 'E: Sentinel'
Closes #464.
2021-04-16 08:23:15 +00:00
|
|
|
|
|
|
|
#[get("/one")]
|
2021-05-11 13:56:35 +00:00
|
|
|
fn one_state(_three: &State<u8>) {}
|
Introduce sentinels: auto-discovered launch abort.
Sentinels resolve a long-standing usability and functional correctness
issue in Rocket: starting an application with guards and/or responders
that depend on state that isn't available. The canonical example is the
'State' guard. Prior to this commit, an application with routes that
queried unmanaged state via 'State' would fail at runtime. With this
commit, the application refuses to launch with a detailed error message.
The 'Sentinel' docs explains it as:
A sentinel, automatically run on ignition, can trigger a launch
abort should an instance fail to meet arbitrary conditions. Every
type that appears in a mounted route's type signature is eligible to
be a sentinel. Of these, those that implement 'Sentinel' have their
'abort()' method invoked automatically, immediately after ignition,
once for each unique type. Sentinels inspect the finalized instance
of 'Rocket' and can trigger a launch abort by returning 'true'.
The following types are now sentinels:
* 'contrib::databases::Connection' (any '#[database]' type)
* 'contrib::templates::Metadata'
* 'contrib::templates::Template'
* 'core::State'
The following are "specialized" sentinels, which allow sentinel
discovery even through type aliases:
* 'Option<T>', 'Debug<T>' if 'T: Sentinel'
* 'Result<T, E>', 'Either<T, E>' if 'T: Sentinel', 'E: Sentinel'
Closes #464.
2021-04-16 08:23:15 +00:00
|
|
|
|
|
|
|
#[async_test]
|
|
|
|
async fn state_sentinel_works() {
|
|
|
|
let err = rocket::build()
|
|
|
|
.configure(Config::debug_default())
|
|
|
|
.mount("/", routes![two_states])
|
|
|
|
.ignite().await
|
|
|
|
.unwrap_err();
|
|
|
|
|
|
|
|
assert!(matches!(err.kind(), SentinelAborts(vec) if vec.len() == 2));
|
|
|
|
|
|
|
|
let err = rocket::build()
|
|
|
|
.configure(Config::debug_default())
|
|
|
|
.mount("/", routes![two_states])
|
|
|
|
.manage(String::new())
|
|
|
|
.ignite().await
|
|
|
|
.unwrap_err();
|
|
|
|
|
|
|
|
assert!(matches!(err.kind(), SentinelAborts(vec) if vec.len() == 1));
|
|
|
|
|
|
|
|
let err = rocket::build()
|
|
|
|
.configure(Config::debug_default())
|
|
|
|
.mount("/", routes![two_states])
|
|
|
|
.manage(1 as u32)
|
|
|
|
.ignite().await
|
|
|
|
.unwrap_err();
|
|
|
|
|
|
|
|
assert!(matches!(err.kind(), SentinelAborts(vec) if vec.len() == 1));
|
|
|
|
|
|
|
|
let result = rocket::build()
|
|
|
|
.configure(Config::debug_default())
|
|
|
|
.mount("/", routes![two_states])
|
|
|
|
.manage(String::new())
|
|
|
|
.manage(1 as u32)
|
|
|
|
.ignite().await;
|
|
|
|
|
|
|
|
assert!(result.is_ok());
|
|
|
|
|
|
|
|
let err = rocket::build()
|
|
|
|
.configure(Config::debug_default())
|
|
|
|
.mount("/", routes![one_state])
|
|
|
|
.ignite().await
|
|
|
|
.unwrap_err();
|
|
|
|
|
|
|
|
assert!(matches!(err.kind(), SentinelAborts(vec) if vec.len() == 1));
|
|
|
|
|
|
|
|
let result = rocket::build()
|
|
|
|
.configure(Config::debug_default())
|
|
|
|
.mount("/", routes![one_state])
|
|
|
|
.manage(1 as u8)
|
|
|
|
.ignite().await;
|
|
|
|
|
|
|
|
assert!(result.is_ok());
|
|
|
|
|
|
|
|
let err = rocket::build()
|
|
|
|
.configure(Config::debug_default())
|
|
|
|
.mount("/", routes![one_state, two_states])
|
|
|
|
.ignite().await
|
|
|
|
.unwrap_err();
|
|
|
|
|
|
|
|
assert!(matches!(err.kind(), SentinelAborts(vec) if vec.len() == 3));
|
|
|
|
|
|
|
|
let err = rocket::build()
|
|
|
|
.configure(Config::debug_default())
|
|
|
|
.mount("/", routes![one_state, two_states])
|
|
|
|
.manage(1 as u32)
|
|
|
|
.ignite().await
|
|
|
|
.unwrap_err();
|
|
|
|
|
|
|
|
assert!(matches!(err.kind(), SentinelAborts(vec) if vec.len() == 2));
|
|
|
|
|
|
|
|
let err = rocket::build()
|
|
|
|
.configure(Config::debug_default())
|
|
|
|
.mount("/", routes![one_state, two_states])
|
|
|
|
.manage(1 as u8)
|
|
|
|
.ignite().await
|
|
|
|
.unwrap_err();
|
|
|
|
|
|
|
|
assert!(matches!(err.kind(), SentinelAborts(vec) if vec.len() == 2));
|
|
|
|
|
|
|
|
let err = rocket::build()
|
|
|
|
.configure(Config::debug_default())
|
|
|
|
.mount("/", routes![one_state, two_states])
|
|
|
|
.manage(1 as u32)
|
|
|
|
.manage(1 as u8)
|
|
|
|
.ignite().await
|
|
|
|
.unwrap_err();
|
|
|
|
|
|
|
|
assert!(matches!(err.kind(), SentinelAborts(vec) if vec.len() == 1));
|
|
|
|
|
|
|
|
let result = rocket::build()
|
|
|
|
.configure(Config::debug_default())
|
|
|
|
.mount("/", routes![one_state, two_states])
|
|
|
|
.manage(1 as u32)
|
|
|
|
.manage(1 as u8)
|
|
|
|
.manage(String::new())
|
|
|
|
.ignite().await;
|
|
|
|
|
|
|
|
assert!(result.is_ok());
|
|
|
|
}
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
fn inner_sentinels_detected() {
|
|
|
|
use rocket::local::blocking::Client;
|
|
|
|
|
|
|
|
#[derive(Responder)]
|
|
|
|
struct MyThing<T>(T);
|
|
|
|
|
|
|
|
struct ResponderSentinel;
|
|
|
|
|
|
|
|
impl<'r, 'o: 'r> response::Responder<'r, 'o> for ResponderSentinel {
|
|
|
|
fn respond_to(self, _: &'r Request<'_>) -> response::Result<'o> {
|
|
|
|
todo!()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
impl Sentinel for ResponderSentinel {
|
|
|
|
fn abort(_: &Rocket<Ignite>) -> bool {
|
|
|
|
true
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
#[get("/")]
|
|
|
|
fn route() -> MyThing<ResponderSentinel> { todo!() }
|
|
|
|
|
|
|
|
let err = Client::debug_with(routes![route]).unwrap_err();
|
|
|
|
assert!(matches!(err.kind(), SentinelAborts(vec) if vec.len() == 1));
|
|
|
|
|
|
|
|
#[derive(Responder)]
|
|
|
|
struct Inner<T>(T);
|
|
|
|
|
|
|
|
#[get("/")]
|
|
|
|
fn inner() -> MyThing<Inner<ResponderSentinel>> { todo!() }
|
|
|
|
|
|
|
|
let err = Client::debug_with(routes![inner]).unwrap_err();
|
|
|
|
assert!(matches!(err.kind(), SentinelAborts(vec) if vec.len() == 1));
|
|
|
|
|
|
|
|
#[get("/")]
|
|
|
|
fn inner_either() -> Either<Inner<ResponderSentinel>, ResponderSentinel> { todo!() }
|
|
|
|
|
|
|
|
let err = Client::debug_with(routes![inner_either]).unwrap_err();
|
|
|
|
assert!(matches!(err.kind(), SentinelAborts(vec) if vec.len() == 2));
|
|
|
|
|
|
|
|
#[derive(Responder)]
|
|
|
|
struct Block<T>(T);
|
|
|
|
|
|
|
|
impl<T> Sentinel for Block<T> {
|
|
|
|
fn abort(_: &Rocket<Ignite>) -> bool {
|
|
|
|
false
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
#[get("/")]
|
|
|
|
fn blocked() -> Block<ResponderSentinel> { todo!() }
|
|
|
|
|
|
|
|
Client::debug_with(routes![blocked]).expect("no sentinel errors");
|
|
|
|
|
|
|
|
#[get("/a")]
|
|
|
|
fn inner_b() -> Either<Inner<Block<ResponderSentinel>>, Block<ResponderSentinel>> {
|
|
|
|
todo!()
|
|
|
|
}
|
|
|
|
|
|
|
|
#[get("/b")]
|
|
|
|
fn inner_b2() -> Either<Block<Inner<ResponderSentinel>>, Block<ResponderSentinel>> {
|
|
|
|
todo!()
|
|
|
|
}
|
|
|
|
|
|
|
|
Client::debug_with(routes![inner_b, inner_b2]).expect("no sentinel errors");
|
|
|
|
|
|
|
|
#[get("/")]
|
|
|
|
fn half_b() -> Either<Inner<ResponderSentinel>, Block<ResponderSentinel>> {
|
|
|
|
todo!()
|
|
|
|
}
|
|
|
|
|
|
|
|
let err = Client::debug_with(routes![half_b]).unwrap_err();
|
|
|
|
assert!(matches!(err.kind(), SentinelAborts(vec) if vec.len() == 1));
|
|
|
|
|
|
|
|
use rocket::response::Responder;
|
|
|
|
|
|
|
|
#[get("/")]
|
|
|
|
fn half_c<'r>() -> Either<
|
|
|
|
Inner<impl Responder<'r, 'static>>,
|
|
|
|
Result<ResponderSentinel, Inner<ResponderSentinel>>
|
|
|
|
> {
|
|
|
|
Either::Left(Inner(()))
|
|
|
|
}
|
|
|
|
|
|
|
|
let err = Client::debug_with(routes![half_c]).unwrap_err();
|
|
|
|
assert!(matches!(err.kind(), SentinelAborts(vec) if vec.len() == 2));
|
|
|
|
|
|
|
|
#[get("/")]
|
|
|
|
fn half_d<'r>() -> Either<
|
|
|
|
Inner<impl Responder<'r, 'static>>,
|
|
|
|
Result<Block<ResponderSentinel>, Inner<ResponderSentinel>>
|
|
|
|
> {
|
|
|
|
Either::Left(Inner(()))
|
|
|
|
}
|
|
|
|
|
|
|
|
let err = Client::debug_with(routes![half_d]).unwrap_err();
|
|
|
|
assert!(matches!(err.kind(), SentinelAborts(vec) if vec.len() == 1));
|
|
|
|
|
|
|
|
// The special `Result` implementation.
|
|
|
|
type MyResult = Result<ResponderSentinel, ResponderSentinel>;
|
|
|
|
|
|
|
|
#[get("/")]
|
|
|
|
fn half_e<'r>() -> Either<Inner<impl Responder<'r, 'static>>, MyResult> {
|
|
|
|
Either::Left(Inner(()))
|
|
|
|
}
|
|
|
|
|
|
|
|
let err = Client::debug_with(routes![half_e]).unwrap_err();
|
|
|
|
assert!(matches!(err.kind(), SentinelAborts(vec) if vec.len() == 1));
|
|
|
|
|
|
|
|
// Another specialized sentinel.
|
|
|
|
|
|
|
|
#[get("/")] fn either_route() -> Either<ResponderSentinel, ResponderSentinel> { todo!() }
|
|
|
|
let err = Client::debug_with(routes![either_route]).unwrap_err();
|
|
|
|
assert!(matches!(err.kind(), SentinelAborts(vec) if vec.len() == 1));
|
|
|
|
|
|
|
|
#[get("/")] fn either_route2() -> Either<ResponderSentinel, ()> { todo!() }
|
|
|
|
let err = Client::debug_with(routes![either_route2]).unwrap_err();
|
|
|
|
assert!(matches!(err.kind(), SentinelAborts(vec) if vec.len() == 1));
|
|
|
|
|
|
|
|
#[get("/")] fn either_route3() -> Either<(), ResponderSentinel> { todo!() }
|
|
|
|
let err = Client::debug_with(routes![either_route3]).unwrap_err();
|
|
|
|
assert!(matches!(err.kind(), SentinelAborts(vec) if vec.len() == 1));
|
|
|
|
|
|
|
|
#[get("/")] fn either_route4() -> Either<(), ()> { todo!() }
|
|
|
|
Client::debug_with(routes![either_route4]).expect("no sentinel error");
|
|
|
|
}
|
2021-06-04 02:31:30 +00:00
|
|
|
|
|
|
|
#[async_test]
|
|
|
|
async fn known_macro_sentinel_works() {
|
|
|
|
use rocket::response::stream::{TextStream, ByteStream, ReaderStream};
|
|
|
|
use rocket::local::asynchronous::Client;
|
|
|
|
use rocket::tokio::io::AsyncRead;
|
|
|
|
|
|
|
|
#[derive(Responder)]
|
|
|
|
struct TextSentinel(&'static str);
|
|
|
|
|
|
|
|
impl Sentinel for TextSentinel {
|
|
|
|
fn abort(_: &Rocket<Ignite>) -> bool {
|
|
|
|
true
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
impl AsRef<str> for TextSentinel {
|
|
|
|
fn as_ref(&self) -> &str {
|
|
|
|
self.0
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
impl AsRef<[u8]> for TextSentinel {
|
|
|
|
fn as_ref(&self) -> &[u8] {
|
|
|
|
self.0.as_bytes()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
impl AsyncRead for TextSentinel {
|
|
|
|
fn poll_read(
|
|
|
|
self: std::pin::Pin<&mut Self>,
|
|
|
|
_: &mut futures::task::Context<'_>,
|
|
|
|
_: &mut tokio::io::ReadBuf<'_>,
|
|
|
|
) -> futures::task::Poll<std::io::Result<()>> {
|
|
|
|
futures::task::Poll::Ready(Ok(()))
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
#[get("/text")]
|
|
|
|
fn text() -> TextStream![TextSentinel] {
|
|
|
|
TextStream!(yield TextSentinel("hi");)
|
|
|
|
}
|
|
|
|
|
|
|
|
#[get("/bytes")]
|
|
|
|
fn byte() -> ByteStream![TextSentinel] {
|
|
|
|
ByteStream!(yield TextSentinel("hi");)
|
|
|
|
}
|
|
|
|
|
|
|
|
#[get("/reader")]
|
|
|
|
fn reader() -> ReaderStream![TextSentinel] {
|
|
|
|
ReaderStream!(yield TextSentinel("hi");)
|
|
|
|
}
|
|
|
|
|
|
|
|
macro_rules! UnknownStream {
|
|
|
|
($t:ty) => (ReaderStream![$t])
|
|
|
|
}
|
|
|
|
|
|
|
|
#[get("/ignore")]
|
|
|
|
fn ignore() -> UnknownStream![TextSentinel] {
|
|
|
|
ReaderStream!(yield TextSentinel("hi");)
|
|
|
|
}
|
|
|
|
|
|
|
|
let err = Client::debug_with(routes![text, byte, reader, ignore]).await.unwrap_err();
|
|
|
|
assert!(matches!(err.kind(), SentinelAborts(vec) if vec.len() == 3));
|
|
|
|
}
|