Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Route tracing #2167

Open
wants to merge 7 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
3 changes: 3 additions & 0 deletions core/codegen/src/attribute/catch/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,8 @@ pub fn _catch(
/// Rocket code generated proxy structure.
#deprecated #vis struct #user_catcher_fn_name { }

impl #CatcherType for #user_catcher_fn_name { }

/// Rocket code generated proxy static conversion implementations.
#[allow(nonstandard_style, deprecated, clippy::style)]
impl #user_catcher_fn_name {
Expand All @@ -83,6 +85,7 @@ pub fn _catch(
name: stringify!(#user_catcher_fn_name),
code: #status_code,
handler: monomorphized_function,
catcher_type: #_Box::new(self),
}
}

Expand Down
3 changes: 3 additions & 0 deletions core/codegen/src/attribute/route/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -342,6 +342,8 @@ fn codegen_route(route: Route) -> Result<TokenStream> {
/// Rocket code generated proxy structure.
#deprecated #vis struct #handler_fn_name { }

impl #RouteType for #handler_fn_name {}

/// Rocket code generated proxy static conversion implementations.
#[allow(nonstandard_style, deprecated, clippy::style)]
impl #handler_fn_name {
Expand All @@ -368,6 +370,7 @@ fn codegen_route(route: Route) -> Result<TokenStream> {
format: #format,
rank: #rank,
sentinels: #sentinels,
route_type: #_Box::new(self),
}
}

Expand Down
2 changes: 2 additions & 0 deletions core/codegen/src/exports.rs
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,9 @@ define_exported_paths! {
StaticRouteInfo => ::rocket::StaticRouteInfo,
StaticCatcherInfo => ::rocket::StaticCatcherInfo,
Route => ::rocket::Route,
RouteType => ::rocket::route::RouteType,
Catcher => ::rocket::Catcher,
CatcherType => ::rocket::catcher::CatcherType,
SmallVec => ::rocket::http::private::SmallVec,
Status => ::rocket::http::Status,
}
Expand Down
30 changes: 29 additions & 1 deletion core/lib/src/catcher/catcher.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
use std::any::{TypeId, Any};
use std::fmt;
use std::io::Cursor;

Expand All @@ -10,6 +11,12 @@ use crate::catcher::{Handler, BoxFuture};

use yansi::Paint;

/// A generic trait for catcher types. This should be automatically implemented on the structs
/// generated by the codegen for each catcher, and manually implemented on custom catcher types.
///
/// Use the `Catcher::with_type::<T>()` method to set the catcher type.
pub trait CatcherType: Any + Send + Sync + 'static { }

/// An error catching route.
///
/// Catchers are routes that run when errors are produced by the application.
Expand Down Expand Up @@ -127,6 +134,9 @@ pub struct Catcher {
///
/// This is -(number of nonempty segments in base).
pub(crate) rank: isize,

/// A unique route type to identify this route
pub(crate) catcher_type: Option<TypeId>,

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Very cool! I had to read about TypeId.

}

// The rank is computed as -(number of nonempty segments in base) => catchers
Expand Down Expand Up @@ -171,6 +181,8 @@ impl Catcher {
///
/// Panics if `code` is not in the HTTP status code error range `[400,
/// 600)`.
///
/// If applicable, `with_type` should also be called to set the route type for testing
#[inline(always)]
pub fn new<S, H>(code: S, handler: H) -> Catcher
where S: Into<Option<u16>>, H: Handler
Expand All @@ -185,7 +197,8 @@ impl Catcher {
base: uri::Origin::ROOT,
handler: Box::new(handler),
rank: rank(uri::Origin::ROOT.path()),
code
code,
catcher_type: None,
}
}

Expand Down Expand Up @@ -261,6 +274,13 @@ impl Catcher {
self
}

/// Marks this catcher with the specified type. For a custom catcher type, i.e. something that can
/// be passed to `.register()`, it should be that type to make identification easier.
pub fn with_type<T: CatcherType>(mut self) -> Self {
self.catcher_type = Some(TypeId::of::<T>());
self
}

/// Maps the `base` of this catcher using `mapper`, returning a new
/// `Catcher` with the returned base.
///
Expand Down Expand Up @@ -307,6 +327,10 @@ impl Catcher {
}
}

/// Catcher type of the default catcher created by Rocket
pub struct DefaultCatcher { _priv: () }
impl CatcherType for DefaultCatcher {}

impl Default for Catcher {
fn default() -> Self {
fn handler<'r>(s: Status, req: &'r Request<'_>) -> BoxFuture<'r> {
Expand All @@ -315,6 +339,7 @@ impl Default for Catcher {

let mut catcher = Catcher::new(None, handler);
catcher.name = Some("<Rocket Catcher>".into());
catcher.catcher_type = Some(TypeId::of::<DefaultCatcher>());
catcher
}
}
Expand All @@ -328,6 +353,8 @@ pub struct StaticInfo {
pub code: Option<u16>,
/// The catcher's handler, i.e, the annotated function.
pub handler: for<'r> fn(Status, &'r Request<'_>) -> BoxFuture<'r>,
/// A unique route type to identify this route
pub catcher_type: Box<dyn CatcherType>,
}

#[doc(hidden)]
Expand All @@ -336,6 +363,7 @@ impl From<StaticInfo> for Catcher {
fn from(info: StaticInfo) -> Catcher {
let mut catcher = Catcher::new(info.code, info.handler);
catcher.name = Some(info.name.into());
catcher.catcher_type = Some(info.catcher_type.as_ref().type_id());
catcher
}
}
Expand Down
7 changes: 5 additions & 2 deletions core/lib/src/fs/server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ use std::path::{PathBuf, Path};

use crate::{Request, Data};
use crate::http::{Method, uri::Segments, ext::IntoOwned};
use crate::route::{Route, Handler, Outcome};
use crate::route::{Route, Handler, Outcome, RouteType};
use crate::response::Redirect;
use crate::fs::NamedFile;

Expand Down Expand Up @@ -180,10 +180,13 @@ impl FileServer {
}
}

impl RouteType for FileServer {}

impl From<FileServer> for Vec<Route> {
fn from(server: FileServer) -> Self {
let source = figment::Source::File(server.root.clone());
let mut route = Route::ranked(server.rank, Method::Get, "/<path..>", server);
let mut route = Route::ranked(server.rank, Method::Get, "/<path..>", server)
.with_type::<FileServer>();
route.name = Some(format!("FileServer: {}", source).into());
vec![route]
}
Expand Down
17 changes: 16 additions & 1 deletion core/lib/src/local/asynchronous/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ use std::fmt;

use parking_lot::RwLock;

use crate::{Rocket, Phase, Orbit, Ignite, Error};
use crate::{Rocket, Phase, Orbit, Ignite, Error, Build};
use crate::local::asynchronous::{LocalRequest, LocalResponse};
use crate::http::{Method, uri::Origin, private::cookie};

Expand Down Expand Up @@ -76,6 +76,21 @@ impl Client {
})
}

// WARNING: This is unstable! Do not use this method outside of Rocket!
// This is used by the `Client` doctests.
#[doc(hidden)]
pub fn _test_with<M, T, F>(mods: M, f: F) -> T
where F: FnOnce(&Self, LocalRequest<'_>, LocalResponse<'_>) -> T + Send,
M: FnOnce(Rocket<Build>) -> Rocket<Build>
{
crate::async_test(async {
let client = Client::debug(mods(crate::build())).await.unwrap();
let request = client.get("/");
let response = request.clone().dispatch().await;
f(&client, request, response)
})
}

#[inline(always)]
pub(crate) fn _rocket(&self) -> &Rocket<Orbit> {
&self.rocket
Expand Down
6 changes: 6 additions & 0 deletions core/lib/src/local/asynchronous/response.rs
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,12 @@ impl<'c> LocalResponse<'c> {
}
}

impl<'r> LocalResponse<'r> {
pub(crate) fn _request(&self) -> &Request<'r> {
&self._request
}
}

impl LocalResponse<'_> {
pub(crate) fn _response(&self) -> &Response<'_> {
&self.response
Expand Down
15 changes: 14 additions & 1 deletion core/lib/src/local/blocking/client.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
use std::fmt;
use std::cell::RefCell;

use crate::{Rocket, Phase, Orbit, Ignite, Error};
use crate::{Rocket, Phase, Orbit, Ignite, Error, Build};
use crate::local::{asynchronous, blocking::{LocalRequest, LocalResponse}};
use crate::http::{Method, uri::Origin};

Expand Down Expand Up @@ -54,6 +54,19 @@ impl Client {
f(&client, request, response)
}

// WARNING: This is unstable! Do not use this method outside of Rocket!
// This is used by the `Client` doctests.
#[doc(hidden)]
pub fn _test_with<M, T, F>(mods: M, f: F) -> T
where F: FnOnce(&Self, LocalRequest<'_>, LocalResponse<'_>) -> T + Send,
M: FnOnce(Rocket<Build>) -> Rocket<Build>
{
let client = Client::debug(mods(crate::build())).unwrap();
let request = client.get("/");
let response = request.clone().dispatch();
f(&client, request, response)
}

#[inline(always)]
pub(crate) fn inner(&self) -> &asynchronous::Client {
self.inner.as_ref().expect("internal invariant broken: self.inner is Some")
Expand Down
8 changes: 7 additions & 1 deletion core/lib/src/local/blocking/response.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
use std::io;
use tokio::io::AsyncReadExt;

use crate::{Response, local::asynchronous, http::CookieJar};
use crate::{Response, local::asynchronous, http::CookieJar, Request};

use super::Client;

Expand Down Expand Up @@ -54,6 +54,12 @@ pub struct LocalResponse<'c> {
pub(in super) client: &'c Client,
}

impl<'r> LocalResponse<'r> {
pub(crate) fn _request(&self) -> &Request<'r> {
&self.inner._request()
}
}

impl LocalResponse<'_> {
fn _response(&self) -> &Response<'_> {
&self.inner._response()
Expand Down
128 changes: 128 additions & 0 deletions core/lib/src/local/response.rs
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,134 @@ macro_rules! pub_response_impl {
self._into_msgpack() $(.$suffix)?
}

/// Checks if a response was generted by a specific route type. This only returns true if the route
/// actually generated the response, and a catcher was _not_ run. See [`was_attempted_by`] to
/// check if a route was attempted, but may not have generated the response
///
/// # Example
///
/// ```rust
/// # use rocket::{get, routes};
/// #[get("/")]
/// fn index() -> &'static str { "Hello World" }
#[doc = $doc_prelude]
/// # Client::_test_with(|r| r.mount("/", routes![index]), |_, _, response| {
/// let response: LocalResponse = response;
/// assert!(response.was_routed_by::<index>());
/// # });
/// ```
///
/// # Other Route types
///
/// [`FileServer`](crate::fs::FileServer) implementes `RouteType`, so a route that should
/// return a static file can be checked against it. Libraries which provide custom Routes should
/// implement `RouteType`, see [`RouteType`](crate::route::RouteType) for more information.
pub fn was_routed_by<T: crate::route::RouteType>(&self) -> bool {
// If this request was caught, the route in `.route()` did NOT generate this response.
if self._request().catcher().is_some() {
false
} else if let Some(route_type) = self._request().route().map(|r| r.route_type).flatten() {
route_type == std::any::TypeId::of::<T>()
} else {
false
}
}

/// Checks if a request was routed to a specific route type. This will return true for routes
/// that were attempted, _but not actually called_. This enables a test to verify that a route
/// was attempted, even if another route actually generated the response, e.g. an
/// authenticated route will typically defer to an error catcher if the request does not have
/// the proper authentication. This makes it possible to verify that a request was routed to
/// the authentication route, even if the response was eventaully generated by another route or
/// a catcher.
///
/// # Example
///
// WARNING: this doc-test is NOT run, because cargo test --doc does not run doc-tests for items
// only available during tests.
/// ```rust
/// # use rocket::{get, routes, async_trait, request::{Request, Outcome, FromRequest}};
/// # struct WillFail {}
/// # #[async_trait]
/// # impl<'r> FromRequest<'r> for WillFail {
/// # type Error = ();
/// # async fn from_request(request: &'r Request<'_>) -> Outcome<Self, Self::Error> {
/// # Outcome::Forward(())
/// # }
/// # }
/// #[get("/", rank = 2)]
/// fn index1(guard: WillFail) -> &'static str { "Hello World" }
/// #[get("/")]
/// fn index2() -> &'static str { "Hello World" }
#[doc = $doc_prelude]
/// # Client::_test_with(|r| r.mount("/", routes![index1, index2]), |_, _, response| {
/// let response: LocalResponse = response;
/// assert!(response.was_attempted_by::<index1>());
/// assert!(response.was_attempted_by::<index2>());
/// assert!(response.was_routed_by::<index2>());
/// # });
/// ```
///
/// # Other Route types
///
/// [`FileServer`](crate::fs::FileServer) implementes `RouteType`, so a route that should
/// return a static file can be checked against it. Libraries which provide custom Routes should
/// implement `RouteType`, see [`RouteType`](crate::route::RouteType) for more information.
///
/// # Note
///
/// This method is marked as `cfg(test)`, and is therefore only available in unit and
/// integration tests. This is because the list of routes attempted is only collected in these
/// testing environments, to minimize performance impacts during normal operation.
#[cfg(test)]
Comment on lines +259 to +262
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is unfortunately not the way cfg(test) works. cfg(test) only gets enabled on the top-level crate being tested, not on its dependencies. So rocket compiled as a dependency of an application, even when compiling that application's tests, will never see this cfg enabled.

pub fn was_attempted_by<T: crate::route::RouteType>(&self) -> bool {
self._request().route_path(|path| path.iter().any(|r|
r.route_type == Some(std::any::TypeId::of::<T>())
))
}

/// Checks if a route was caught by a specific route type
///
/// # Example
///
/// ```rust
/// # use rocket::{catch, catchers};
/// #[catch(404)]
/// fn default_404() -> &'static str { "Hello World" }
#[doc = $doc_prelude]
/// # Client::_test_with(|r| r.register("/", catchers![default_404]), |_, _, response| {
/// let response: LocalResponse = response;
/// assert!(response.was_caught_by::<default_404>());
/// # });
/// ```
///
/// # Rocket's default catcher
///
/// The default catcher has a `CatcherType` of [`DefaultCatcher`](crate::catcher::DefaultCatcher)
pub fn was_caught_by<T: crate::catcher::CatcherType>(&self) -> bool {
if let Some(catcher_type) = self._request().catcher().map(|r| r.catcher_type).flatten() {
catcher_type == std::any::TypeId::of::<T>()
} else {
false
}
}

/// Checks if a route was caught by a catcher
///
/// # Example
///
/// ```rust
/// # use rocket::get;
#[doc = $doc_prelude]
/// # Client::_test(|_, _, response| {
/// let response: LocalResponse = response;
/// assert!(response.was_caught())
/// # });
/// ```
pub fn was_caught(&self) -> bool {
self._request().catcher().is_some()
}

#[cfg(test)]
#[allow(dead_code)]
fn _ensure_impls_exist() {
Expand Down