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