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