Skip to main content

dynamic_config_actix/
lib.rs

1//! A request-scoped configuration snapshot for Actix Web.
2//!
3//! ```no_run
4//! use actix_web::{get, App, HttpServer};
5//! use dynamic_config_actix::{Config, DynamicConfig};
6//! use dynamic_config_web_core::sections;
7//! # use dynamic_config::dynamic_config;
8//! # use serde::Deserialize;
9//! # #[dynamic_config] #[derive(Deserialize)] struct Server { port: u16 }
10//! # #[dynamic_config] #[derive(Deserialize)] struct Features { cache: bool }
11//!
12//! #[get("/")]
13//! async fn index(server: Config<Server>, features: Config<Features>) -> String {
14//!     // Both came out of one snapshot, taken when the request began.
15//!     // `Sections::take` retries if a reload lands mid-read, so these
16//!     // two cannot be different generations.
17//!     format!("{} {}", server.port, features.cache)
18//! }
19//!
20//! # fn build() {
21//! HttpServer::new(|| {
22//!     App::new()
23//!         .wrap(DynamicConfig::new(sections![Server, Features]))
24//!         .service(index)
25//! });
26//! # }
27//! ```
28//!
29//! # What this is for
30//!
31//! Actix runs handlers across several worker threads, and `Server::current()`
32//! is an atomic load that every worker can make without a lock. That part is
33//! already right. What it does not give you is *two* sections that agree: a
34//! reload landing between two reads lets one response mix generations.
35//!
36//! [`DynamicConfig`] reads every listed section once, before the handler
37//! runs, and puts the result in the request's extensions. [`Config<T>`]
38//! reads it back out.
39//!
40//! Note what is **not** here: no `web::Data<ServerConfig>`. Handing a
41//! snapshot to the app factory freezes it at start-up, and each worker then
42//! serves the configuration that existed when it was built.
43//!
44//! # What this is not
45//!
46//! It does not load configuration, watch files, or own a [`WatchHandle`].
47//! That stays in the startup code that calls `init()` and holds the handles
48//! for the life of the process.
49//!
50//! [`WatchHandle`]: https://docs.rs/dynamic-config/latest/dynamic_config/watch/struct.WatchHandle.html
51
52#![forbid(unsafe_code)]
53#![warn(missing_docs, missing_debug_implementations, rust_2018_idioms)]
54#![cfg_attr(docsrs, feature(doc_cfg))]
55
56use std::any::Any;
57use std::future::{ready, Ready};
58use std::sync::Arc;
59
60use actix_web::dev::{forward_ready, Service, ServiceRequest, ServiceResponse, Transform};
61use actix_web::http::StatusCode;
62use actix_web::{Error, FromRequest, HttpMessage, HttpRequest, ResponseError};
63use dynamic_config_web_core::{NotInScope, Sections, Snapshot};
64
65pub use dynamic_config_web_core::{sections, NotInScope as OutOfScope, Sections as ConfigSections};
66
67/// One section of this request's configuration.
68///
69/// ```no_run
70/// # use dynamic_config_actix::Config;
71/// # struct Database { host: String }
72/// async fn handler(db: Config<Database>) -> String {
73///     db.host.clone()
74/// }
75/// ```
76///
77/// Extracting the same type twice in one handler answers the same `Arc`.
78/// Extracting one the middleware was not given answers `500` — see
79/// [`SnapshotMissing`].
80pub struct Config<T>(pub Arc<T>);
81
82impl<T> Clone for Config<T> {
83    /// Hand-written: cloning an `Arc` never needs `T: Clone`.
84    fn clone(&self) -> Self {
85        Self(Arc::clone(&self.0))
86    }
87}
88
89impl<T> std::fmt::Debug for Config<T> {
90    /// The type's name, never the section's contents — a section holds
91    /// credentials, and `?config` is how one reaches a log line.
92    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
93        formatter
94            .debug_tuple("Config")
95            .field(&std::any::type_name::<T>())
96            .finish()
97    }
98}
99
100impl<T> std::ops::Deref for Config<T> {
101    type Target = T;
102
103    fn deref(&self) -> &Self::Target {
104        &self.0
105    }
106}
107
108impl<T> FromRequest for Config<T>
109where
110    T: Any + Send + Sync,
111{
112    type Error = Error;
113    type Future = Ready<Result<Self, Error>>;
114
115    fn from_request(request: &HttpRequest, _payload: &mut actix_web::dev::Payload) -> Self::Future {
116        ready(from_parts(request).map_err(Into::into))
117    }
118}
119
120fn from_parts<T: Any + Send + Sync>(request: &HttpRequest) -> Result<Config<T>, SnapshotMissing> {
121    let extensions = request.extensions();
122    let snapshot = extensions
123        .get::<Snapshot>()
124        .ok_or(SnapshotMissing::NoMiddleware)?;
125
126    snapshot
127        .require::<T>()
128        .map(Config)
129        .map_err(SnapshotMissing::Section)
130}
131
132/// Why a [`Config`] extractor could not answer.
133///
134/// Every variant is a wiring mistake rather than anything a client did,
135/// which is why they are all `500`.
136#[derive(Debug, Clone, Copy)]
137pub enum SnapshotMissing {
138    /// No [`DynamicConfig`] middleware ran for this request.
139    NoMiddleware,
140    /// It ran, and this section was not in what it took.
141    Section(NotInScope),
142}
143
144impl std::fmt::Display for SnapshotMissing {
145    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
146        match self {
147            Self::NoMiddleware => formatter.write_str(
148                "no configuration snapshot on this request: add \
149                 `.wrap(DynamicConfig::new(sections![..]))` to the app",
150            ),
151            Self::Section(why) => write!(formatter, "{why}"),
152        }
153    }
154}
155
156impl std::error::Error for SnapshotMissing {}
157
158impl ResponseError for SnapshotMissing {
159    fn status_code(&self) -> StatusCode {
160        StatusCode::INTERNAL_SERVER_ERROR
161    }
162
163    fn error_response(&self) -> actix_web::HttpResponse {
164        // Overridden, because the default renders `Display` into the body
165        // and `Display` names an internal type path. That detail is for
166        // whoever reads the logs; the client is told only that the server
167        // is misconfigured. The axum adapter answers the same way.
168        actix_web::HttpResponse::build(self.status_code())
169            .content_type("text/plain; charset=utf-8")
170            .body("configuration is not wired for this handler")
171    }
172}
173
174/// The snapshot this request began with, for code holding an
175/// [`HttpRequest`] rather than using the extractor.
176///
177/// Answers a clone, because actix hands out extensions behind a `RefCell`
178/// and a borrow could not outlive the call.
179///
180/// # Errors
181///
182/// [`SnapshotMissing::NoMiddleware`] when the middleware did not run.
183pub fn snapshot(request: &HttpRequest) -> Result<Snapshot, SnapshotMissing> {
184    request
185        .extensions()
186        .get::<Snapshot>()
187        .cloned()
188        .ok_or(SnapshotMissing::NoMiddleware)
189}
190
191/// Takes one snapshot per request and puts it in the request's extensions.
192///
193/// ```no_run
194/// # use actix_web::App;
195/// # use dynamic_config_actix::DynamicConfig;
196/// # use dynamic_config_web_core::Sections;
197/// # fn build() {
198/// # let sections = || Sections::new();
199/// App::new().wrap(DynamicConfig::new(sections()));
200/// # }
201/// ```
202///
203/// `HttpServer::new` takes a factory and calls it once per worker thread,
204/// so either build the [`Sections`] inside the closure — free, since it
205/// holds closures over process-wide configuration — or build one list and
206/// clone it in:
207///
208/// ```no_run
209/// # use actix_web::{App, HttpServer};
210/// # use dynamic_config_actix::DynamicConfig;
211/// # use dynamic_config_web_core::Sections;
212/// # fn build() {
213/// let configuration = DynamicConfig::new(Sections::new());
214///
215/// HttpServer::new(move || App::new().wrap(configuration.clone()));
216/// # }
217/// ```
218pub struct DynamicConfig {
219    sections: Arc<Sections>,
220}
221
222impl DynamicConfig {
223    /// Builds the middleware over the sections a request should read.
224    #[must_use]
225    pub fn new(sections: Sections) -> Self {
226        Self {
227            sections: Arc::new(sections),
228        }
229    }
230
231    /// The type names it will take, in order.
232    #[must_use]
233    pub fn names(&self) -> Vec<&'static str> {
234        self.sections.names()
235    }
236}
237
238impl std::fmt::Debug for DynamicConfig {
239    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
240        formatter
241            .debug_struct("DynamicConfig")
242            .field("sections", &self.sections.names())
243            .finish()
244    }
245}
246
247impl Clone for DynamicConfig {
248    fn clone(&self) -> Self {
249        Self {
250            sections: Arc::clone(&self.sections),
251        }
252    }
253}
254
255impl<S, B> Transform<S, ServiceRequest> for DynamicConfig
256where
257    S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error> + 'static,
258    S::Future: 'static,
259    B: 'static,
260{
261    type Response = ServiceResponse<B>;
262    type Error = Error;
263    type InitError = ();
264    type Transform = SnapshotMiddleware<S>;
265    type Future = Ready<Result<Self::Transform, Self::InitError>>;
266
267    fn new_transform(&self, service: S) -> Self::Future {
268        ready(Ok(SnapshotMiddleware {
269            service,
270            sections: Arc::clone(&self.sections),
271        }))
272    }
273}
274
275/// The service [`DynamicConfig`] wraps an application in.
276pub struct SnapshotMiddleware<S> {
277    service: S,
278    sections: Arc<Sections>,
279}
280
281impl<S> std::fmt::Debug for SnapshotMiddleware<S> {
282    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
283        formatter
284            .debug_struct("SnapshotMiddleware")
285            .field("sections", &self.sections.names())
286            .finish_non_exhaustive()
287    }
288}
289
290impl<S, B> Service<ServiceRequest> for SnapshotMiddleware<S>
291where
292    S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error> + 'static,
293    S::Future: 'static,
294    B: 'static,
295{
296    type Response = ServiceResponse<B>;
297    type Error = Error;
298    // The inner future, unboxed: nothing here runs after `call`, so there
299    // is nothing to wrap and no allocation to make per request.
300    type Future = S::Future;
301
302    forward_ready!(service);
303
304    fn call(&self, request: ServiceRequest) -> Self::Future {
305        // Taken *before* the extensions are borrowed: `take()` calls
306        // user-supplied readers, and actix hands out extensions behind a
307        // `RefCell` that panics on an overlapping borrow.
308        let taken = self.sections.take();
309
310        // Merged rather than inserted, because middleware nests: an
311        // `App::wrap` and a `scope().wrap()` may each carry a list, and a
312        // bare insert would erase the outer one.
313        let merged = match request.extensions_mut().remove::<Snapshot>() {
314            Some(outer) => outer.merged_with(taken),
315            None => taken,
316        };
317
318        request.extensions_mut().insert(merged);
319
320        self.service.call(request)
321    }
322}