When developing web servers, we sometimes need a mechanism to share application state, such as configurations or database connections. actix-web
makes it possible to inject shared data into application state using the app_data
method and retrieve and pass the data to route handlers using Data extractor.
Let's explore just enough to understand how it works under the hood!
Note: The examples are based on zero2prod and actix-web v4 source code.
The following is an example of an actix-web
server that exposes a /subscriptions
route. subscribeHandler
accepts a Postgres database connection (PgConnection
), which we added to the application state via the app_data
method.
pub fn run(listener: TcpListener, connection: PgConnection) -> Result<Server, std::io::Error> { let connection = web::Data::new(connection); let server = HttpServer::new(move || { App::new() .route("/subscriptions", web::post().to(subscribeHandler)) // vvvvv Store PgConnection app data here .app_data(connection.clone()) }) .listen(listener)? .run(); Ok(server)}
...
// Extract PgConnection from application statepub async fn subscribeHandler(_connection: web::Data<PgConnection>) -> HttpResponse { //...}
Question: Since it is possible to store other data types, how does actix-web
know to retrieve and pass a PgConnection
value to subscribeHandler
instead of a different type?
Firstly, let's take a look at the web::post().to(...)
implementation:
// in actix-web's src/web.rs
pub fn to<F, I, R>(handler: F) -> Routewhere F: Handler<I, R>, I: FromRequest + 'static, // <--- Notice this! R: Future + 'static, R::Output: Responder + 'static,{ Route::new().to(handler)}
We could dive deeper into the function calls, but it would get quite complex and unnecessary to know. So instead, take note of the FromRequest
trait associated with the Handler
.
Next, we shall take a look at web::Data<T>
, which subscribeHandler
takes as a parameter, and see that it implements the FromRequest
trait with the from_request
function:
// in actix-web's src/data.rs
pub struct Data<T: ?Sized>(Arc<T>);
impl<T: ?Sized + 'static> FromRequest for Data<T> { type Config = (); type Error = Error; type Future = Ready<Result<Self, Error>>;
#[inline] fn from_request(req: &HttpRequest, _: &mut Payload) -> Self::Future { if let Some(st) = req.app_data::<Data<T>>() { ok(st.clone()) } else { log::debug!( "Failed to construct App-level Data extractor. \ Request path: {:?} (type: {})", req.path(), type_name::<T>(), ); err(ErrorInternalServerError( "App data is not configured, to configure use App::data()", )) } }}
from_request
calls the req.app_data::<Data<T>>()
, which returns an Option
. It returns the data, st
, if found. Otherwise it returns an Internal Server Error.
So, how exactly does calling req.app_data::<Data<T>>
return the correct PgConnection
data that we specified in the generic type parameter?
Let's look at the app_data
method implementation:
// in actix-web's src/request.rs
pub struct HttpRequest { pub(crate) inner: Rc<HttpRequestInner>,}
pub(crate) struct HttpRequestInner { pub(crate) app_data: SmallVec<[Rc<Extensions>; 4]>,}
impl HttpRequest { pub fn app_data<T: 'static>(&self) -> Option<&T> { // container has type `Extensions` for container in self.inner.app_data.iter().rev() { if let Some(data) = container.get::<T>() { return Some(data); } }
None }}
It appears that the incoming HttpRequest
data contains app_data
, which the code iterates through and then calls container.get::<T>()
.
// in actix-http's src/extensions.rs
pub struct Extensions { map: AHashMap<TypeId, Box<dyn Any>>,}
impl Extensions pub fn get<T: 'static>(&self) -> Option<&T> { self.map .get(&TypeId::of::<T>()) .and_then(|boxed| boxed.downcast_ref()) }}
get::<T>()
tries to get the value at the key TypeId::of::<T>()
from the hashmap, map
. What does TypeId::of::<T>()
do?
/// A `TypeId` represents a globally unique identifier for a type.
pub struct TypeId { t: u64,}
impl TypeId { /// Returns the `TypeId` of the type this generic function has been /// instantiated with. /// /// # Examples /// /// ``` /// use std::any::{Any, TypeId}; /// /// fn is_string<T: ?Sized + Any>(_s: &T) -> bool { /// TypeId::of::<String>() == TypeId::of::<T>() /// } /// /// assert_eq!(is_string(&0), false); /// assert_eq!(is_string(&"cookie monster".to_string()), true); /// ``` pub const fn of<T: ?Sized + 'static>() -> TypeId { TypeId { t: intrinsics::type_id::<T>() } }}
Aha! Given a type, such as PgConnection
, Rust allows us to obtain a unique u64
value that identifies it! Hence calling TypeId::of::<PgConnection>()
gives us a value that we can use a key for the hashmap inside of app_data
.
But wait a moment! Isn't PgConnection
just a generic type parameter? So, at runtime, how does Rust ensure that calling TypeId::of::<PgConnection>()
gives us the value associated with PgConnection
and not some other type?
Rust generates different copies of generic functions during compilation for each concrete type needed, such as PgConnection
. This process is known as monomorphization. Given that we use web::Data<PgConnnection>
, Rust will compile copies of the from_request
, app_data
, get
and TypeId::of
functions that are specific to the PgConnection
type.
At runtime, when a request hits the /subscriptions
route, the PgConnection
-specific version of the TypeId::of
function will return a concrete TypeId
value associated with PgConnection
. This value is then used to search app_data
's internal hashmap and return the stored PgConnection
data that we added to the application state.
So, we've figured out how actix-web
retrieves PgConnection
from the application state. Now, to complete our understanding, let's look at how actix-web
stores data in the application state:
// Server code
App::new() .route("/subscriptions", web::post().to(subscribeHandler)) // vvvvv Store PgConnection app data here .app_data(connection.clone())
Digging into the app_data
method's implementation:
// in actix-web's src/app.rs
impl App { pub fn app_data<U: 'static>(mut self, ext: U) -> Self { self.extensions.insert(ext); self }}
App
inserts the PgConnection
value into its own extensions
, which contains a hashmap (remember from earlier):
// in actix-http's src/extensions.rs
impl Extensions pub fn insert<T: 'static>(&mut self, val: T) -> Option<T> { self.map .insert(TypeId::of::<T>(), Box::new(val)) .and_then(downcast_owned) }}
Hey! It's the same Extensions
type! The method inserts the PgConnection
value with the key of TypeId::of::<T>()
, which resolves to the PgConnection
-specific value.
There we have it! actix-web
stores data in application state's by inserting and retrieving them from a hashmap using keys derived from TypeId::of::<T>()
, which returns unique identifiers for different types. And this is made possible by monomorphization
in Rust!