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.

refactor: split handle resolution

Changed files
+403 -151
src
+12
src/config.rs
··· 266 266 /// use quickdid::config::{Args, Config}; 267 267 /// use clap::Parser; 268 268 /// 269 + /// # fn main() -> Result<(), Box<dyn std::error::Error>> { 269 270 /// let args = Args::parse(); 270 271 /// let config = Config::from_args(args)?; 271 272 /// config.validate()?; 272 273 /// 273 274 /// println!("Service running at: {}", config.http_external); 274 275 /// println!("Service DID: {}", config.service_did); 276 + /// # Ok(()) 277 + /// # } 275 278 /// ``` 276 279 #[derive(Clone)] 277 280 pub struct Config { ··· 349 352 /// use quickdid::config::{Args, Config}; 350 353 /// use clap::Parser; 351 354 /// 355 + /// # fn main() -> Result<(), Box<dyn std::error::Error>> { 352 356 /// // Parse from environment and command-line 353 357 /// let args = Args::parse(); 354 358 /// let config = Config::from_args(args)?; 355 359 /// 356 360 /// // The service DID is automatically generated from HTTP_EXTERNAL 357 361 /// assert!(config.service_did.starts_with("did:web:")); 362 + /// # Ok(()) 363 + /// # } 358 364 /// ``` 359 365 /// 360 366 /// ## Errors ··· 477 483 /// ## Example 478 484 /// 479 485 /// ```rust,no_run 486 + /// # use quickdid::config::{Args, Config}; 487 + /// # use clap::Parser; 488 + /// # fn main() -> Result<(), Box<dyn std::error::Error>> { 489 + /// # let args = Args::parse(); 480 490 /// let config = Config::from_args(args)?; 481 491 /// config.validate()?; // Ensures all values are valid 492 + /// # Ok(()) 493 + /// # } 482 494 /// ``` 483 495 /// 484 496 /// ## Errors
+57 -151
src/handle_resolver.rs src/handle_resolver/redis.rs
··· 1 + //! Redis-backed caching handle resolver. 2 + //! 3 + //! This module provides a handle resolver that caches resolution results in Redis 4 + //! with configurable expiration times. Redis caching provides persistence across 5 + //! service restarts and allows sharing of cached results across multiple instances. 6 + 7 + use super::errors::HandleResolverError; 8 + use super::traits::HandleResolver; 1 9 use crate::handle_resolution_result::HandleResolutionResult; 2 10 use async_trait::async_trait; 3 - use atproto_identity::resolve::{DnsResolver, resolve_subject}; 4 - use chrono::Utc; 5 11 use deadpool_redis::{Pool as RedisPool, redis::AsyncCommands}; 6 12 use metrohash::MetroHash64; 7 - use reqwest::Client; 8 - use std::collections::HashMap; 9 13 use std::hash::Hasher as _; 10 14 use std::sync::Arc; 11 - use thiserror::Error; 12 - use tokio::sync::RwLock; 13 - 14 - /// Errors that can occur during handle resolution 15 - #[derive(Error, Debug)] 16 - pub enum HandleResolverError { 17 - #[error("error-quickdid-resolve-1 Failed to resolve subject: {0}")] 18 - ResolutionFailed(String), 19 - 20 - #[error("error-quickdid-resolve-2 Handle not found (cached): {0}")] 21 - HandleNotFoundCached(String), 22 - 23 - #[error("error-quickdid-resolve-3 Handle not found (cached)")] 24 - HandleNotFound, 25 15 26 - #[error("error-quickdid-resolve-4 Mock resolution failure")] 27 - MockResolutionFailure, 28 - } 29 - 30 - #[async_trait] 31 - pub trait HandleResolver: Send + Sync { 32 - async fn resolve(&self, s: &str) -> Result<String, HandleResolverError>; 33 - } 34 - 35 - pub struct BaseHandleResolver { 36 - /// DNS resolver for handle-to-DID resolution via TXT records. 37 - pub dns_resolver: Arc<dyn DnsResolver>, 38 - /// HTTP client for DID document retrieval and well-known endpoint queries. 39 - pub http_client: Client, 40 - /// Hostname of the PLC directory server for `did:plc` resolution. 41 - pub plc_hostname: String, 42 - } 43 - 44 - #[async_trait] 45 - impl HandleResolver for BaseHandleResolver { 46 - async fn resolve(&self, s: &str) -> Result<String, HandleResolverError> { 47 - resolve_subject(&self.http_client, &*self.dns_resolver, s) 48 - .await 49 - .map_err(|e| HandleResolverError::ResolutionFailed(e.to_string())) 50 - } 51 - } 52 - 53 - #[derive(Clone, Debug)] 54 - pub enum ResolveHandleResult { 55 - Found(u64, String), 56 - NotFound(u64, String), 57 - } 58 - 59 - pub struct CachingHandleResolver { 60 - inner: Arc<dyn HandleResolver>, 61 - cache: Arc<RwLock<HashMap<String, ResolveHandleResult>>>, 62 - ttl_seconds: u64, 63 - } 64 - 65 - impl CachingHandleResolver { 66 - pub fn new(inner: Arc<dyn HandleResolver>, ttl_seconds: u64) -> Self { 67 - Self { 68 - inner, 69 - cache: Arc::new(RwLock::new(HashMap::new())), 70 - ttl_seconds, 71 - } 72 - } 73 - 74 - fn current_timestamp() -> u64 { 75 - Utc::now().timestamp() as u64 76 - } 77 - 78 - fn is_expired(&self, timestamp: u64) -> bool { 79 - let current = Self::current_timestamp(); 80 - current > timestamp && (current - timestamp) > self.ttl_seconds 81 - } 82 - } 83 - 84 - #[async_trait] 85 - impl HandleResolver for CachingHandleResolver { 86 - async fn resolve(&self, s: &str) -> Result<String, HandleResolverError> { 87 - let handle = s.to_string(); 88 - 89 - // Check cache first 90 - { 91 - let cache = self.cache.read().await; 92 - if let Some(cached) = cache.get(&handle) { 93 - match cached { 94 - ResolveHandleResult::Found(timestamp, did) => { 95 - if !self.is_expired(*timestamp) { 96 - tracing::debug!("Cache hit for handle {}: {}", handle, did); 97 - return Ok(did.clone()); 98 - } 99 - tracing::debug!("Cache entry expired for handle {}", handle); 100 - } 101 - ResolveHandleResult::NotFound(timestamp, error) => { 102 - if !self.is_expired(*timestamp) { 103 - tracing::debug!( 104 - "Cache hit (not found) for handle {}: {}", 105 - handle, 106 - error 107 - ); 108 - return Err(HandleResolverError::HandleNotFoundCached(error.clone())); 109 - } 110 - tracing::debug!("Cache entry expired for handle {}", handle); 111 - } 112 - } 113 - } 114 - } 115 - 116 - // Not in cache or expired, resolve through inner resolver 117 - tracing::debug!("Cache miss for handle {}, resolving...", handle); 118 - let result = self.inner.resolve(s).await; 119 - let timestamp = Self::current_timestamp(); 120 - 121 - // Store in cache 122 - { 123 - let mut cache = self.cache.write().await; 124 - match &result { 125 - Ok(did) => { 126 - cache.insert( 127 - handle.clone(), 128 - ResolveHandleResult::Found(timestamp, did.clone()), 129 - ); 130 - tracing::debug!( 131 - "Cached successful resolution for handle {}: {}", 132 - handle, 133 - did 134 - ); 135 - } 136 - Err(e) => { 137 - cache.insert( 138 - handle.clone(), 139 - ResolveHandleResult::NotFound(timestamp, e.to_string()), 140 - ); 141 - tracing::debug!("Cached failed resolution for handle {}: {}", handle, e); 142 - } 143 - } 144 - } 145 - 146 - result 147 - } 148 - } 149 - 150 - /// Redis-backed caching handle resolver that caches resolution results in Redis 151 - /// with a configurable expiration time. 16 + /// Redis-backed caching handle resolver. 17 + /// 18 + /// This resolver caches handle resolution results in Redis with a configurable TTL. 19 + /// Results are stored in a compact binary format using bincode serialization 20 + /// to minimize storage overhead. 21 + /// 22 + /// # Features 23 + /// 24 + /// - Persistent caching across service restarts 25 + /// - Shared cache across multiple service instances 26 + /// - Configurable TTL (default: 90 days) 27 + /// - Compact binary storage format 28 + /// - Graceful fallback if Redis is unavailable 29 + /// 30 + /// # Example 31 + /// 32 + /// ```no_run 33 + /// use std::sync::Arc; 34 + /// use deadpool_redis::Pool; 35 + /// use quickdid::handle_resolver::{RedisHandleResolver, BaseHandleResolver}; 36 + /// 37 + /// # async fn example() { 38 + /// # let base_resolver: BaseHandleResolver = todo!(); 39 + /// # let redis_pool: Pool = todo!(); 40 + /// // Create with default 90-day TTL 41 + /// let resolver = RedisHandleResolver::new( 42 + /// Arc::new(base_resolver), 43 + /// redis_pool.clone() 44 + /// ); 45 + /// 46 + /// // Or with custom TTL 47 + /// let resolver_with_ttl = RedisHandleResolver::with_ttl( 48 + /// Arc::new(base_resolver), 49 + /// redis_pool, 50 + /// 86400 // 1 day in seconds 51 + /// ); 52 + /// # } 53 + /// ``` 152 54 pub struct RedisHandleResolver { 153 55 /// Base handle resolver to perform actual resolution 154 56 inner: Arc<dyn HandleResolver>, ··· 161 63 } 162 64 163 65 impl RedisHandleResolver { 164 - /// Create a new Redis-backed handle resolver with default 90-day TTL 66 + /// Create a new Redis-backed handle resolver with default 90-day TTL. 165 67 pub fn new(inner: Arc<dyn HandleResolver>, pool: RedisPool) -> Self { 166 68 Self::with_ttl(inner, pool, 90 * 24 * 60 * 60) // 90 days default 167 69 } 168 70 169 - /// Create a new Redis-backed handle resolver with custom TTL 71 + /// Create a new Redis-backed handle resolver with custom TTL. 170 72 pub fn with_ttl(inner: Arc<dyn HandleResolver>, pool: RedisPool, ttl_seconds: u64) -> Self { 171 73 Self::with_full_config(inner, pool, "handle:".to_string(), ttl_seconds) 172 74 } 173 75 174 - /// Create a new Redis-backed handle resolver with a custom key prefix 76 + /// Create a new Redis-backed handle resolver with a custom key prefix. 175 77 pub fn with_prefix( 176 78 inner: Arc<dyn HandleResolver>, 177 79 pool: RedisPool, ··· 180 82 Self::with_full_config(inner, pool, key_prefix, 90 * 24 * 60 * 60) 181 83 } 182 84 183 - /// Create a new Redis-backed handle resolver with full configuration 85 + /// Create a new Redis-backed handle resolver with full configuration. 184 86 pub fn with_full_config( 185 87 inner: Arc<dyn HandleResolver>, 186 88 pool: RedisPool, ··· 195 97 } 196 98 } 197 99 198 - /// Generate the Redis key for a handle 100 + /// Generate the Redis key for a handle. 101 + /// 102 + /// Uses MetroHash64 to generate a consistent hash of the handle 103 + /// for use as the Redis key. This provides better key distribution 104 + /// and avoids issues with special characters in handles. 199 105 fn make_key(&self, handle: &str) -> String { 200 106 let mut h = MetroHash64::default(); 201 107 h.write(handle.as_bytes()); 202 108 format!("{}{}", self.key_prefix, h.finish()) 203 109 } 204 110 205 - /// Get the TTL in seconds 111 + /// Get the TTL in seconds. 206 112 fn ttl_seconds(&self) -> u64 { 207 113 self.ttl_seconds 208 114 } ··· 445 351 let _: Result<(), _> = conn.del(key).await; 446 352 } 447 353 } 448 - } 354 + }
+59
src/handle_resolver/base.rs
··· 1 + //! Base handle resolver implementation. 2 + //! 3 + //! This module provides the fundamental handle resolution implementation that 4 + //! performs actual DNS and HTTP lookups to resolve AT Protocol handles to DIDs. 5 + 6 + use super::errors::HandleResolverError; 7 + use super::traits::HandleResolver; 8 + use async_trait::async_trait; 9 + use atproto_identity::resolve::{DnsResolver, resolve_subject}; 10 + use reqwest::Client; 11 + use std::sync::Arc; 12 + 13 + /// Base handle resolver that performs actual resolution via DNS and HTTP. 14 + /// 15 + /// This resolver implements the core AT Protocol handle resolution logic: 16 + /// 1. DNS TXT record lookup for `_atproto.{handle}` 17 + /// 2. HTTP well-known endpoint query at `https://{handle}/.well-known/atproto-did` 18 + /// 3. DID document retrieval from PLC directory or web DIDs 19 + /// 20 + /// # Example 21 + /// 22 + /// ```no_run 23 + /// use std::sync::Arc; 24 + /// use reqwest::Client; 25 + /// use atproto_identity::resolve::HickoryDnsResolver; 26 + /// use quickdid::handle_resolver::{BaseHandleResolver, HandleResolver}; 27 + /// 28 + /// # async fn example() { 29 + /// let dns_resolver = Arc::new(HickoryDnsResolver::create_resolver(&[])); 30 + /// let http_client = Client::new(); 31 + /// 32 + /// let resolver = BaseHandleResolver { 33 + /// dns_resolver, 34 + /// http_client, 35 + /// plc_hostname: "plc.directory".to_string(), 36 + /// }; 37 + /// 38 + /// let did = resolver.resolve("alice.bsky.social").await.unwrap(); 39 + /// # } 40 + /// ``` 41 + pub struct BaseHandleResolver { 42 + /// DNS resolver for handle-to-DID resolution via TXT records. 43 + pub dns_resolver: Arc<dyn DnsResolver>, 44 + 45 + /// HTTP client for DID document retrieval and well-known endpoint queries. 46 + pub http_client: Client, 47 + 48 + /// Hostname of the PLC directory server for `did:plc` resolution. 49 + pub plc_hostname: String, 50 + } 51 + 52 + #[async_trait] 53 + impl HandleResolver for BaseHandleResolver { 54 + async fn resolve(&self, s: &str) -> Result<String, HandleResolverError> { 55 + resolve_subject(&self.http_client, &*self.dns_resolver, s) 56 + .await 57 + .map_err(|e| HandleResolverError::ResolutionFailed(e.to_string())) 58 + } 59 + }
+26
src/handle_resolver/errors.rs
··· 1 + //! Error types for handle resolution operations. 2 + //! 3 + //! This module defines all error types used throughout the handle resolver components, 4 + //! following the QuickDID error format conventions. 5 + 6 + use thiserror::Error; 7 + 8 + /// Errors that can occur during handle resolution 9 + #[derive(Error, Debug)] 10 + pub enum HandleResolverError { 11 + /// Failed to resolve subject through DNS or HTTP 12 + #[error("error-quickdid-resolve-1 Failed to resolve subject: {0}")] 13 + ResolutionFailed(String), 14 + 15 + /// Handle not found in cache with specific error message 16 + #[error("error-quickdid-resolve-2 Handle not found (cached): {0}")] 17 + HandleNotFoundCached(String), 18 + 19 + /// Handle not found in cache (generic) 20 + #[error("error-quickdid-resolve-3 Handle not found (cached)")] 21 + HandleNotFound, 22 + 23 + /// Mock resolver failure for testing 24 + #[error("error-quickdid-resolve-4 Mock resolution failure")] 25 + MockResolutionFailure, 26 + }
+145
src/handle_resolver/memory.rs
··· 1 + //! In-memory caching handle resolver. 2 + //! 3 + //! This module provides a handle resolver that caches resolution results in memory 4 + //! with a configurable TTL. This is useful for reducing DNS/HTTP lookups and 5 + //! improving performance when Redis is not available. 6 + 7 + use super::errors::HandleResolverError; 8 + use super::traits::HandleResolver; 9 + use async_trait::async_trait; 10 + use chrono::Utc; 11 + use std::collections::HashMap; 12 + use std::sync::Arc; 13 + use tokio::sync::RwLock; 14 + 15 + /// Result of a handle resolution cached in memory. 16 + #[derive(Clone, Debug)] 17 + pub enum ResolveHandleResult { 18 + /// Handle was successfully resolved to a DID 19 + Found(u64, String), 20 + /// Handle resolution failed 21 + NotFound(u64, String), 22 + } 23 + 24 + /// In-memory caching wrapper for handle resolvers. 25 + /// 26 + /// This resolver wraps another resolver and caches results in memory with 27 + /// a configurable TTL. Both successful and failed resolutions are cached 28 + /// to avoid repeated lookups. 29 + /// 30 + /// # Example 31 + /// 32 + /// ```no_run 33 + /// use std::sync::Arc; 34 + /// use quickdid::handle_resolver::{CachingHandleResolver, BaseHandleResolver, HandleResolver}; 35 + /// 36 + /// # async fn example() { 37 + /// # let base_resolver: BaseHandleResolver = todo!(); 38 + /// let caching_resolver = CachingHandleResolver::new( 39 + /// Arc::new(base_resolver), 40 + /// 300 // 5 minute TTL 41 + /// ); 42 + /// 43 + /// // First call hits the underlying resolver 44 + /// let did1 = caching_resolver.resolve("alice.bsky.social").await.unwrap(); 45 + /// 46 + /// // Second call returns cached result 47 + /// let did2 = caching_resolver.resolve("alice.bsky.social").await.unwrap(); 48 + /// # } 49 + /// ``` 50 + pub struct CachingHandleResolver { 51 + inner: Arc<dyn HandleResolver>, 52 + cache: Arc<RwLock<HashMap<String, ResolveHandleResult>>>, 53 + ttl_seconds: u64, 54 + } 55 + 56 + impl CachingHandleResolver { 57 + /// Create a new caching handle resolver. 58 + /// 59 + /// # Arguments 60 + /// 61 + /// * `inner` - The underlying resolver to use for actual resolution 62 + /// * `ttl_seconds` - How long to cache results in seconds 63 + pub fn new(inner: Arc<dyn HandleResolver>, ttl_seconds: u64) -> Self { 64 + Self { 65 + inner, 66 + cache: Arc::new(RwLock::new(HashMap::new())), 67 + ttl_seconds, 68 + } 69 + } 70 + 71 + fn current_timestamp() -> u64 { 72 + Utc::now().timestamp() as u64 73 + } 74 + 75 + fn is_expired(&self, timestamp: u64) -> bool { 76 + let current = Self::current_timestamp(); 77 + current > timestamp && (current - timestamp) > self.ttl_seconds 78 + } 79 + } 80 + 81 + #[async_trait] 82 + impl HandleResolver for CachingHandleResolver { 83 + async fn resolve(&self, s: &str) -> Result<String, HandleResolverError> { 84 + let handle = s.to_string(); 85 + 86 + // Check cache first 87 + { 88 + let cache = self.cache.read().await; 89 + if let Some(cached) = cache.get(&handle) { 90 + match cached { 91 + ResolveHandleResult::Found(timestamp, did) => { 92 + if !self.is_expired(*timestamp) { 93 + tracing::debug!("Cache hit for handle {}: {}", handle, did); 94 + return Ok(did.clone()); 95 + } 96 + tracing::debug!("Cache entry expired for handle {}", handle); 97 + } 98 + ResolveHandleResult::NotFound(timestamp, error) => { 99 + if !self.is_expired(*timestamp) { 100 + tracing::debug!( 101 + "Cache hit (not found) for handle {}: {}", 102 + handle, 103 + error 104 + ); 105 + return Err(HandleResolverError::HandleNotFoundCached(error.clone())); 106 + } 107 + tracing::debug!("Cache entry expired for handle {}", handle); 108 + } 109 + } 110 + } 111 + } 112 + 113 + // Not in cache or expired, resolve through inner resolver 114 + tracing::debug!("Cache miss for handle {}, resolving...", handle); 115 + let result = self.inner.resolve(s).await; 116 + let timestamp = Self::current_timestamp(); 117 + 118 + // Store in cache 119 + { 120 + let mut cache = self.cache.write().await; 121 + match &result { 122 + Ok(did) => { 123 + cache.insert( 124 + handle.clone(), 125 + ResolveHandleResult::Found(timestamp, did.clone()), 126 + ); 127 + tracing::debug!( 128 + "Cached successful resolution for handle {}: {}", 129 + handle, 130 + did 131 + ); 132 + } 133 + Err(e) => { 134 + cache.insert( 135 + handle.clone(), 136 + ResolveHandleResult::NotFound(timestamp, e.to_string()), 137 + ); 138 + tracing::debug!("Cached failed resolution for handle {}: {}", handle, e); 139 + } 140 + } 141 + } 142 + 143 + result 144 + } 145 + }
+54
src/handle_resolver/mod.rs
··· 1 + //! Handle resolution module for AT Protocol identity resolution. 2 + //! 3 + //! This module provides various implementations of handle-to-DID resolution 4 + //! with different caching strategies. 5 + //! 6 + //! # Architecture 7 + //! 8 + //! The module is structured around the [`HandleResolver`] trait with multiple 9 + //! implementations: 10 + //! 11 + //! - [`BaseHandleResolver`]: Core resolver that performs actual DNS/HTTP lookups 12 + //! - [`CachingHandleResolver`]: In-memory caching wrapper with configurable TTL 13 + //! - [`RedisHandleResolver`]: Redis-backed persistent caching with binary serialization 14 + //! 15 + //! # Example Usage 16 + //! 17 + //! ```no_run 18 + //! use std::sync::Arc; 19 + //! use quickdid::handle_resolver::{BaseHandleResolver, CachingHandleResolver, HandleResolver}; 20 + //! 21 + //! # async fn example() -> Result<(), Box<dyn std::error::Error>> { 22 + //! # use atproto_identity::resolve::HickoryDnsResolver; 23 + //! # use reqwest::Client; 24 + //! # let dns_resolver = Arc::new(HickoryDnsResolver::create_resolver(&[])); 25 + //! # let http_client = Client::new(); 26 + //! // Create base resolver 27 + //! let base = Arc::new(BaseHandleResolver { 28 + //! dns_resolver, 29 + //! http_client, 30 + //! plc_hostname: "plc.directory".to_string(), 31 + //! }); 32 + //! 33 + //! // Wrap with in-memory caching 34 + //! let resolver = CachingHandleResolver::new(base, 300); 35 + //! 36 + //! // Resolve a handle 37 + //! let did = resolver.resolve("alice.bsky.social").await?; 38 + //! # Ok(()) 39 + //! # } 40 + //! ``` 41 + 42 + // Module structure 43 + mod errors; 44 + mod traits; 45 + mod base; 46 + mod memory; 47 + mod redis; 48 + 49 + // Re-export public API 50 + pub use errors::HandleResolverError; 51 + pub use traits::HandleResolver; 52 + pub use base::BaseHandleResolver; 53 + pub use memory::{CachingHandleResolver, ResolveHandleResult}; 54 + pub use redis::RedisHandleResolver;
+50
src/handle_resolver/traits.rs
··· 1 + //! Core traits for handle resolution. 2 + //! 3 + //! This module defines the fundamental `HandleResolver` trait that all resolver 4 + //! implementations must satisfy. 5 + 6 + use super::errors::HandleResolverError; 7 + use async_trait::async_trait; 8 + 9 + /// Core trait for handle-to-DID resolution. 10 + /// 11 + /// Implementations of this trait provide different strategies for resolving 12 + /// AT Protocol handles (like `alice.bsky.social`) to their corresponding 13 + /// DID identifiers (like `did:plc:xyz123`). 14 + /// 15 + /// # Examples 16 + /// 17 + /// ```no_run 18 + /// use async_trait::async_trait; 19 + /// use quickdid::handle_resolver::{HandleResolver, HandleResolverError}; 20 + /// 21 + /// struct MyResolver; 22 + /// 23 + /// #[async_trait] 24 + /// impl HandleResolver for MyResolver { 25 + /// async fn resolve(&self, s: &str) -> Result<String, HandleResolverError> { 26 + /// // Custom resolution logic 27 + /// Ok(format!("did:plc:{}", s.replace('.', ""))) 28 + /// } 29 + /// } 30 + /// ``` 31 + #[async_trait] 32 + pub trait HandleResolver: Send + Sync { 33 + /// Resolve a handle to its DID. 34 + /// 35 + /// # Arguments 36 + /// 37 + /// * `s` - The handle to resolve (e.g., "alice.bsky.social") 38 + /// 39 + /// # Returns 40 + /// 41 + /// The resolved DID on success, or an error if resolution fails. 42 + /// 43 + /// # Errors 44 + /// 45 + /// Returns [`HandleResolverError`] if: 46 + /// - The handle cannot be resolved 47 + /// - Network errors occur during resolution 48 + /// - The handle is invalid or doesn't exist 49 + async fn resolve(&self, s: &str) -> Result<String, HandleResolverError>; 50 + }