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