QuickDID is a high-performance AT Protocol identity resolution service written in Rust. It provides handle-to-DID resolution with Redis-backed caching and queue processing.
1use crate::handle_resolver::HandleResolver;
2use crate::metrics::SharedMetricsPublisher;
3use crate::queue::{HandleResolutionWork, QueueAdapter};
4use atproto_lexicon::resolve::LexiconResolver;
5use axum::{
6 Router,
7 extract::{MatchedPath, State},
8 http::Request,
9 middleware::{self, Next},
10 response::{Json, Response},
11 routing::get,
12};
13use serde_json::json;
14use std::sync::Arc;
15use std::time::Instant;
16use tower_http::services::ServeDir;
17
18pub(crate) struct InnerAppContext {
19 pub(crate) handle_resolver: Arc<dyn HandleResolver>,
20 pub(crate) handle_queue: Arc<dyn QueueAdapter<HandleResolutionWork>>,
21 pub(crate) lexicon_resolver: Arc<dyn LexiconResolver>,
22 pub(crate) metrics: SharedMetricsPublisher,
23 pub(crate) etag_seed: String,
24 pub(crate) cache_control_header: Option<String>,
25 pub(crate) static_files_dir: String,
26}
27
28#[derive(Clone)]
29pub struct AppContext(pub(crate) Arc<InnerAppContext>);
30
31impl AppContext {
32 /// Create a new AppContext with the provided configuration.
33 pub fn new(
34 handle_resolver: Arc<dyn HandleResolver>,
35 handle_queue: Arc<dyn QueueAdapter<HandleResolutionWork>>,
36 lexicon_resolver: Arc<dyn LexiconResolver>,
37 metrics: SharedMetricsPublisher,
38 etag_seed: String,
39 cache_control_header: Option<String>,
40 static_files_dir: String,
41 ) -> Self {
42 Self(Arc::new(InnerAppContext {
43 handle_resolver,
44 handle_queue,
45 lexicon_resolver,
46 metrics,
47 etag_seed,
48 cache_control_header,
49 static_files_dir,
50 }))
51 }
52
53 // Internal accessor methods for handlers
54 pub(super) fn etag_seed(&self) -> &str {
55 &self.0.etag_seed
56 }
57
58 pub(super) fn cache_control_header(&self) -> Option<&str> {
59 self.0.cache_control_header.as_deref()
60 }
61
62 pub(super) fn static_files_dir(&self) -> &str {
63 &self.0.static_files_dir
64 }
65}
66
67use axum::extract::FromRef;
68
69macro_rules! impl_from_ref {
70 ($context:ty, $field:ident, $type:ty) => {
71 impl FromRef<$context> for $type {
72 fn from_ref(context: &$context) -> Self {
73 context.0.$field.clone()
74 }
75 }
76 };
77}
78
79impl_from_ref!(AppContext, handle_resolver, Arc<dyn HandleResolver>);
80impl_from_ref!(
81 AppContext,
82 handle_queue,
83 Arc<dyn QueueAdapter<HandleResolutionWork>>
84);
85impl_from_ref!(AppContext, lexicon_resolver, Arc<dyn LexiconResolver>);
86impl_from_ref!(AppContext, metrics, SharedMetricsPublisher);
87
88/// Middleware to track HTTP request metrics
89async fn metrics_middleware(
90 State(metrics): State<SharedMetricsPublisher>,
91 matched_path: Option<MatchedPath>,
92 request: Request<axum::body::Body>,
93 next: Next,
94) -> Response {
95 let start = Instant::now();
96 let method = request.method().to_string();
97 let path = matched_path
98 .as_ref()
99 .map(|p| p.as_str().to_string())
100 .unwrap_or_else(|| "unknown".to_string());
101
102 // Process the request
103 let response = next.run(request).await;
104
105 // Calculate duration
106 let duration_ms = start.elapsed().as_millis() as u64;
107 let status_code = response.status().as_u16().to_string();
108
109 // Publish metrics with tags
110 metrics
111 .time_with_tags(
112 "http.request.duration_ms",
113 duration_ms,
114 &[
115 ("method", &method),
116 ("path", &path),
117 ("status", &status_code),
118 ],
119 )
120 .await;
121
122 response
123}
124
125pub fn create_router(app_context: AppContext) -> Router {
126 let static_dir = app_context.static_files_dir().to_string();
127
128 Router::new()
129 .route("/xrpc/_health", get(handle_xrpc_health))
130 .route(
131 "/xrpc/com.atproto.identity.resolveHandle",
132 get(super::handle_xrpc_resolve_handle::handle_xrpc_resolve_handle)
133 .options(super::handle_xrpc_resolve_handle::handle_xrpc_resolve_handle_options),
134 )
135 .route(
136 "/xrpc/com.atproto.lexicon.resolveLexicon",
137 get(super::handle_xrpc_resolve_lexicon::handle_xrpc_resolve_lexicon)
138 .options(super::handle_xrpc_resolve_lexicon::handle_xrpc_resolve_lexicon_options),
139 )
140 .fallback_service(ServeDir::new(static_dir))
141 .layer(middleware::from_fn_with_state(
142 app_context.0.metrics.clone(),
143 metrics_middleware,
144 ))
145 .with_state(app_context)
146}
147
148pub(super) async fn handle_xrpc_health() -> Json<serde_json::Value> {
149 Json(json!({
150 "version": "0.1.0",
151 }))
152}