1mod bind;
2mod hooks;
3
4pub(crate) use hooks::{AppInner, ListenParts, ShutdownHook, StartupHook};
5pub use bind::{Bind, BoundApp, Http};
6pub use hooks::Server;
7
8use crate::error::{Error, Result};
9use crate::events::EventBus;
10use crate::handler::BoxFuture;
11use crate::plugin::{
12 check_plugin_sdk, InstalledPlugin, Plugin, SdkCompat, PLUGIN_SDK_VERSION,
13};
14use crate::request::Request;
15use crate::response::Response;
16use crate::router::Router;
17use crate::service::{BackgroundService, BoxedService};
18use crate::state::StateMap;
19use bytes::Bytes;
20use http::Method;
21use std::collections::{HashMap, HashSet};
22use std::fs;
23use std::net::{IpAddr, SocketAddr};
24use std::ops::{Deref, DerefMut};
25use std::sync::Arc;
26use std::time::Duration;
27
28pub(crate) type CliCommandFn =
29 Arc<dyn Fn(Arc<StateMap>, Vec<String>) -> BoxFuture<Result<()>> + Send + Sync>;
30
31pub(crate) type CheckFn =
32 Arc<dyn Fn(Arc<StateMap>) -> BoxFuture<Result<()>> + Send + Sync>;
33
34#[derive(Debug, Clone, Copy, PartialEq, Eq)]
36pub enum CheckKind {
37 Ready,
39 Audit,
41}
42
43#[derive(Debug, Clone)]
45pub struct CheckResult {
46 pub name: &'static str,
47 pub ok: bool,
48 pub error: Option<String>,
49}
50
51type CheckEntry = (&'static str, CheckKind, CheckFn);
52type CheckList = Arc<std::sync::Mutex<Vec<CheckEntry>>>;
53
54const DEFAULT_MAX_BODY: usize = 2 * 1024 * 1024;
55const DEFAULT_MAX_CONNECTIONS: usize = 1024;
56const DEFAULT_MAX_UPGRADED: usize = 1024;
57const DEFAULT_MAX_CONCURRENT_STREAMS: usize = 200;
58const DEFAULT_MAX_HEADERS: usize = 100;
59const DEFAULT_DRAIN_TIMEOUT: Duration = Duration::from_secs(20);
61const DEFAULT_HEADER_READ_TIMEOUT: Duration = Duration::from_secs(10);
62const DEFAULT_IDLE_TIMEOUT: Duration = Duration::from_secs(60);
64
65pub struct App {
67 pub(crate) router: Router,
68 pub(crate) max_body_size: usize,
69 pub(crate) max_connections: usize,
70 pub(crate) max_upgraded_connections: usize,
71 pub(crate) max_concurrent_streams: usize,
72 pub(crate) max_headers: usize,
73 pub(crate) max_buf_size: Option<usize>,
74 pub(crate) request_timeout: Option<Duration>,
75 pub(crate) header_read_timeout: Duration,
76 pub(crate) idle_timeout: Duration,
77 pub(crate) drain_timeout: Duration,
78 pub(crate) keep_alive: bool,
79 pub(crate) trust_proxy: bool,
80 pub(crate) reuseport: bool,
81 pub(crate) cli_mode: bool,
83 pub(crate) service_in_cli: bool,
84 pub(crate) hsts: bool,
85 pub(crate) alt_svc: Option<String>,
86 #[cfg(feature = "tls")]
88 pub(crate) tls: Option<crate::Tls>,
89 pub(crate) installed_plugins: HashSet<&'static str>,
90 pub(crate) installed_plugin_meta: Vec<InstalledPlugin>,
91 pub(crate) missing_plugin_requires: Vec<(&'static str, &'static str)>,
92 pub(crate) duplicate_plugin_ids: Vec<&'static str>,
93 pub(crate) plugin_sdk_errors: Vec<String>,
94 pub(crate) on_startup: Vec<StartupHook>,
95 pub(crate) on_shutdown: Vec<ShutdownHook>,
96 pub(crate) services: Vec<BoxedService>,
97 pub(crate) cli_commands: HashMap<&'static str, CliCommandFn>,
98 pub(crate) checks: CheckList,
99 pub(crate) probes: bool,
100}
101
102impl App {
103 pub fn new() -> Self {
104 Self {
105 router: Router::new(),
106 max_body_size: DEFAULT_MAX_BODY,
107 max_connections: DEFAULT_MAX_CONNECTIONS,
108 max_upgraded_connections: DEFAULT_MAX_UPGRADED,
109 max_concurrent_streams: DEFAULT_MAX_CONCURRENT_STREAMS,
110 max_headers: DEFAULT_MAX_HEADERS,
111 max_buf_size: None,
112 request_timeout: Some(Duration::from_secs(30)),
113 header_read_timeout: DEFAULT_HEADER_READ_TIMEOUT,
114 idle_timeout: DEFAULT_IDLE_TIMEOUT,
115 drain_timeout: DEFAULT_DRAIN_TIMEOUT,
116 keep_alive: true,
117 trust_proxy: false,
118 reuseport: false,
119 cli_mode: false,
120 service_in_cli: false,
121 hsts: false,
122 alt_svc: None,
123 #[cfg(feature = "tls")]
124 tls: None,
125 installed_plugins: HashSet::new(),
126 installed_plugin_meta: Vec::new(),
127 missing_plugin_requires: Vec::new(),
128 duplicate_plugin_ids: Vec::new(),
129 plugin_sdk_errors: Vec::new(),
130 on_startup: Vec::new(),
131 on_shutdown: Vec::new(),
132 services: Vec::new(),
133 cli_commands: HashMap::new(),
134 checks: Arc::new(std::sync::Mutex::new(Vec::new())),
135 probes: false,
136 }
137 }
138
139 pub fn register_cli<F, Fut>(&mut self, name: &'static str, f: F) -> &mut Self
141 where
142 F: Fn(Arc<StateMap>, Vec<String>) -> Fut + Send + Sync + 'static,
143 Fut: std::future::Future<Output = Result<()>> + Send + 'static,
144 {
145 self.cli_commands
146 .insert(name, Arc::new(move |state, args| Box::pin(f(state, args))));
147 self
148 }
149
150 pub fn register_check<F, Fut>(&mut self, name: &'static str, f: F) -> &mut Self
152 where
153 F: Fn(Arc<StateMap>) -> Fut + Send + Sync + 'static,
154 Fut: std::future::Future<Output = Result<()>> + Send + 'static,
155 {
156 self.push_check(name, CheckKind::Ready, f)
157 }
158
159 pub fn register_audit<F, Fut>(&mut self, name: &'static str, f: F) -> &mut Self
161 where
162 F: Fn(Arc<StateMap>) -> Fut + Send + Sync + 'static,
163 Fut: std::future::Future<Output = Result<()>> + Send + 'static,
164 {
165 self.push_check(name, CheckKind::Audit, f)
166 }
167
168 fn push_check<F, Fut>(&mut self, name: &'static str, kind: CheckKind, f: F) -> &mut Self
169 where
170 F: Fn(Arc<StateMap>) -> Fut + Send + Sync + 'static,
171 Fut: std::future::Future<Output = Result<()>> + Send + 'static,
172 {
173 self.checks
174 .lock()
175 .expect("checks lock")
176 .push((name, kind, Arc::new(move |state| Box::pin(f(state)))));
177 self
178 }
179
180 pub async fn run_checks(
182 &self,
183 state: Arc<StateMap>,
184 kinds: &[CheckKind],
185 ) -> Vec<CheckResult> {
186 run_check_list(&self.checks, state, kinds).await
187 }
188
189 pub fn with_probes(&mut self) -> &mut Self {
193 if self.probes {
194 return self;
195 }
196 self.probes = true;
197 let checks = Arc::clone(&self.checks);
198
199 self.get("/healthz", || async {
200 Response::json(&serde_json::json!({ "status": "ok" }))
201 });
202
203 self.get("/ready", move |req: Request| {
204 let checks = Arc::clone(&checks);
205 async move {
206 let results =
207 run_check_list(&checks, req.states(), &[CheckKind::Ready]).await;
208 ready_response(&results)
209 }
210 });
211
212 self
213 }
214
215 pub fn max_body_size(&mut self, bytes: usize) -> &mut Self {
216 self.max_body_size = bytes;
217 self.router.defaults.insert(crate::limits::MaxBody::bytes(bytes));
218 self
219 }
220
221 pub fn max_connections(&mut self, n: usize) -> &mut Self {
223 self.max_connections = n.max(1);
224 self
225 }
226
227 pub fn max_upgraded_connections(&mut self, n: usize) -> &mut Self {
229 self.max_upgraded_connections = n.max(1);
230 self
231 }
232
233 pub fn max_concurrent_streams(&mut self, n: usize) -> &mut Self {
236 self.max_concurrent_streams = n.max(1);
237 self
238 }
239
240 pub fn max_headers(&mut self, n: usize) -> &mut Self {
242 self.max_headers = n.max(1);
243 self
244 }
245
246 pub fn max_buf_size(&mut self, bytes: usize) -> &mut Self {
249 self.max_buf_size = Some(bytes.max(8192));
250 self
251 }
252
253 pub fn request_timeout(&mut self, timeout: Option<Duration>) -> &mut Self {
259 self.request_timeout = timeout;
260 if let Some(d) = timeout {
261 self.router
262 .defaults
263 .insert(crate::limits::RequestTimeout(d));
264 }
265 self
266 }
267
268 pub fn header_read_timeout(&mut self, timeout: Duration) -> &mut Self {
271 self.header_read_timeout = timeout;
272 self
273 }
274
275 pub fn idle_timeout(&mut self, timeout: Duration) -> &mut Self {
279 self.idle_timeout = timeout;
280 self
281 }
282
283 pub fn drain_timeout(&mut self, timeout: Duration) -> &mut Self {
285 self.drain_timeout = timeout;
286 self
287 }
288
289 pub fn keep_alive(&mut self, enabled: bool) -> &mut Self {
291 self.keep_alive = enabled;
292 self
293 }
294
295 pub fn trust_proxy(&mut self, trust: bool) -> &mut Self {
297 self.trust_proxy = trust;
298 self
299 }
300
301 pub fn cli_mode(&mut self, enabled: bool) -> &mut Self {
303 self.cli_mode = enabled;
304 self
305 }
306
307 pub fn service_in_cli(&mut self, enabled: bool) -> &mut Self {
309 self.service_in_cli = enabled;
310 self
311 }
312
313 pub fn install<P: Plugin>(&mut self, plugin: P) -> &mut Self {
314 let plugin_id = plugin.id();
315 if self.installed_plugins.contains(plugin_id) {
316 self.duplicate_plugin_ids.push(plugin_id);
317 return self;
318 }
319 let meta = plugin.meta();
320 for dep in plugin.requires() {
321 if !self.installed_plugins.contains(dep) {
322 self.missing_plugin_requires.push((plugin_id, dep));
323 }
324 }
325 match check_plugin_sdk(meta.sdk, PLUGIN_SDK_VERSION) {
326 SdkCompat::Ok => {}
327 SdkCompat::Warn { core, plugin: declared } => {
328 tracing::warn!(
329 plugin = plugin_id,
330 plugin_sdk = %declared,
331 core_sdk = %core,
332 "plugin SDK is older than core; consider rebuilding against the current Plugin SDK"
333 );
334 }
335 SdkCompat::Error(msg) => {
336 self.plugin_sdk_errors
337 .push(format!("plugin `{plugin_id}`: {msg}"));
338 }
339 }
340 self.installed_plugin_meta.push(InstalledPlugin {
341 id: plugin_id,
342 meta,
343 });
344 plugin.install(self);
345 self.installed_plugins.insert(plugin_id);
346 self
347 }
348
349 pub fn has_plugin(&self, id: &str) -> bool {
351 self.installed_plugins.contains(id)
352 }
353
354 pub fn installed_plugin_meta(&self) -> &[InstalledPlugin] {
356 &self.installed_plugin_meta
357 }
358
359 pub async fn run(self) -> Result<()> {
372 self.bind(Bind::Env { default_port: 3000 }).run().await
373 }
374
375 pub(crate) async fn run_cli_command(&self, args: &[String]) -> Result<bool> {
376 let Some(cmd) = args.first().map(String::as_str) else {
377 return Ok(false);
378 };
379
380 let server = self.build()?;
381 let state = server.state();
382 for hook in &server.startups {
383 hook(Arc::clone(&state)).await?;
384 }
385
386 let rest: Vec<String> = args.iter().skip(1).cloned().collect();
387 let handled = if let Some(handler) = self.cli_commands.get(cmd) {
388 handler(Arc::clone(&state), rest).await?;
389 true
390 } else {
391 match cmd {
392 "check" => {
393 println!("ok plugins");
395 let results = self
396 .run_checks(
397 Arc::clone(&state),
398 &[CheckKind::Ready, CheckKind::Audit],
399 )
400 .await;
401 let mut failed = false;
402 for r in &results {
403 if r.ok {
404 println!("ok {}", r.name);
405 } else {
406 println!(
407 "fail {} — {}",
408 r.name,
409 r.error.as_deref().unwrap_or("")
410 );
411 failed = true;
412 }
413 }
414 if failed {
415 return Err(Error::Internal(
416 "one or more checks failed".into(),
417 ));
418 }
419 println!("ok");
420 true
421 }
422 "routes" => {
423 println!("{}", self.explain());
424 true
425 }
426 "plugins" => {
427 for p in &self.installed_plugin_meta {
428 let desc = if p.meta.description.is_empty() {
429 "-"
430 } else {
431 p.meta.description
432 };
433 println!(
434 "{:<24} {:<20} sdk={} {}",
435 p.id, p.meta.name, p.meta.sdk, desc
436 );
437 }
438 if self.installed_plugin_meta.is_empty() {
439 println!("(no plugins installed)");
440 }
441 true
442 }
443 "openapi" => {
444 let out_idx = args.iter().position(|a| a == "--out");
445 let out_path = out_idx
446 .and_then(|idx| args.get(idx + 1))
447 .ok_or_else(|| Error::Internal("openapi requires --out <path>".into()))?;
448 let res = server
449 .handle_request(Method::GET, "/docs/openapi.json", "")
450 .await;
451 if !res.status_code().is_success() {
452 return Err(Error::Internal(format!(
453 "openapi endpoint failed with status {}",
454 res.status_code()
455 )));
456 }
457 let bytes = res
458 .body_bytes()
459 .ok_or_else(|| Error::Internal("openapi body is streaming".into()))?;
460 fs::write(out_path, bytes).map_err(|e| {
461 Error::Internal(format!("failed writing openapi to {out_path}: {e}"))
462 })?;
463 println!("wrote {}", out_path);
464 true
465 }
466 "tasks" => {
467 println!(
468 "tasks CLI requires the Tasks plugin (`app.install(Tasks::…)`).\n\
469 Then: tasks list | tasks schedule | tasks run NAME"
470 );
471 true
472 }
473 "i18n" if args.get(1).map(String::as_str) == Some("missing") => {
474 let res = server
475 .handle_request(Method::GET, "/_i18n/_missing.json", "")
476 .await;
477 if let Some(body) = res.body_bytes() {
478 println!("{}", String::from_utf8_lossy(body));
479 } else {
480 println!("i18n missing endpoint returned streaming body");
481 }
482 true
483 }
484 "i18n" => false,
485 _ => false,
486 }
487 };
488
489 if handled {
490 for hook in &server.shutdowns {
491 hook().await;
492 }
493 }
494 Ok(handled)
495 }
496
497 pub fn service<S: BackgroundService + 'static>(&mut self, service: S) -> &mut Self {
502 self.services.push(Box::new(service));
503 self
504 }
505
506 pub fn on_startup<F, Fut>(&mut self, f: F) -> &mut Self
508 where
509 F: Fn(Arc<StateMap>) -> Fut + Send + Sync + 'static,
510 Fut: std::future::Future<Output = Result<()>> + Send + 'static,
511 {
512 self.on_startup
513 .push(Arc::new(move |state| Box::pin(f(state))));
514 self
515 }
516
517 pub fn on_shutdown<F, Fut>(&mut self, f: F) -> &mut Self
519 where
520 F: Fn() -> Fut + Send + Sync + 'static,
521 Fut: std::future::Future<Output = ()> + Send + 'static,
522 {
523 self.on_shutdown
524 .push(Arc::new(move || Box::pin(f())));
525 self
526 }
527
528 pub fn events(&mut self) -> EventBus {
530 if let Some(bus) = self.try_state::<EventBus>() {
531 return (*bus).clone();
532 }
533 let bus = EventBus::new();
534 self.state(bus.clone());
535 bus
536 }
537
538 pub fn explain(&self) -> String {
540 self.router.explain()
541 }
542
543 pub async fn handle(&self, req: Request) -> Response {
545 match self.build() {
546 Ok(server) => server.handle(req).await,
547 Err(err) => err.into_response(),
548 }
549 }
550
551 pub async fn handle_request(&self, method: Method, path: &str, body: &str) -> Response {
555 let req = Request::builder()
556 .method(method)
557 .path(path)
558 .body(Bytes::from(body.to_string()))
559 .build();
560 self.handle(req).await
561 }
562
563 #[cfg(any(test, feature = "testing"))]
565 pub async fn run_startup(&self) -> Result<Arc<StateMap>> {
566 self.build()?.run_startup().await
567 }
568
569 #[cfg(any(test, feature = "testing"))]
571 pub async fn run_shutdown(&self) {
572 if let Ok(server) = self.build() {
573 server.run_shutdown().await;
574 }
575 }
576}
577
578pub(crate) fn addr_from_env(default_port: u16) -> Result<SocketAddr> {
579 let port = std::env::var("PORT")
580 .ok()
581 .and_then(|p| p.parse::<u16>().ok())
582 .unwrap_or(default_port);
583
584 match std::env::var("HOST") {
585 Ok(host) if !host.is_empty() => {
586 if let Ok(ip) = host.parse::<IpAddr>() {
587 return Ok(SocketAddr::new(ip, port));
588 }
589 if let Ok(addr) = host.parse::<SocketAddr>() {
591 return Ok(addr);
592 }
593 format!("{host}:{port}")
594 .parse()
595 .map_err(|e| Error::Internal(format!("HOST={host:?} invalid: {e}")))
596 }
597 _ => Ok(SocketAddr::from(([0, 0, 0, 0], port))),
598 }
599}
600
601impl Default for App {
602 fn default() -> Self {
603 Self::new()
604 }
605}
606
607impl Deref for App {
608 type Target = Router;
609
610 fn deref(&self) -> &Router {
611 &self.router
612 }
613}
614
615impl DerefMut for App {
616 fn deref_mut(&mut self) -> &mut Router {
617 &mut self.router
618 }
619}
620
621async fn run_check_list(
622 checks: &CheckList,
623 state: Arc<StateMap>,
624 kinds: &[CheckKind],
625) -> Vec<CheckResult> {
626 let entries: Vec<CheckEntry> = checks.lock().expect("checks lock").clone();
627 let mut out = Vec::with_capacity(entries.len());
628 for (name, kind, check) in entries {
629 if !kinds.contains(&kind) {
630 continue;
631 }
632 match check(Arc::clone(&state)).await {
633 Ok(()) => out.push(CheckResult {
634 name,
635 ok: true,
636 error: None,
637 }),
638 Err(e) => out.push(CheckResult {
639 name,
640 ok: false,
641 error: Some(e.to_string()),
642 }),
643 }
644 }
645 out
646}
647
648fn ready_response(results: &[CheckResult]) -> Response {
649 let mut checks = serde_json::Map::new();
650 let mut failed = Vec::new();
651 for r in results {
652 if r.ok {
653 checks.insert(r.name.to_string(), serde_json::json!("ok"));
654 } else {
655 let msg = r.error.clone().unwrap_or_else(|| "failed".into());
656 checks.insert(r.name.to_string(), serde_json::json!(msg));
657 failed.push(r.name);
658 }
659 }
660 let mut res = if failed.is_empty() {
661 Response::json(&serde_json::json!({
662 "status": "ok",
663 "checks": checks,
664 }))
665 } else {
666 Response::json(&serde_json::json!({
667 "status": "not_ready",
668 "failed": failed,
669 "checks": checks,
670 }))
671 .status(503)
672 };
673 if let Ok(id) = std::env::var("SOVA_INSTANCE_ID") {
676 if !id.is_empty() {
677 res = res.header("x-sova-instance", id);
678 }
679 }
680 res
681}
682
683#[cfg(test)]
684mod env_addr_tests {
685 use super::addr_from_env;
686 use std::sync::Mutex;
687
688 static ENV_LOCK: Mutex<()> = Mutex::new(());
689
690 #[test]
691 fn port_from_env() {
692 let _g = ENV_LOCK.lock().unwrap();
693 std::env::set_var("PORT", "9876");
694 std::env::remove_var("HOST");
695 let addr = addr_from_env(3000).unwrap();
696 assert_eq!(addr.port(), 9876);
697 std::env::remove_var("PORT");
698 }
699
700 #[test]
701 fn host_ip_from_env() {
702 let _g = ENV_LOCK.lock().unwrap();
703 std::env::remove_var("PORT");
704 std::env::set_var("HOST", "127.0.0.1");
705 let addr = addr_from_env(3000).unwrap();
706 assert_eq!(addr, "127.0.0.1:3000".parse().unwrap());
707 std::env::remove_var("HOST");
708 }
709}