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