Skip to main content

reddb_server/
server.rs

1//! Minimal HTTP server for RedDB management and remote access.
2
3pub(crate) use crate::application::json_input::{
4    json_bool_field, json_f32_field, json_string_field, json_usize_field,
5};
6pub(crate) use crate::application::{
7    AdminUseCases, CreateDocumentInput, CreateEdgeInput, CreateEntityOutput, CreateKvInput,
8    CreateNodeEmbeddingInput, CreateNodeGraphLinkInput, CreateNodeInput, CreateNodeTableLinkInput,
9    CreateRowInput, CreateVectorInput, DeleteEntityInput, EntityUseCases, ExecuteQueryInput,
10    ExplainQueryInput, GraphCentralityInput, GraphClusteringInput, GraphCommunitiesInput,
11    GraphComponentsInput, GraphCyclesInput, GraphHitsInput, GraphNeighborhoodInput,
12    GraphPersonalizedPageRankInput, GraphShortestPathInput, GraphTopologicalSortInput,
13    GraphTraversalInput, GraphUseCases, InspectNativeArtifactInput, NativeUseCases,
14    PatchEntityInput, PatchEntityOperation, PatchEntityOperationType, SearchHybridInput,
15    SearchIvfInput, SearchMultimodalInput, SearchSimilarInput, SearchTextInput, TreeUseCases,
16};
17use std::collections::{BTreeMap, HashMap};
18use std::io::{self, Read, Write};
19use std::net::{TcpListener, TcpStream};
20use std::sync::atomic::{AtomicU64, Ordering};
21use std::thread;
22use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};
23
24use std::sync::Arc;
25
26use crate::api::{RedDBError, RedDBOptions, RedDBResult};
27use crate::auth::store::AuthStore;
28use crate::catalog::{CatalogModelSnapshot, CollectionDescriptor, CollectionModel, SchemaMode};
29use crate::health::{HealthProvider, HealthReport, HealthState};
30use crate::json::{parse_json, to_vec as json_to_vec, Map, Value as JsonValue};
31use crate::runtime::{
32    RedDBRuntime, RuntimeFilter, RuntimeFilterValue, RuntimeGraphCentralityAlgorithm,
33    RuntimeGraphCentralityResult, RuntimeGraphClusteringResult, RuntimeGraphCommunityAlgorithm,
34    RuntimeGraphCommunityResult, RuntimeGraphComponentsMode, RuntimeGraphComponentsResult,
35    RuntimeGraphCyclesResult, RuntimeGraphDirection, RuntimeGraphHitsResult,
36    RuntimeGraphNeighborhoodResult, RuntimeGraphPathAlgorithm, RuntimeGraphPathResult,
37    RuntimeGraphPattern, RuntimeGraphProjection, RuntimeGraphTopologicalSortResult,
38    RuntimeGraphTraversalResult, RuntimeGraphTraversalStrategy, RuntimeIvfSearchResult,
39    RuntimeQueryWeights, RuntimeStats, ScanCursor, ScanPage,
40};
41use crate::storage::unified::devx::refs::{NodeRef, TableRef, VectorRef};
42use crate::storage::unified::dsl::{MatchComponents, QueryResult as DslQueryResult};
43use crate::storage::unified::{MetadataValue, RefTarget, SparseVector};
44use crate::storage::{CrossRef, EntityData, EntityId, EntityKind, SimilarResult, UnifiedEntity};
45use crate::transport::execution_admission::ExecutionAdmission;
46use reddb_types::Value;
47
48fn analytics_job_json(job: &crate::PhysicalAnalyticsJob) -> JsonValue {
49    crate::presentation::admin_json::analytics_job_json(job)
50}
51
52// Process-wide test guard for every RED_ADMIN_TOKEN mutation in server tests.
53#[cfg(test)]
54pub(crate) static RED_ADMIN_TOKEN_ENV_LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());
55
56#[cfg(test)]
57mod tests {
58    use super::*;
59    use crate::api::RedDBOptions;
60    use crate::health::HealthReport;
61    use crate::transport::{TransportListenerFailure, TransportListenerState, TransportReadiness};
62
63    #[test]
64    fn server_options_default_http_body_limit_is_32_mib() {
65        assert_eq!(ServerOptions::default().max_body_bytes, 32 * 1024 * 1024);
66    }
67
68    #[test]
69    fn health_json_reports_transport_listeners() {
70        let runtime = RedDBRuntime::with_options(RedDBOptions::in_memory()).expect("runtime");
71        let mut options = ServerOptions::default();
72        options.transport_readiness = TransportReadiness {
73            active: vec![TransportListenerState {
74                transport: "grpc".to_string(),
75                bind_addr: "127.0.0.1:5000".to_string(),
76                explicit: true,
77            }],
78            failed: vec![TransportListenerFailure {
79                transport: "http".to_string(),
80                bind_addr: "127.0.0.1:5000".to_string(),
81                explicit: false,
82                reason: "http listener bind 127.0.0.1:5000: address in use".to_string(),
83            }],
84        };
85        let server = RedDBServer::with_options(runtime, options);
86
87        let payload = server.health_json_with_transport(&HealthReport::healthy());
88        let JsonValue::Object(root) = payload else {
89            panic!("health payload should be an object");
90        };
91        let Some(JsonValue::Object(listeners)) = root.get("transport_listeners") else {
92            panic!("health payload should include transport_listeners");
93        };
94        let Some(JsonValue::Array(active)) = listeners.get("active") else {
95            panic!("transport_listeners.active should be an array");
96        };
97        let Some(JsonValue::Array(failed)) = listeners.get("failed") else {
98            panic!("transport_listeners.failed should be an array");
99        };
100
101        assert_eq!(active.len(), 1);
102        assert_eq!(failed.len(), 1);
103    }
104}
105
106fn graph_projection_json(projection: &crate::PhysicalGraphProjection) -> JsonValue {
107    crate::presentation::admin_json::graph_projection_json(projection)
108}
109
110mod axum_edge;
111pub mod handlers_admin;
112mod handlers_admin_metrics;
113mod handlers_admin_status;
114mod handlers_ai;
115mod handlers_ai_model_cache;
116pub(crate) mod handlers_auth;
117mod handlers_backup;
118mod handlers_browser_auth;
119mod handlers_cache;
120mod handlers_capabilities;
121mod handlers_collection_policy;
122mod handlers_ec;
123pub(crate) mod handlers_entity;
124mod handlers_failover;
125mod handlers_geo;
126mod handlers_graph;
127mod handlers_iam_policy;
128mod handlers_keyed;
129mod handlers_log;
130mod handlers_metrics;
131mod handlers_ops;
132mod handlers_ops_policy;
133/// Local `red ui` bridge: serves the UI bundle and fronts the embedded
134/// engine over RedWire-over-WebSocket for `file://` targets (issue #1042,
135/// ADR 0047/0049). Public so the `red ui` CLI command can drive it.
136pub mod ui_bridge;
137mod ws_edge;
138// `red ui` bundle resolver (issue #1043, ADR 0050): pin→URL resolution,
139// HTTPS download, SHA-256 verification, tgz extraction, and local cache
140// management for the pinned red-ui release asset.
141pub mod ui_bundle_resolver;
142// `red ui --token` credential handoff (issue #1048, ADR 0051): one-time
143// loopback nonce channel and served-UI auth-mode wiring.
144pub mod ui_auth;
145// `red server --ui` static bundle surface (issue #1047, ADR 0051): serve
146// the resolved red-ui bundle as inert static assets on the server's HTTP
147// edge so a remote browser can load it and connect back over RedWire-over-WS.
148mod ui_static;
149// `red ui <uri>` deep-link dispatch (issue #1046, ADR 0051): prefer the
150// installed desktop app via the `redui://` scheme, fall back to the served
151// browser bridge when no handler is registered. Decision + canonical
152// deep-link string live behind a testable seam.
153pub mod ui_deeplink;
154// `pub(crate)` so the RedWire input-stream path (issue #764 / S5)
155// can reuse the canonical S4 INSERT builders / identifier checks
156// (`build_insert_sql`, `is_safe_sql_identifier`) rather than fork
157// the SQL-escaping logic.
158mod execution_watchdog;
159pub(crate) mod handlers_query;
160mod handlers_replication;
161mod handlers_topology;
162mod handlers_vector;
163pub mod header_escape_guard;
164pub mod http_connection_limiter;
165pub mod http_handler_metrics;
166pub mod http_limits;
167pub mod http_principal_limiter;
168pub mod http_request_metrics;
169pub mod ingest_pipeline;
170pub mod output_stream;
171mod patch_support;
172mod request_body;
173mod request_context;
174mod request_identity;
175pub(crate) mod route_catalog;
176mod routes;
177pub(crate) use routes::discovered_route_catalog;
178mod routing;
179mod serverless_support;
180pub mod tls;
181mod transport;
182mod transport_surface;
183
184/// Generate the command-coverage artifact: every discovered command joined
185/// against each transport's own binding table. See
186/// [`route_catalog::render_command_coverage_matrix`].
187#[doc(hidden)]
188pub fn command_coverage_matrix() -> String {
189    route_catalog::render_command_coverage_matrix(routes::discovered_route_catalog())
190}
191
192use self::execution_watchdog::ExecutionWatchdog;
193use self::handlers_ai::*;
194use self::handlers_entity::*;
195use self::handlers_graph::*;
196use self::handlers_keyed::*;
197use self::handlers_metrics::*;
198use self::handlers_ops::*;
199use self::handlers_query::*;
200use self::http_connection_limiter::{
201    HandlerDeadline, HttpConnectionLimiter, MonotonicClock, SystemMonotonicClock,
202};
203use self::http_handler_metrics::{HttpHandlerMetrics, HttpRejectReason, HttpTransport};
204pub use self::http_limits::{
205    HttpLimitsCliInput, HttpLimitsResolved, DEFAULT_HANDLER_TIMEOUT_MS, DEFAULT_RETRY_AFTER_SECS,
206};
207use self::http_principal_limiter::PrincipalConnectionLimiter;
208use self::http_request_metrics::HttpRequestMetrics;
209use self::patch_support::*;
210pub(crate) use self::request_body::is_trivial_scalar_sql;
211use self::request_body::*;
212use self::routing::*;
213use self::serverless_support::*;
214use self::transport::*;
215
216pub(crate) fn command_catalog() -> &'static route_catalog::CommandCatalog {
217    routes::discovered_route_catalog()
218}
219
220/// PLAN.md Phase 6.2 — endpoint segregation. A given HTTP listener
221/// can serve either every public surface (`Public`, default) or a
222/// restricted slice (`AdminOnly`, `MetricsOnly`). The route filter at
223/// the top of `route()` consults this so a port bound only to
224/// loopback for admin work won't accidentally hand out DML.
225#[derive(Debug, Clone, Copy, PartialEq, Eq)]
226pub enum ServerSurface {
227    /// Everything routed normally (default — matches v0 behaviour).
228    Public,
229    /// Only `/admin/*`, `/metrics`, and `/health/*`. Other paths
230    /// return 404. Intended for `RED_ADMIN_BIND` operator listeners
231    /// which default to `127.0.0.1`.
232    AdminOnly,
233    /// Only `/metrics` and `/health/*`. Intended for
234    /// `RED_METRICS_BIND` Prometheus scrape ports that may be
235    /// exposed to non-admin networks.
236    MetricsOnly,
237}
238
239#[derive(Debug, Clone)]
240pub struct ServerOptions {
241    pub bind_addr: String,
242    pub max_body_bytes: usize,
243    pub read_timeout_ms: u64,
244    pub write_timeout_ms: u64,
245    pub max_scan_limit: usize,
246    /// Which subset of paths this listener serves. Defaults to
247    /// `Public`. Set to `AdminOnly` / `MetricsOnly` for dedicated
248    /// admin / scrape ports (PLAN.md Phase 6.2).
249    pub surface: ServerSurface,
250    pub transport_readiness: crate::transport::TransportReadiness,
251    /// Allowed `Origin` values for the RedWire-over-WSS browser endpoint
252    /// (issue #935, ADR 0036). WebSocket is not covered by CORS, so the
253    /// upgrade is gated on an explicit allowlist to block Cross-Site
254    /// WebSocket Hijacking. **Default-deny:** empty means the `/redwire`
255    /// route is not mounted at all — operators opt in by configuring at
256    /// least one origin. Matched exactly (scheme+host+port), e.g.
257    /// `https://app.example.com`.
258    pub websocket_allowed_origins: Vec<String>,
259    /// `red server --ui` (issue #1047, ADR 0051). When `Some`, this
260    /// listener also serves the resolved red-ui bundle from this directory
261    /// as inert static assets (`/` → `index.html`), alongside the existing
262    /// API. The assets carry no credential, so exposing them is safe by
263    /// construction — auth lives on the RedWire data endpoint, not the
264    /// asset path. Network reach is governed entirely by `bind_addr`
265    /// (default localhost), so this never widens reach on its own.
266    /// `None` (the default) leaves the listener API-only.
267    pub ui_dir: Option<std::path::PathBuf>,
268}
269
270pub const DEFAULT_HTTP_MAX_BODY_BYTES: usize = 32 * 1024 * 1024;
271
272impl Default for ServerOptions {
273    fn default() -> Self {
274        Self {
275            bind_addr: "127.0.0.1:5000".to_string(),
276            max_body_bytes: DEFAULT_HTTP_MAX_BODY_BYTES,
277            read_timeout_ms: 5_000,
278            write_timeout_ms: 5_000,
279            max_scan_limit: 1_000,
280            surface: ServerSurface::Public,
281            transport_readiness: crate::transport::TransportReadiness::default(),
282            websocket_allowed_origins: Vec::new(),
283            ui_dir: None,
284        }
285    }
286}
287
288/// Replication state exposed to the HTTP server.
289pub struct ServerReplicationState {
290    pub config: crate::replication::ReplicationConfig,
291    pub primary: Option<crate::replication::primary::PrimaryReplication>,
292}
293
294#[derive(Clone)]
295pub struct RedDBServer {
296    runtime: RedDBRuntime,
297    options: ServerOptions,
298    auth_store: Option<Arc<AuthStore>>,
299    replication: Option<Arc<ServerReplicationState>>,
300    /// Bounded handler-thread admission for the clear-text HTTP accept
301    /// loop (issue #570 slice 1). Cloned with the server; `Clone` of
302    /// `HttpConnectionLimiter` shares an `Arc` so every serve loop on
303    /// the same `RedDBServer` shares one cap.
304    http_limiter: HttpConnectionLimiter,
305    /// CPU-scaled execution slots for synchronous engine calls. The outer
306    /// `http_limiter` bounds admitted requests; this gate keeps only a CPU-sized
307    /// subset active and parks the remainder on a bounded async FIFO queue.
308    execution_admission: ExecutionAdmission,
309    /// Fail readiness while a handler that exceeded its deadline still owns a
310    /// blocking executor worker. This is independent from normal admission:
311    /// overload alone stays ready, while work that cannot unwind does not.
312    execution_watchdog: ExecutionWatchdog,
313    /// Small, independently bounded admission reserve for Kubernetes health
314    /// probes. A saturated query pool must not make a healthy process fail
315    /// readiness and disappear from its EndpointSlice. Only the four exact
316    /// GET health routes use this limiter; all other work remains subject to
317    /// `http_limiter` and the per-principal cap.
318    health_limiter: HttpConnectionLimiter,
319    /// Per-handler total-time deadline (issue #570 slice 2). Each
320    /// clear-text handler thread arms a deadline at spawn and bails
321    /// with a best-effort 503 at coarse boundaries between request
322    /// parse, route dispatch, and response write. Hard-coded to 30s
323    /// here; the config knob lands in slice 5.
324    handler_timeout: Duration,
325    /// Monotonic clock the per-handler deadline (issue #621) is armed
326    /// against — the same [`MonotonicClock`] abstraction the limiter
327    /// uses. Production wires [`SystemMonotonicClock`] (real wall time);
328    /// tests inject a fake to drive timeout expiry deterministically
329    /// without `sleep()`. Shared via `Arc` so cloned server handles
330    /// (e.g. `serve_in_background`) read the same clock.
331    handler_clock: Arc<dyn MonotonicClock>,
332    /// Test-only synchronous delay injected between route dispatch and
333    /// response write so an integration test can simulate a slow
334    /// downstream tripping the deadline. `u64::MAX` holds the worker until
335    /// the value changes, providing a deterministic gate for timeout races.
336    /// Default 0 (no-op). Shared via `Arc` so a cloned `RedDBServer` (e.g.
337    /// `serve_in_background`) observes flips from the originating handle.
338    /// Production callers have no way to set this — the setter is
339    /// `#[doc(hidden)]`.
340    slow_inject_ms: Arc<AtomicU64>,
341    /// Prometheus metrics for the HTTP handler-thread pool (issue
342    /// #573 slice 4). Records rejections (cap_exhausted /
343    /// handler_timeout) and per-handler duration histograms. Cloned
344    /// with the server via `Arc` so every serve loop on the same
345    /// `RedDBServer` writes to one set of counters.
346    http_metrics: HttpHandlerMetrics,
347    /// Issue #1239 / PRD #1237 — `reddb_http_requests_total` counters by
348    /// `method`, matched route template, and status class. Recorded once
349    /// per handled request in `route()`; read by `/metrics` and the
350    /// operational telemetry read model. Cloned with the server via `Arc`
351    /// so every serve loop on the same `RedDBServer` shares one counter set.
352    http_request_metrics: HttpRequestMetrics,
353    /// `Retry-After` value (seconds) emitted in the async edge's
354    /// capacity-reject 503 path (issue #574 slice 5). Read on the reject
355    /// path in `axum_edge`.
356    retry_after_secs: u64,
357    /// Issue #934 / PRD #930 — per-principal concurrent in-flight cap. The
358    /// global `http_limiter` bounds *total* in-flight work (async
359    /// backpressure); this bounds any *single* principal's share so one
360    /// abusive caller can't drain the whole global cap and starve the rest.
361    /// Consulted at the async edge after global admission; a principal over
362    /// its cap gets a structured 429 refusal. Shared via `Arc` (inside the
363    /// limiter) so cloned server handles enforce one set of counters.
364    principal_limiter: PrincipalConnectionLimiter,
365    /// Issue #761 / S2 — process-wide output-stream capacity registry.
366    /// Shared via `Arc` so cloned server handles (e.g.
367    /// `serve_in_background`) all enforce against one set of counters.
368    /// The HTTP NDJSON path acquires through this in
369    /// `try_route_streaming` before invoking the handler; the guard is
370    /// dropped on return so any stream-end path (success / mid-stream
371    /// error / snapshot expiry / panic unwind) releases the slot.
372    pub(crate) stream_capacity: Arc<output_stream::StreamCapacityRegistry>,
373    /// Issue #766 / S7 — resume coordinator ledger. Tracks
374    /// `(snapshot_lsn → opened_at_ms, ttl_ms)` for resume-eligibility
375    /// checks. Shared via `Arc` so cloned server handles see one
376    /// ledger.
377    pub(crate) lease_registry: Arc<output_stream::LeaseRegistry>,
378    /// Issue #807 / PRD #750 — `/query/stream` cursor registry. Holds the
379    /// opaque token → (snapshot pin, TTL, tenant, principal, query) entries
380    /// that let a client resume or reference a streamed read. Shared via
381    /// `Arc` so cloned server handles see one registry.
382    pub(crate) cursor_registry: Arc<output_stream::CursorRegistry>,
383}
384
385/// Default per-handler total-time budget (issue #571 slice 2).
386const DEFAULT_HANDLER_TIMEOUT: Duration = Duration::from_millis(30_000);
387
388/// Health probes are cheap and infrequent, but the reserve is still bounded
389/// so probe traffic cannot create an unlimited bypass around admission.
390const HEALTH_HANDLER_CAP: usize = 4;
391
392#[derive(Debug, Clone, Copy, PartialEq, Eq)]
393enum ServerlessWarmupScope {
394    Indexes,
395    GraphProjections,
396    AnalyticsJobs,
397    NativeArtifacts,
398}
399
400#[derive(Debug, Clone, Copy, PartialEq, Eq)]
401enum DeploymentProfile {
402    Embedded,
403    Server,
404    Serverless,
405}
406
407fn percent_decode_path_segment(input: &str) -> Result<String, String> {
408    let bytes = input.as_bytes();
409    let mut out = Vec::with_capacity(bytes.len());
410    let mut index = 0;
411    while index < bytes.len() {
412        match bytes[index] {
413            b'%' => {
414                if index + 2 >= bytes.len() {
415                    return Err("truncated percent escape".to_string());
416                }
417                let high = hex_value(bytes[index + 1])
418                    .ok_or_else(|| "invalid percent escape".to_string())?;
419                let low = hex_value(bytes[index + 2])
420                    .ok_or_else(|| "invalid percent escape".to_string())?;
421                out.push((high << 4) | low);
422                index += 3;
423            }
424            byte => {
425                out.push(byte);
426                index += 1;
427            }
428        }
429    }
430    String::from_utf8(out).map_err(|_| "path segment is not valid UTF-8".to_string())
431}
432
433fn hex_value(byte: u8) -> Option<u8> {
434    match byte {
435        b'0'..=b'9' => Some(byte - b'0'),
436        b'a'..=b'f' => Some(byte - b'a' + 10),
437        b'A'..=b'F' => Some(byte - b'A' + 10),
438        _ => None,
439    }
440}
441
442#[derive(Debug, Clone)]
443struct ParsedQueryRequest {
444    query: String,
445    entity_types: Option<Vec<String>>,
446    capabilities: Option<Vec<String>>,
447    commit_policy: Option<crate::replication::CommitPolicy>,
448    /// Optional positional `$N` bind parameters (#358), decoded into the
449    /// Request module's transport-neutral vocabulary.
450    params: Option<Vec<crate::runtime::query_request::ParamValue>>,
451}
452
453#[derive(Debug, Clone, Copy)]
454enum PatchOperationType {
455    Set,
456    Replace,
457    Unset,
458}
459
460#[derive(Debug, Clone)]
461struct PatchOperation {
462    op: PatchOperationType,
463    path: Vec<String>,
464    value: Option<JsonValue>,
465}
466
467impl RedDBServer {
468    pub fn new(runtime: RedDBRuntime) -> Self {
469        Self::with_options(runtime, ServerOptions::default())
470    }
471
472    pub fn from_database_options(
473        db_options: RedDBOptions,
474        server_options: ServerOptions,
475    ) -> RedDBResult<Self> {
476        let runtime = RedDBRuntime::with_options(db_options)?;
477        Ok(Self::with_options(runtime, server_options))
478    }
479
480    pub fn with_options(runtime: RedDBRuntime, options: ServerOptions) -> Self {
481        let http_limits = HttpLimitsResolved::builtin_defaults();
482        let execution_admission = runtime.configure_execution_admission(
483            http_limits.execution_workers,
484            http_limits
485                .max_handlers
486                .saturating_sub(http_limits.execution_workers),
487            http_limits.handler_timeout_ms,
488        );
489        Self {
490            runtime,
491            options,
492            auth_store: None,
493            replication: None,
494            http_limiter: HttpConnectionLimiter::new(http_limits.max_handlers),
495            execution_admission,
496            execution_watchdog: ExecutionWatchdog::default(),
497            health_limiter: HttpConnectionLimiter::new(HEALTH_HANDLER_CAP),
498            handler_timeout: DEFAULT_HANDLER_TIMEOUT,
499            handler_clock: Arc::new(SystemMonotonicClock::new()),
500            slow_inject_ms: Arc::new(AtomicU64::new(0)),
501            http_metrics: HttpHandlerMetrics::new(),
502            http_request_metrics: HttpRequestMetrics::new(),
503            retry_after_secs: DEFAULT_RETRY_AFTER_SECS,
504            principal_limiter: PrincipalConnectionLimiter::new(
505                http_limits.max_inflight_per_principal,
506            ),
507            stream_capacity: output_stream::StreamCapacityRegistry::new(),
508            lease_registry: output_stream::LeaseRegistry::new(),
509            cursor_registry: output_stream::CursorRegistry::new(),
510        }
511    }
512
513    #[doc(hidden)]
514    pub fn stream_capacity(&self) -> &Arc<output_stream::StreamCapacityRegistry> {
515        &self.stream_capacity
516    }
517
518    #[doc(hidden)]
519    pub fn lease_registry(&self) -> &Arc<output_stream::LeaseRegistry> {
520        &self.lease_registry
521    }
522
523    #[doc(hidden)]
524    pub fn cursor_registry(&self) -> &Arc<output_stream::CursorRegistry> {
525        &self.cursor_registry
526    }
527
528    #[doc(hidden)]
529    pub fn http_metrics(&self) -> &HttpHandlerMetrics {
530        &self.http_metrics
531    }
532
533    /// HTTP request/error volume counters (`reddb_http_requests_total`).
534    /// Read by `/metrics` and the operational telemetry read model.
535    #[doc(hidden)]
536    pub fn http_request_metrics(&self) -> &HttpRequestMetrics {
537        &self.http_request_metrics
538    }
539
540    /// Visible for tests. Lets the integration test in
541    /// `tests/http_connection_limiter.rs` saturate the cap and observe
542    /// `503 Service Unavailable` responses without spinning up
543    /// thousands of sockets.
544    #[doc(hidden)]
545    pub fn with_http_limiter_cap(mut self, cap: usize) -> Self {
546        self.http_limiter = HttpConnectionLimiter::new(cap);
547        let execution_workers = self.execution_admission.active_max().min(cap);
548        self.execution_admission = self.runtime.configure_execution_admission(
549            execution_workers,
550            cap.saturating_sub(execution_workers),
551            self.handler_timeout.as_millis().min(u64::MAX as u128) as u64,
552        );
553        self
554    }
555
556    /// Stamp resolved HTTP limits onto the server (issue #574 slice 5).
557    /// Replaces the limiter cap, the per-handler deadline, and the
558    /// `Retry-After` value used by the limiter's reject path. All
559    /// values are assumed validated by [`http_limits::resolve_http_limits`].
560    pub fn with_http_limits(mut self, limits: HttpLimitsResolved) -> Self {
561        self.http_limiter = HttpConnectionLimiter::new(limits.max_handlers);
562        self.execution_admission = self.runtime.configure_execution_admission(
563            limits.execution_workers,
564            limits.max_handlers.saturating_sub(limits.execution_workers),
565            limits.handler_timeout_ms,
566        );
567        self.handler_timeout = Duration::from_millis(limits.handler_timeout_ms);
568        self.retry_after_secs = limits.retry_after_secs;
569        self.principal_limiter = PrincipalConnectionLimiter::new(limits.max_inflight_per_principal);
570        self
571    }
572
573    /// Visible for tests. Override the per-principal concurrent in-flight
574    /// cap (issue #934) so the integration test can saturate one principal
575    /// and observe the structured 429 refusal without standing up hundreds
576    /// of real sockets. `0` disables the per-principal cap.
577    #[doc(hidden)]
578    pub fn with_principal_inflight_cap(mut self, cap: usize) -> Self {
579        self.principal_limiter = PrincipalConnectionLimiter::new(cap);
580        self
581    }
582
583    #[doc(hidden)]
584    pub fn principal_limiter(&self) -> &PrincipalConnectionLimiter {
585        &self.principal_limiter
586    }
587
588    #[doc(hidden)]
589    pub fn retry_after_secs(&self) -> u64 {
590        self.retry_after_secs
591    }
592
593    #[doc(hidden)]
594    pub fn http_limiter(&self) -> &HttpConnectionLimiter {
595        &self.http_limiter
596    }
597
598    pub(crate) fn execution_admission(&self) -> &ExecutionAdmission {
599        &self.execution_admission
600    }
601
602    #[doc(hidden)]
603    pub fn http_execution_active(&self) -> usize {
604        self.execution_admission.active()
605    }
606
607    #[doc(hidden)]
608    pub fn http_execution_queued(&self) -> usize {
609        self.execution_admission.queued()
610    }
611
612    #[doc(hidden)]
613    pub fn http_execution_workers(&self) -> usize {
614        self.execution_admission.active_max()
615    }
616
617    /// Test-only override for deterministic single-worker queue coverage.
618    #[doc(hidden)]
619    pub fn with_http_execution_workers(mut self, workers: usize) -> Self {
620        assert!(workers > 0, "HTTP execution workers must be positive");
621        let workers = workers.min(self.http_limiter.cap());
622        self.execution_admission = self.runtime.configure_execution_admission(
623            workers,
624            self.http_limiter.cap().saturating_sub(workers),
625            self.handler_timeout.as_millis().min(u64::MAX as u128) as u64,
626        );
627        self
628    }
629
630    /// Visible for tests that prove health and query admission are isolated.
631    #[doc(hidden)]
632    pub fn health_limiter(&self) -> &HttpConnectionLimiter {
633        &self.health_limiter
634    }
635
636    /// Visible for tests. Production always uses [`HEALTH_HANDLER_CAP`].
637    #[doc(hidden)]
638    pub fn with_health_limiter_cap(mut self, cap: usize) -> Self {
639        self.health_limiter = HttpConnectionLimiter::new(cap);
640        self
641    }
642
643    /// Visible for tests. Override the per-handler total-time deadline
644    /// (issue #570 slice 2). Default 30s.
645    #[doc(hidden)]
646    pub fn with_handler_timeout(mut self, timeout: Duration) -> Self {
647        self.handler_timeout = timeout;
648        self
649    }
650
651    #[doc(hidden)]
652    pub fn handler_timeout(&self) -> Duration {
653        self.handler_timeout
654    }
655
656    /// Visible for tests. Override the clock the per-handler deadline
657    /// (issue #621) is armed against, so timeout expiry can be driven
658    /// deterministically without real sleeps. Default is the real
659    /// monotonic clock.
660    #[doc(hidden)]
661    pub fn with_handler_clock(mut self, clock: Arc<dyn MonotonicClock>) -> Self {
662        self.handler_clock = clock;
663        self
664    }
665
666    /// Test hook: set a synchronous delay (in ms) inserted between route
667    /// dispatch and response write. `u64::MAX` blocks until this is changed,
668    /// allowing deterministic timeout/recovery assertions. Shared via
669    /// `Arc<AtomicU64>` so cloned server handles see the same flip.
670    #[doc(hidden)]
671    pub fn set_test_slow_inject_ms(&self, ms: u64) {
672        self.slow_inject_ms.store(ms, Ordering::Release);
673    }
674
675    fn apply_test_slow_inject(&self) {
676        let inject_ms = self.slow_inject_ms.load(Ordering::Acquire);
677        if inject_ms == 0 {
678            return;
679        }
680        if inject_ms != u64::MAX {
681            thread::sleep(Duration::from_millis(inject_ms));
682            return;
683        }
684        while self.slow_inject_ms.load(Ordering::Acquire) == u64::MAX {
685            thread::park_timeout(Duration::from_millis(1));
686        }
687    }
688
689    fn test_slow_inject_is_blocking_gate(&self) -> bool {
690        self.slow_inject_ms.load(Ordering::Acquire) == u64::MAX
691    }
692
693    /// Attach an `AuthStore` for HTTP-layer authentication.
694    /// Also injects the store into the runtime so that `Value::Secret`
695    /// auto-encrypt/decrypt can reach the vault AES key.
696    pub fn with_auth(mut self, auth_store: Arc<AuthStore>) -> Self {
697        self.runtime.set_auth_store(Arc::clone(&auth_store));
698        self.auth_store = Some(auth_store);
699        self
700    }
701
702    /// Attach replication state for status and snapshot endpoints.
703    pub fn with_replication(mut self, state: Arc<ServerReplicationState>) -> Self {
704        self.replication = Some(state);
705        self
706    }
707
708    /// Set the `Origin` allowlist that enables the RedWire-over-WSS
709    /// browser endpoint (issue #935, ADR 0036). A non-empty list mounts
710    /// the `/redwire` upgrade route on the TLS edge; an empty list leaves
711    /// it unmounted (default-deny).
712    pub fn with_websocket_allowed_origins(mut self, origins: Vec<String>) -> Self {
713        self.options.websocket_allowed_origins = origins;
714        self
715    }
716
717    /// The configured RedWire-over-WSS `Origin` allowlist (issue #935).
718    pub(crate) fn websocket_allowed_origins(&self) -> &[String] {
719        &self.options.websocket_allowed_origins
720    }
721
722    /// Serve the resolved red-ui bundle from `dir` as inert static assets
723    /// on this listener (`red server --ui`, issue #1047, ADR 0051). The
724    /// bundle is served as a fallback after the API routing table, so it
725    /// can never shadow a real endpoint; `GET /` resolves to the bundle's
726    /// `index.html`.
727    pub fn with_ui_dir(mut self, dir: std::path::PathBuf) -> Self {
728        self.options.ui_dir = Some(dir);
729        self
730    }
731
732    /// The directory this listener serves the red-ui bundle from, if
733    /// `--ui` is active (issue #1047).
734    pub(crate) fn ui_dir(&self) -> Option<&std::path::Path> {
735        self.options.ui_dir.as_deref()
736    }
737
738    /// Enable the browser credential layer (issue #936, PRD #930) by
739    /// wiring a hybrid-token authority into the runtime. Once set, the
740    /// `/auth/browser/*` HTTP endpoints issue/rotate the access+refresh
741    /// pair and the RedWire WS handshake accepts the access JWT. Left
742    /// unset, the browser flow is inert (default-deny), matching the WS
743    /// endpoint's own opt-in posture.
744    pub fn with_browser_token_authority(
745        self,
746        authority: Arc<crate::auth::browser_token::BrowserTokenAuthority>,
747    ) -> Self {
748        self.runtime.set_browser_token_authority(Some(authority));
749        self
750    }
751
752    pub fn runtime(&self) -> &RedDBRuntime {
753        &self.runtime
754    }
755
756    pub fn options(&self) -> &ServerOptions {
757        &self.options
758    }
759
760    fn admin_use_cases(&self) -> AdminUseCases<'_, RedDBRuntime> {
761        AdminUseCases::new(&self.runtime)
762    }
763
764    fn entity_use_cases(&self) -> EntityUseCases<'_, RedDBRuntime> {
765        EntityUseCases::new(&self.runtime)
766    }
767
768    fn graph_use_cases(&self) -> GraphUseCases<'_, RedDBRuntime> {
769        GraphUseCases::new(&self.runtime)
770    }
771
772    fn native_use_cases(&self) -> NativeUseCases<'_, RedDBRuntime> {
773        NativeUseCases::new(&self.runtime)
774    }
775
776    fn tree_use_cases(&self) -> TreeUseCases<'_, RedDBRuntime> {
777        TreeUseCases::new(&self.runtime)
778    }
779
780    fn transport_readiness_json(&self) -> JsonValue {
781        let active = self
782            .options
783            .transport_readiness
784            .active
785            .iter()
786            .map(|listener| {
787                let mut object = Map::new();
788                object.insert(
789                    "transport".to_string(),
790                    JsonValue::String(listener.transport.clone()),
791                );
792                object.insert(
793                    "bind_addr".to_string(),
794                    JsonValue::String(listener.bind_addr.clone()),
795                );
796                object.insert("explicit".to_string(), JsonValue::Bool(listener.explicit));
797                JsonValue::Object(object)
798            })
799            .collect();
800        let failed = self
801            .options
802            .transport_readiness
803            .failed
804            .iter()
805            .map(|listener| {
806                let mut object = Map::new();
807                object.insert(
808                    "transport".to_string(),
809                    JsonValue::String(listener.transport.clone()),
810                );
811                object.insert(
812                    "bind_addr".to_string(),
813                    JsonValue::String(listener.bind_addr.clone()),
814                );
815                object.insert("explicit".to_string(), JsonValue::Bool(listener.explicit));
816                object.insert(
817                    "reason".to_string(),
818                    JsonValue::String(listener.reason.clone()),
819                );
820                JsonValue::Object(object)
821            })
822            .collect();
823
824        let mut object = Map::new();
825        object.insert("active".to_string(), JsonValue::Array(active));
826        object.insert("failed".to_string(), JsonValue::Array(failed));
827        JsonValue::Object(object)
828    }
829
830    fn handle_grpc_discovery(&self) -> HttpResponse {
831        let mut methods = Map::new();
832        methods.insert(
833            "query".to_string(),
834            JsonValue::String("reddb.v1.RedDB/Query".to_string()),
835        );
836        methods.insert(
837            "batch_query".to_string(),
838            JsonValue::String("reddb.v1.RedDB/BatchQuery".to_string()),
839        );
840        methods.insert(
841            "health".to_string(),
842            JsonValue::String("reddb.v1.RedDB/Health".to_string()),
843        );
844        methods.insert(
845            "prepare".to_string(),
846            JsonValue::String("reddb.v1.RedDB/Prepare".to_string()),
847        );
848        methods.insert(
849            "execute_prepared".to_string(),
850            JsonValue::String("reddb.v1.RedDB/ExecutePrepared".to_string()),
851        );
852
853        let mut examples = Map::new();
854        examples.insert(
855            "query".to_string(),
856            JsonValue::String(
857                "grpcurl -plaintext -d '{\"query\":\"SELECT 1\"}' 127.0.0.1:5000 reddb.v1.RedDB/Query"
858                    .to_string(),
859            ),
860        );
861        examples.insert(
862            "query_with_params".to_string(),
863            JsonValue::String(
864                "grpcurl -plaintext -d '{\"query\":\"SELECT $1 AS value\",\"params\":[{\"intValue\":42}]}' 127.0.0.1:5000 reddb.v1.RedDB/Query"
865                    .to_string(),
866            ),
867        );
868        examples.insert(
869            "health".to_string(),
870            JsonValue::String(
871                "grpcurl -plaintext -d '{}' 127.0.0.1:5000 reddb.v1.RedDB/Health".to_string(),
872            ),
873        );
874
875        let mut object = Map::new();
876        object.insert("ok".to_string(), JsonValue::Bool(true));
877        object.insert(
878            "service".to_string(),
879            JsonValue::String("reddb.v1.RedDB".to_string()),
880        );
881        object.insert(
882            "package".to_string(),
883            JsonValue::String("reddb.v1".to_string()),
884        );
885        object.insert(
886            "proto".to_string(),
887            JsonValue::String("crates/reddb-grpc-proto/proto/reddb.proto".to_string()),
888        );
889        object.insert("methods".to_string(), JsonValue::Object(methods));
890        object.insert("examples".to_string(), JsonValue::Object(examples));
891        object.insert(
892            "transport_listeners".to_string(),
893            self.transport_readiness_json(),
894        );
895        object.insert(
896            "hint".to_string(),
897            JsonValue::String(
898                "If grpcurl cannot list services, pass the proto file with -import-path crates/reddb-grpc-proto/proto -proto reddb.proto."
899                    .to_string(),
900            ),
901        );
902        json_response(200, JsonValue::Object(object))
903    }
904
905    fn handle_query_contract(&self) -> HttpResponse {
906        let mut examples = Map::new();
907        examples.insert(
908            "raw_sql".to_string(),
909            JsonValue::String("curl -sS http://127.0.0.1:5000/query -d 'SELECT 1'".to_string()),
910        );
911        examples.insert(
912            "json_query".to_string(),
913            JsonValue::String(
914                "curl -sS http://127.0.0.1:5000/query -H 'content-type: application/json' -d '{\"query\":\"SELECT 1\"}'"
915                    .to_string(),
916            ),
917        );
918        examples.insert(
919            "json_query_with_params".to_string(),
920            JsonValue::String(
921                "curl -sS http://127.0.0.1:5000/query -H 'content-type: application/json' -d '{\"query\":\"SELECT $1 AS value\",\"params\":[42]}'"
922                    .to_string(),
923            ),
924        );
925
926        let mut request_body = Map::new();
927        request_body.insert(
928            "query".to_string(),
929            JsonValue::String("required string".to_string()),
930        );
931        request_body.insert(
932            "params".to_string(),
933            JsonValue::String("optional array".to_string()),
934        );
935
936        let mut response_shape = Map::new();
937        response_shape.insert(
938            "columns".to_string(),
939            JsonValue::String("projected column names".to_string()),
940        );
941        response_shape.insert(
942            "records[].values".to_string(),
943            JsonValue::String("only projected values".to_string()),
944        );
945        response_shape.insert(
946            "records[].meta".to_string(),
947            JsonValue::String("internal metadata when present".to_string()),
948        );
949
950        let mut object = Map::new();
951        object.insert("ok".to_string(), JsonValue::Bool(false));
952        object.insert(
953            "code".to_string(),
954            JsonValue::String("method_not_allowed".to_string()),
955        );
956        object.insert(
957            "message".to_string(),
958            JsonValue::String("/query accepts POST requests".to_string()),
959        );
960        object.insert(
961            "hint".to_string(),
962            JsonValue::String(
963                "Send raw SQL in the body, or JSON with a string 'query' field.".to_string(),
964            ),
965        );
966        object.insert("method".to_string(), JsonValue::String("POST".to_string()));
967        object.insert("path".to_string(), JsonValue::String("/query".to_string()));
968        object.insert("request_body".to_string(), JsonValue::Object(request_body));
969        object.insert(
970            "response_shape".to_string(),
971            JsonValue::Object(response_shape),
972        );
973        object.insert("examples".to_string(), JsonValue::Object(examples));
974        object.insert(
975            "docs".to_string(),
976            JsonValue::String("https://reddb.io/docs/query".to_string()),
977        );
978
979        json_response(405, JsonValue::Object(object))
980            .with_header("Allow", http::HeaderValue::from_static("POST"))
981    }
982
983    fn handle_root_discovery(&self) -> HttpResponse {
984        let mut endpoints = Map::new();
985        endpoints.insert(
986            "health".to_string(),
987            JsonValue::String("GET /health".to_string()),
988        );
989        endpoints.insert(
990            "ready".to_string(),
991            JsonValue::String("GET /ready".to_string()),
992        );
993        endpoints.insert(
994            "query".to_string(),
995            JsonValue::String("POST /query".to_string()),
996        );
997        endpoints.insert(
998            "query_readiness".to_string(),
999            JsonValue::String("GET /ready/query".to_string()),
1000        );
1001        endpoints.insert(
1002            "catalog".to_string(),
1003            JsonValue::String("GET /catalog".to_string()),
1004        );
1005        endpoints.insert(
1006            "deployment_profiles".to_string(),
1007            JsonValue::String("GET /deployment/profiles".to_string()),
1008        );
1009
1010        let mut examples = Map::new();
1011        examples.insert(
1012            "http_raw_sql".to_string(),
1013            JsonValue::String("curl -sS http://127.0.0.1:5000/query -d 'SELECT 1'".to_string()),
1014        );
1015        examples.insert(
1016            "http_json_query".to_string(),
1017            JsonValue::String(
1018                "curl -sS http://127.0.0.1:5000/query -H 'content-type: application/json' -d '{\"query\":\"SELECT 1\"}'"
1019                    .to_string(),
1020            ),
1021        );
1022        examples.insert(
1023            "http_json_query_with_params".to_string(),
1024            JsonValue::String(
1025                "curl -sS http://127.0.0.1:5000/query -H 'content-type: application/json' -d '{\"query\":\"SELECT $1 AS value\",\"params\":[42]}'"
1026                    .to_string(),
1027            ),
1028        );
1029
1030        let mut object = Map::new();
1031        object.insert("ok".to_string(), JsonValue::Bool(true));
1032        object.insert(
1033            "service".to_string(),
1034            JsonValue::String("reddb".to_string()),
1035        );
1036        object.insert(
1037            "version".to_string(),
1038            JsonValue::String(env!("CARGO_PKG_VERSION").to_string()),
1039        );
1040        object.insert("endpoints".to_string(), JsonValue::Object(endpoints));
1041        object.insert("examples".to_string(), JsonValue::Object(examples));
1042        object.insert(
1043            "docs".to_string(),
1044            JsonValue::String("https://reddb.io/docs".to_string()),
1045        );
1046        object.insert(
1047            "transport_listeners".to_string(),
1048            self.transport_readiness_json(),
1049        );
1050        json_response(200, JsonValue::Object(object))
1051    }
1052
1053    fn health_json_with_transport(&self, report: &HealthReport) -> JsonValue {
1054        let mut value = crate::presentation::ops_json::health_json(report);
1055        if let JsonValue::Object(ref mut object) = value {
1056            object.insert(
1057                "transport_listeners".to_string(),
1058                self.transport_readiness_json(),
1059            );
1060        }
1061        value
1062    }
1063
1064    pub fn serve(&self) -> io::Result<()> {
1065        let listener = TcpListener::bind(&self.options.bind_addr)?;
1066        self.serve_on(listener)
1067    }
1068
1069    /// Serve the async axum/hyper HTTP edge (issue #931) on the given
1070    /// listener until it errors fatally. A dedicated multi-threaded tokio
1071    /// runtime drives the I/O; the synchronous disk-backed engine is
1072    /// reached via `spawn_blocking`. This replaces the retired
1073    /// thread-per-connection accept loop and its `(2*num_cpus)` thread
1074    /// cap — idle keep-alive connections are now cheap parked tasks.
1075    pub fn serve_on(&self, listener: TcpListener) -> io::Result<()> {
1076        let runtime = axum_edge::build_edge_runtime()?;
1077        runtime.block_on(
1078            self.clone()
1079                .serve_edge_on_std(listener, HttpTransport::Http),
1080        )
1081    }
1082
1083    /// Accept and serve a single connection to completion, then return.
1084    /// Used by tests that want a one-shot HTTP server alongside another
1085    /// transport.
1086    pub fn serve_one_on(&self, listener: TcpListener) -> io::Result<()> {
1087        let runtime = axum_edge::build_background_edge_runtime()?;
1088        let server = self.clone();
1089        runtime.block_on(async move {
1090            listener.set_nonblocking(true)?;
1091            let listener = tokio::net::TcpListener::from_std(listener)?;
1092            let (stream, _peer) = listener.accept().await?;
1093            server.serve_edge_one(stream).await;
1094            Ok(())
1095        })
1096    }
1097
1098    pub fn serve_in_background(&self) -> thread::JoinHandle<io::Result<()>> {
1099        let server = self.clone();
1100        thread::spawn(move || server.serve())
1101    }
1102
1103    pub fn serve_in_background_on(
1104        &self,
1105        listener: TcpListener,
1106    ) -> thread::JoinHandle<io::Result<()>> {
1107        let server = self.clone();
1108        thread::spawn(move || {
1109            let runtime = axum_edge::build_background_edge_runtime()?;
1110            runtime.block_on(server.serve_edge_on_std(listener, HttpTransport::Http))
1111        })
1112    }
1113
1114    /// Serve TLS-wrapped HTTPS on the configured `bind_addr`. The
1115    /// `tls_config` is shared across all connections (rustls
1116    /// `ServerConfig` is `Send + Sync`).
1117    pub fn serve_tls(&self, tls_config: std::sync::Arc<rustls::ServerConfig>) -> io::Result<()> {
1118        let listener = TcpListener::bind(&self.options.bind_addr)?;
1119        self.serve_tls_on(listener, tls_config)
1120    }
1121
1122    pub fn serve_tls_on(
1123        &self,
1124        listener: TcpListener,
1125        tls_config: std::sync::Arc<rustls::ServerConfig>,
1126    ) -> io::Result<()> {
1127        let runtime = axum_edge::build_edge_runtime()?;
1128        let acceptor = axum_edge::tls_acceptor(tls_config);
1129        runtime.block_on(self.clone().serve_edge_tls_on_std(
1130            listener,
1131            acceptor,
1132            HttpTransport::Https,
1133        ))
1134    }
1135
1136    pub fn serve_tls_in_background(
1137        &self,
1138        tls_config: std::sync::Arc<rustls::ServerConfig>,
1139    ) -> thread::JoinHandle<io::Result<()>> {
1140        let server = self.clone();
1141        thread::spawn(move || server.serve_tls(tls_config))
1142    }
1143
1144    pub fn serve_tls_in_background_on(
1145        &self,
1146        listener: TcpListener,
1147        tls_config: std::sync::Arc<rustls::ServerConfig>,
1148    ) -> thread::JoinHandle<io::Result<()>> {
1149        let server = self.clone();
1150        thread::spawn(move || {
1151            let runtime = axum_edge::build_background_edge_runtime()?;
1152            let acceptor = axum_edge::tls_acceptor(tls_config);
1153            runtime.block_on(server.serve_edge_tls_on_std(listener, acceptor, HttpTransport::Https))
1154        })
1155    }
1156
1157    fn handle_connection(&self, stream: TcpStream) -> io::Result<()> {
1158        let started = Instant::now();
1159        let result = self.handle_connection_inner(stream);
1160        let elapsed = started.elapsed().as_secs_f64();
1161        self.http_metrics
1162            .record_duration(HttpTransport::Http, elapsed);
1163        result
1164    }
1165
1166    fn handle_connection_inner(&self, mut stream: TcpStream) -> io::Result<()> {
1167        stream.set_read_timeout(Some(Duration::from_millis(self.options.read_timeout_ms)))?;
1168        stream.set_write_timeout(Some(Duration::from_millis(self.options.write_timeout_ms)))?;
1169
1170        // Issue #570 slice 2 / #621: arm a deadline at handler spawn and
1171        // check at coarse boundaries. Armed against the injectable
1172        // monotonic clock (#621) so timeout behaviour is deterministically
1173        // testable without real sleeps; production wires the real clock,
1174        // so this tracks wall time. No hard pre-emption — a thread blocked
1175        // inside a true syscall is still bounded only by the per-socket
1176        // read/write timeouts.
1177        let deadline = HandlerDeadline::arm(Arc::clone(&self.handler_clock), self.handler_timeout);
1178
1179        let request = HttpRequest::read_from(&mut stream, self.options.max_body_bytes)?;
1180
1181        // Boundary (a): between request parse and route dispatch.
1182        if deadline.expired() {
1183            self.http_metrics
1184                .record_reject(HttpTransport::Http, HttpRejectReason::HandlerTimeout);
1185            Self::write_handler_timeout_503(&mut stream);
1186            return Ok(());
1187        }
1188
1189        if self.try_route_streaming(&request, &mut stream)? {
1190            return Ok(());
1191        }
1192        let response = self.route(request);
1193
1194        // Test-only injected slow downstream (issue #570 slice 2
1195        // integration test). Production builds set this to 0, so this
1196        // is a single relaxed atomic load on the hot path.
1197        self.apply_test_slow_inject();
1198
1199        // Boundary (b): between route dispatch and response write.
1200        if deadline.expired() {
1201            self.http_metrics
1202                .record_reject(HttpTransport::Http, HttpRejectReason::HandlerTimeout);
1203            Self::write_handler_timeout_503(&mut stream);
1204            return Ok(());
1205        }
1206
1207        response.write_to(&mut stream)?;
1208        stream.flush()?;
1209        Ok(())
1210    }
1211
1212    /// Best-effort 503 emitted when the per-handler deadline expires
1213    /// at a coarse boundary. Writes are swallowed — the caller has
1214    /// already exceeded its budget, so we do not propagate write
1215    /// errors. Permit drop happens on the handler thread's normal
1216    /// exit path.
1217    fn write_handler_timeout_503<S: Write>(stream: &mut S) {
1218        const RESPONSE: &[u8] = b"HTTP/1.1 503 Service Unavailable\r\n\
1219            Connection: close\r\n\
1220            Content-Length: 0\r\n\
1221            \r\n";
1222        let _ = stream.write_all(RESPONSE);
1223        let _ = stream.flush();
1224    }
1225}