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.

feature: cache-control header

+231 -2
+128
docs/configuration-reference.md
··· 550 550 551 551 ## HTTP Caching Configuration 552 552 553 + ### `CACHE_MAX_AGE` 554 + 555 + **Required**: No 556 + **Type**: Integer (seconds) 557 + **Default**: `86400` (24 hours) 558 + **Range**: 0-31536000 (0 to 1 year) 559 + 560 + Maximum age for HTTP Cache-Control header in seconds. When set to 0, the Cache-Control header is disabled and will not be added to responses. This controls how long clients and intermediate caches can cache responses. 561 + 562 + **Examples**: 563 + ```bash 564 + # Default (24 hours) 565 + CACHE_MAX_AGE=86400 566 + 567 + # Aggressive caching (7 days) 568 + CACHE_MAX_AGE=604800 569 + 570 + # Conservative caching (1 hour) 571 + CACHE_MAX_AGE=3600 572 + 573 + # Disable Cache-Control header 574 + CACHE_MAX_AGE=0 575 + ``` 576 + 577 + ### `CACHE_STALE_IF_ERROR` 578 + 579 + **Required**: No 580 + **Type**: Integer (seconds) 581 + **Default**: `172800` (48 hours) 582 + 583 + Allows stale content to be served if the backend encounters an error. This provides resilience during service outages. 584 + 585 + **Examples**: 586 + ```bash 587 + # Default (48 hours) 588 + CACHE_STALE_IF_ERROR=172800 589 + 590 + # Extended error tolerance (7 days) 591 + CACHE_STALE_IF_ERROR=604800 592 + 593 + # Minimal error tolerance (1 hour) 594 + CACHE_STALE_IF_ERROR=3600 595 + ``` 596 + 597 + ### `CACHE_STALE_WHILE_REVALIDATE` 598 + 599 + **Required**: No 600 + **Type**: Integer (seconds) 601 + **Default**: `86400` (24 hours) 602 + 603 + Allows stale content to be served while fresh content is being fetched in the background. This improves perceived performance. 604 + 605 + **Examples**: 606 + ```bash 607 + # Default (24 hours) 608 + CACHE_STALE_WHILE_REVALIDATE=86400 609 + 610 + # Quick revalidation (1 hour) 611 + CACHE_STALE_WHILE_REVALIDATE=3600 612 + 613 + # Extended revalidation (7 days) 614 + CACHE_STALE_WHILE_REVALIDATE=604800 615 + ``` 616 + 617 + ### `CACHE_MAX_STALE` 618 + 619 + **Required**: No 620 + **Type**: Integer (seconds) 621 + **Default**: `172800` (48 hours) 622 + 623 + Maximum time a client will accept stale responses. This provides an upper bound on how old cached content can be. 624 + 625 + **Examples**: 626 + ```bash 627 + # Default (48 hours) 628 + CACHE_MAX_STALE=172800 629 + 630 + # Extended staleness (7 days) 631 + CACHE_MAX_STALE=604800 632 + 633 + # Strict freshness (1 hour) 634 + CACHE_MAX_STALE=3600 635 + ``` 636 + 637 + ### `CACHE_MIN_FRESH` 638 + 639 + **Required**: No 640 + **Type**: Integer (seconds) 641 + **Default**: `3600` (1 hour) 642 + 643 + Minimum time a response must remain fresh. Clients will not accept responses that will expire within this time. 644 + 645 + **Examples**: 646 + ```bash 647 + # Default (1 hour) 648 + CACHE_MIN_FRESH=3600 649 + 650 + # Strict freshness (24 hours) 651 + CACHE_MIN_FRESH=86400 652 + 653 + # Relaxed freshness (5 minutes) 654 + CACHE_MIN_FRESH=300 655 + ``` 656 + 657 + **Cache-Control Header Format**: 658 + 659 + When `CACHE_MAX_AGE` is greater than 0, the following Cache-Control header is added to responses: 660 + ``` 661 + Cache-Control: public, max-age=86400, stale-while-revalidate=86400, stale-if-error=172800, max-stale=172800, min-fresh=3600 662 + ``` 663 + 664 + **Recommendations**: 665 + - **High-traffic services**: Use longer max-age (86400-604800) to reduce load 666 + - **Frequently changing data**: Use shorter max-age (3600-14400) 667 + - **Critical services**: Set higher stale-if-error for resilience 668 + - **Performance-sensitive**: Enable stale-while-revalidate for better UX 669 + - **Disable caching**: Set CACHE_MAX_AGE=0 for real-time data 670 + 553 671 ### `ETAG_SEED` 554 672 555 673 **Required**: No ··· 633 751 RESOLVER_MAX_CONCURRENT=100 634 752 RESOLVER_MAX_CONCURRENT_TIMEOUT_MS=5000 # 5 second timeout 635 753 754 + # HTTP Caching (Cache-Control headers) 755 + CACHE_MAX_AGE=86400 # 24 hours 756 + CACHE_STALE_IF_ERROR=172800 # 48 hours 757 + CACHE_STALE_WHILE_REVALIDATE=86400 # 24 hours 758 + 636 759 # Logging 637 760 RUST_LOG=info 638 761 ``` ··· 662 785 # Rate Limiting (optional, recommended for production) 663 786 RESOLVER_MAX_CONCURRENT=100 664 787 RESOLVER_MAX_CONCURRENT_TIMEOUT_MS=5000 # 5 second timeout 788 + 789 + # HTTP Caching (Cache-Control headers) 790 + CACHE_MAX_AGE=86400 # 24 hours 791 + CACHE_STALE_IF_ERROR=172800 # 48 hours 792 + CACHE_STALE_WHILE_REVALIDATE=86400 # 24 hours 665 793 666 794 # Logging 667 795 RUST_LOG=info
+25
docs/production-deployment.md
··· 213 213 # Default uses the application version from Cargo.toml 214 214 # ETAG_SEED=prod-2024-01-15 215 215 216 + # Maximum age for HTTP Cache-Control header in seconds (default: 86400 = 24 hours) 217 + # Set to 0 to disable Cache-Control header 218 + # Controls how long clients and intermediate caches can cache responses 219 + CACHE_MAX_AGE=86400 220 + 221 + # Stale-if-error directive for Cache-Control in seconds (default: 172800 = 48 hours) 222 + # Allows stale content to be served if backend errors occur 223 + # Provides resilience during service outages 224 + CACHE_STALE_IF_ERROR=172800 225 + 226 + # Stale-while-revalidate directive for Cache-Control in seconds (default: 86400 = 24 hours) 227 + # Allows stale content to be served while fetching fresh content in background 228 + # Improves perceived performance for users 229 + CACHE_STALE_WHILE_REVALIDATE=86400 230 + 231 + # Max-stale directive for Cache-Control in seconds (default: 172800 = 48 hours) 232 + # Maximum time client will accept stale responses 233 + # Provides upper bound on cached content age 234 + CACHE_MAX_STALE=172800 235 + 236 + # Min-fresh directive for Cache-Control in seconds (default: 3600 = 1 hour) 237 + # Minimum time response must remain fresh 238 + # Clients won't accept responses expiring within this time 239 + CACHE_MIN_FRESH=3600 240 + 216 241 # ---------------------------------------------------------------------------- 217 242 # PERFORMANCE TUNING 218 243 # ----------------------------------------------------------------------------
+1
src/bin/quickdid.rs
··· 441 441 handle_resolver.clone(), 442 442 handle_queue, 443 443 config.etag_seed.clone(), 444 + config.cache_control_header.clone(), 444 445 ); 445 446 446 447 // Create router
+60 -2
src/config.rs
··· 181 181 /// to invalidate client-cached responses after major changes. 182 182 /// Default: application version 183 183 pub etag_seed: String, 184 + 185 + /// Maximum age for HTTP cache control in seconds. 186 + /// When set to 0, Cache-Control header is disabled. 187 + /// Default: 86400 (24 hours) 188 + pub cache_max_age: u64, 189 + 190 + /// Stale-if-error directive for Cache-Control in seconds. 191 + /// Allows stale content to be served if backend errors occur. 192 + /// Default: 172800 (48 hours) 193 + pub cache_stale_if_error: u64, 194 + 195 + /// Stale-while-revalidate directive for Cache-Control in seconds. 196 + /// Allows stale content to be served while fetching fresh content. 197 + /// Default: 86400 (24 hours) 198 + pub cache_stale_while_revalidate: u64, 199 + 200 + /// Max-stale directive for Cache-Control in seconds. 201 + /// Maximum time client will accept stale responses. 202 + /// Default: 172800 (48 hours) 203 + pub cache_max_stale: u64, 204 + 205 + /// Min-fresh directive for Cache-Control in seconds. 206 + /// Minimum time response must remain fresh. 207 + /// Default: 3600 (1 hour) 208 + pub cache_min_fresh: u64, 209 + 210 + /// Pre-calculated Cache-Control header value. 211 + /// Calculated at startup for efficiency. 212 + /// None if cache_max_age is 0 (disabled). 213 + pub cache_control_header: Option<String>, 184 214 } 185 215 186 216 impl Config { ··· 238 268 format!("did:web:{}", http_external) 239 269 }; 240 270 241 - Ok(Config { 271 + let mut config = Config { 242 272 http_port: get_env_or_default("HTTP_PORT", Some("8080")).unwrap(), 243 273 plc_hostname: get_env_or_default("PLC_HOSTNAME", Some("plc.directory")).unwrap(), 244 274 http_external, ··· 266 296 resolver_max_concurrent: parse_env("RESOLVER_MAX_CONCURRENT", 0)?, 267 297 resolver_max_concurrent_timeout_ms: parse_env("RESOLVER_MAX_CONCURRENT_TIMEOUT_MS", 0)?, 268 298 etag_seed: get_env_or_default("ETAG_SEED", Some(env!("CARGO_PKG_VERSION"))).unwrap(), 269 - }) 299 + cache_max_age: parse_env("CACHE_MAX_AGE", 86400)?, // 24 hours 300 + cache_stale_if_error: parse_env("CACHE_STALE_IF_ERROR", 172800)?, // 48 hours 301 + cache_stale_while_revalidate: parse_env("CACHE_STALE_WHILE_REVALIDATE", 86400)?, // 24 hours 302 + cache_max_stale: parse_env("CACHE_MAX_STALE", 172800)?, // 48 hours 303 + cache_min_fresh: parse_env("CACHE_MIN_FRESH", 3600)?, // 1 hour 304 + cache_control_header: None, // Will be calculated below 305 + }; 306 + 307 + // Calculate the Cache-Control header value if enabled 308 + config.cache_control_header = config.calculate_cache_control_header(); 309 + 310 + Ok(config) 311 + } 312 + 313 + /// Calculate the Cache-Control header value based on configuration. 314 + /// Returns None if cache_max_age is 0 (disabled). 315 + fn calculate_cache_control_header(&self) -> Option<String> { 316 + if self.cache_max_age == 0 { 317 + return None; 318 + } 319 + 320 + Some(format!( 321 + "public, max-age={}, stale-while-revalidate={}, stale-if-error={}, max-stale={}, min-fresh={}", 322 + self.cache_max_age, 323 + self.cache_stale_while_revalidate, 324 + self.cache_stale_if_error, 325 + self.cache_max_stale, 326 + self.cache_min_fresh 327 + )) 270 328 } 271 329 272 330 /// Validate the configuration for correctness and consistency
+10
src/http/handle_xrpc_resolve_handle.rs
··· 159 159 } 160 160 }; 161 161 162 + // Add Cache-Control header if configured 163 + if let Some(cache_control) = app_context.cache_control_header() { 164 + if let Ok(cache_control_value) = HeaderValue::from_str(cache_control) { 165 + response 166 + .headers_mut() 167 + .insert(header::CACHE_CONTROL, cache_control_value); 168 + } 169 + } 170 + 171 + // Add ETag header 162 172 match HeaderValue::from_str(&etag) { 163 173 Ok(etag_header_value) => { 164 174 response
+7
src/http/server.rs
··· 16 16 pub(crate) handle_resolver: Arc<dyn HandleResolver>, 17 17 pub(crate) handle_queue: Arc<dyn QueueAdapter<HandleResolutionWork>>, 18 18 pub(crate) etag_seed: String, 19 + pub(crate) cache_control_header: Option<String>, 19 20 } 20 21 21 22 #[derive(Clone)] ··· 29 30 handle_resolver: Arc<dyn HandleResolver>, 30 31 handle_queue: Arc<dyn QueueAdapter<HandleResolutionWork>>, 31 32 etag_seed: String, 33 + cache_control_header: Option<String>, 32 34 ) -> Self { 33 35 Self(Arc::new(InnerAppContext { 34 36 service_document, ··· 36 38 handle_resolver, 37 39 handle_queue, 38 40 etag_seed, 41 + cache_control_header, 39 42 })) 40 43 } 41 44 ··· 50 53 51 54 pub(super) fn etag_seed(&self) -> &str { 52 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() 53 60 } 54 61 } 55 62