1mod bind;
2mod hooks;
3
4pub use bind::{Bind, BoundApp, Http};
5pub use hooks::Server;
6pub(crate) use hooks::{AppInner, ListenParts, ShutdownHook, StartupHook};
7
8use crate::error::{Error, Result};
9use crate::events::EventBus;
10use crate::handler::BoxFuture;
11use crate::plugin::{check_plugin_sdk, InstalledPlugin, Plugin, SdkCompat, PLUGIN_SDK_VERSION};
12use crate::request::Request;
13use crate::response::Response;
14use crate::router::Router;
15use crate::service::{BackgroundService, BoxedService};
16use crate::state::StateMap;
17use bytes::Bytes;
18use http::Method;
19use std::collections::{HashMap, HashSet};
20use std::fs;
21use std::net::{IpAddr, SocketAddr};
22use std::ops::{Deref, DerefMut};
23use std::sync::Arc;
24use std::time::Duration;
25
26pub(crate) type CliCommandFn =
27 Arc<dyn Fn(Arc<StateMap>, Vec<String>) -> BoxFuture<Result<()>> + Send + Sync>;
28
29pub(crate) type CheckFn = Arc<dyn Fn(Arc<StateMap>) -> BoxFuture<Result<()>> + Send + Sync>;
30
31#[derive(Debug, Clone, Copy, PartialEq, Eq)]
33pub enum CheckKind {
34 Ready,
36 Audit,
38}
39
40#[derive(Debug, Clone)]
42pub struct CheckResult {
43 pub name: &'static str,
44 pub ok: bool,
45 pub error: Option<String>,
46}
47
48type CheckEntry = (&'static str, CheckKind, CheckFn);
49type CheckList = Arc<std::sync::Mutex<Vec<CheckEntry>>>;
50
51const DEFAULT_MAX_BODY: usize = 2 * 1024 * 1024;
52const DEFAULT_MAX_CONNECTIONS: usize = 1024;
53const DEFAULT_MAX_UPGRADED: usize = 1024;
54const DEFAULT_MAX_CONCURRENT_STREAMS: usize = 200;
55const DEFAULT_MAX_HEADERS: usize = 100;
56const DEFAULT_DRAIN_TIMEOUT: Duration = Duration::from_secs(20);
58const DEFAULT_HEADER_READ_TIMEOUT: Duration = Duration::from_secs(10);
59const DEFAULT_IDLE_TIMEOUT: Duration = Duration::from_secs(60);
61
62pub struct App {
64 pub(crate) router: Router,
65 pub(crate) max_body_size: usize,
66 pub(crate) max_connections: usize,
67 pub(crate) max_upgraded_connections: usize,
68 pub(crate) max_concurrent_streams: usize,
69 pub(crate) max_headers: usize,
70 pub(crate) max_buf_size: Option<usize>,
71 pub(crate) request_timeout: Option<Duration>,
72 pub(crate) header_read_timeout: Duration,
73 pub(crate) idle_timeout: Duration,
74 pub(crate) drain_timeout: Duration,
75 pub(crate) keep_alive: bool,
76 pub(crate) trust_proxy: bool,
77 pub(crate) reuseport: bool,
78 pub(crate) cli_mode: bool,
80 pub(crate) service_in_cli: bool,
81 pub(crate) hsts: bool,
82 pub(crate) alt_svc: Option<String>,
83 #[cfg(feature = "tls")]
85 pub(crate) tls: Option<crate::Tls>,
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 #[cfg(feature = "tls")]
121 tls: None,
122 installed_plugins: HashSet::new(),
123 installed_plugin_meta: Vec::new(),
124 missing_plugin_requires: Vec::new(),
125 duplicate_plugin_ids: Vec::new(),
126 plugin_sdk_errors: Vec::new(),
127 on_startup: Vec::new(),
128 on_shutdown: Vec::new(),
129 services: Vec::new(),
130 cli_commands: HashMap::new(),
131 checks: Arc::new(std::sync::Mutex::new(Vec::new())),
132 probes: false,
133 }
134 }
135
136 pub fn register_cli<F, Fut>(&mut self, name: &'static str, f: F) -> &mut Self
138 where
139 F: Fn(Arc<StateMap>, Vec<String>) -> Fut + Send + Sync + 'static,
140 Fut: std::future::Future<Output = Result<()>> + Send + 'static,
141 {
142 self.cli_commands
143 .insert(name, Arc::new(move |state, args| Box::pin(f(state, args))));
144 self
145 }
146
147 pub fn register_check<F, Fut>(&mut self, name: &'static str, f: F) -> &mut Self
149 where
150 F: Fn(Arc<StateMap>) -> Fut + Send + Sync + 'static,
151 Fut: std::future::Future<Output = Result<()>> + Send + 'static,
152 {
153 self.push_check(name, CheckKind::Ready, f)
154 }
155
156 pub fn register_audit<F, Fut>(&mut self, name: &'static str, f: F) -> &mut Self
158 where
159 F: Fn(Arc<StateMap>) -> Fut + Send + Sync + 'static,
160 Fut: std::future::Future<Output = Result<()>> + Send + 'static,
161 {
162 self.push_check(name, CheckKind::Audit, f)
163 }
164
165 fn push_check<F, Fut>(&mut self, name: &'static str, kind: CheckKind, f: F) -> &mut Self
166 where
167 F: Fn(Arc<StateMap>) -> Fut + Send + Sync + 'static,
168 Fut: std::future::Future<Output = Result<()>> + Send + 'static,
169 {
170 self.checks.lock().expect("checks lock").push((
171 name,
172 kind,
173 Arc::new(move |state| Box::pin(f(state))),
174 ));
175 self
176 }
177
178 pub async fn run_checks(&self, state: Arc<StateMap>, kinds: &[CheckKind]) -> 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 = run_check_list(&checks, req.states(), &[CheckKind::Ready]).await;
201 ready_response(&results)
202 }
203 });
204
205 self
206 }
207
208 pub fn max_body_size(&mut self, bytes: usize) -> &mut Self {
209 self.max_body_size = bytes;
210 self.router
211 .defaults
212 .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 {
323 core,
324 plugin: declared,
325 } => {
326 tracing::warn!(
327 plugin = plugin_id,
328 plugin_sdk = %declared,
329 core_sdk = %core,
330 "plugin SDK is older than core; consider rebuilding against the current Plugin SDK"
331 );
332 }
333 SdkCompat::Error(msg) => {
334 self.plugin_sdk_errors
335 .push(format!("plugin `{plugin_id}`: {msg}"));
336 }
337 }
338 self.installed_plugin_meta.push(InstalledPlugin {
339 id: plugin_id,
340 meta,
341 });
342 plugin.install(self);
343 self.installed_plugins.insert(plugin_id);
344 self
345 }
346
347 pub fn has_plugin(&self, id: &str) -> bool {
349 self.installed_plugins.contains(id)
350 }
351
352 pub fn installed_plugin_meta(&self) -> &[InstalledPlugin] {
354 &self.installed_plugin_meta
355 }
356
357 pub async fn run(self) -> Result<()> {
370 self.bind(Bind::Env { default_port: 3000 }).run().await
371 }
372
373 pub(crate) async fn run_cli_command(&self, args: &[String]) -> Result<bool> {
374 let Some(cmd) = args.first().map(String::as_str) else {
375 return Ok(false);
376 };
377
378 let server = self.build()?;
379 let state = server.state();
380 for hook in &server.startups {
381 hook(Arc::clone(&state)).await?;
382 }
383
384 let rest: Vec<String> = args.iter().skip(1).cloned().collect();
385 let handled = if let Some(handler) = self.cli_commands.get(cmd) {
386 handler(Arc::clone(&state), rest).await?;
387 true
388 } else {
389 match cmd {
390 "check" => {
391 println!("ok plugins");
393 let results = self
394 .run_checks(Arc::clone(&state), &[CheckKind::Ready, CheckKind::Audit])
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!("fail {} — {}", r.name, r.error.as_deref().unwrap_or(""));
402 failed = true;
403 }
404 }
405 if failed {
406 return Err(Error::Internal("one or more checks failed".into()));
407 }
408 println!("ok");
409 true
410 }
411 "routes" => {
412 println!("{}", self.explain());
413 true
414 }
415 "plugins" => {
416 for p in &self.installed_plugin_meta {
417 let desc = if p.meta.description.is_empty() {
418 "-"
419 } else {
420 p.meta.description
421 };
422 println!(
423 "{:<24} {:<20} sdk={} {}",
424 p.id, p.meta.name, p.meta.sdk, desc
425 );
426 }
427 if self.installed_plugin_meta.is_empty() {
428 println!("(no plugins installed)");
429 }
430 true
431 }
432 "openapi" => {
433 let out_idx = args.iter().position(|a| a == "--out");
434 let out_path = out_idx
435 .and_then(|idx| args.get(idx + 1))
436 .ok_or_else(|| Error::Internal("openapi requires --out <path>".into()))?;
437 let res = server
438 .handle_request(Method::GET, "/docs/openapi.json", "")
439 .await;
440 if !res.status_code().is_success() {
441 return Err(Error::Internal(format!(
442 "openapi endpoint failed with status {}",
443 res.status_code()
444 )));
445 }
446 let bytes = res
447 .body_bytes()
448 .ok_or_else(|| Error::Internal("openapi body is streaming".into()))?;
449 fs::write(out_path, bytes).map_err(|e| {
450 Error::Internal(format!("failed writing openapi to {out_path}: {e}"))
451 })?;
452 println!("wrote {}", out_path);
453 true
454 }
455 "tasks" => {
456 println!(
457 "tasks CLI requires the Tasks plugin (`app.install(Tasks::…)`).\n\
458 Then: tasks list | tasks schedule | tasks run NAME"
459 );
460 true
461 }
462 "i18n" if args.get(1).map(String::as_str) == Some("missing") => {
463 let res = server
464 .handle_request(Method::GET, "/_i18n/_missing.json", "")
465 .await;
466 if let Some(body) = res.body_bytes() {
467 println!("{}", String::from_utf8_lossy(body));
468 } else {
469 println!("i18n missing endpoint returned streaming body");
470 }
471 true
472 }
473 "i18n" => false,
474 _ => false,
475 }
476 };
477
478 if handled {
479 for hook in &server.shutdowns {
480 hook().await;
481 }
482 }
483 Ok(handled)
484 }
485
486 pub fn service<S: BackgroundService + 'static>(&mut self, service: S) -> &mut Self {
491 self.services.push(Box::new(service));
492 self
493 }
494
495 pub fn on_startup<F, Fut>(&mut self, f: F) -> &mut Self
497 where
498 F: Fn(Arc<StateMap>) -> Fut + Send + Sync + 'static,
499 Fut: std::future::Future<Output = Result<()>> + Send + 'static,
500 {
501 self.on_startup
502 .push(Arc::new(move |state| Box::pin(f(state))));
503 self
504 }
505
506 pub fn on_shutdown<F, Fut>(&mut self, f: F) -> &mut Self
508 where
509 F: Fn() -> Fut + Send + Sync + 'static,
510 Fut: std::future::Future<Output = ()> + Send + 'static,
511 {
512 self.on_shutdown.push(Arc::new(move || Box::pin(f())));
513 self
514 }
515
516 pub fn events(&mut self) -> EventBus {
518 if let Some(bus) = self.try_state::<EventBus>() {
519 return (*bus).clone();
520 }
521 let bus = EventBus::new();
522 self.state(bus.clone());
523 bus
524 }
525
526 pub fn explain(&self) -> String {
528 self.router.explain()
529 }
530
531 pub async fn handle(&self, req: Request) -> Response {
533 match self.build() {
534 Ok(server) => server.handle(req).await,
535 Err(err) => err.into_response(),
536 }
537 }
538
539 pub async fn handle_request(&self, method: Method, path: &str, body: &str) -> Response {
543 let req = Request::builder()
544 .method(method)
545 .path(path)
546 .body(Bytes::from(body.to_string()))
547 .build();
548 self.handle(req).await
549 }
550
551 #[cfg(any(test, feature = "testing"))]
553 pub async fn run_startup(&self) -> Result<Arc<StateMap>> {
554 self.build()?.run_startup().await
555 }
556
557 #[cfg(any(test, feature = "testing"))]
559 pub async fn run_shutdown(&self) {
560 if let Ok(server) = self.build() {
561 server.run_shutdown().await;
562 }
563 }
564}
565
566pub(crate) fn addr_from_env(default_port: u16) -> Result<SocketAddr> {
567 let port = std::env::var("PORT")
568 .ok()
569 .and_then(|p| p.parse::<u16>().ok())
570 .unwrap_or(default_port);
571
572 match std::env::var("HOST") {
573 Ok(host) if !host.is_empty() => {
574 if let Ok(ip) = host.parse::<IpAddr>() {
575 return Ok(SocketAddr::new(ip, port));
576 }
577 if let Ok(addr) = host.parse::<SocketAddr>() {
579 return Ok(addr);
580 }
581 format!("{host}:{port}")
582 .parse()
583 .map_err(|e| Error::Internal(format!("HOST={host:?} invalid: {e}")))
584 }
585 _ => Ok(SocketAddr::from(([0, 0, 0, 0], port))),
586 }
587}
588
589impl Default for App {
590 fn default() -> Self {
591 Self::new()
592 }
593}
594
595impl Deref for App {
596 type Target = Router;
597
598 fn deref(&self) -> &Router {
599 &self.router
600 }
601}
602
603impl DerefMut for App {
604 fn deref_mut(&mut self) -> &mut Router {
605 &mut self.router
606 }
607}
608
609async fn run_check_list(
610 checks: &CheckList,
611 state: Arc<StateMap>,
612 kinds: &[CheckKind],
613) -> Vec<CheckResult> {
614 let entries: Vec<CheckEntry> = checks.lock().expect("checks lock").clone();
615 let mut out = Vec::with_capacity(entries.len());
616 for (name, kind, check) in entries {
617 if !kinds.contains(&kind) {
618 continue;
619 }
620 match check(Arc::clone(&state)).await {
621 Ok(()) => out.push(CheckResult {
622 name,
623 ok: true,
624 error: None,
625 }),
626 Err(e) => out.push(CheckResult {
627 name,
628 ok: false,
629 error: Some(e.to_string()),
630 }),
631 }
632 }
633 out
634}
635
636fn ready_response(results: &[CheckResult]) -> Response {
637 let mut checks = serde_json::Map::new();
638 let mut failed = Vec::new();
639 for r in results {
640 if r.ok {
641 checks.insert(r.name.to_string(), serde_json::json!("ok"));
642 } else {
643 let msg = r.error.clone().unwrap_or_else(|| "failed".into());
644 checks.insert(r.name.to_string(), serde_json::json!(msg));
645 failed.push(r.name);
646 }
647 }
648 let mut res = if failed.is_empty() {
649 Response::json(&serde_json::json!({
650 "status": "ok",
651 "checks": checks,
652 }))
653 } else {
654 Response::json(&serde_json::json!({
655 "status": "not_ready",
656 "failed": failed,
657 "checks": checks,
658 }))
659 .status(503)
660 };
661 if let Ok(id) = std::env::var("SOVA_INSTANCE_ID") {
664 if !id.is_empty() {
665 res = res.header("x-sova-instance", id);
666 }
667 }
668 res
669}
670
671#[cfg(test)]
672mod env_addr_tests {
673 use super::addr_from_env;
674 use std::sync::Mutex;
675
676 static ENV_LOCK: Mutex<()> = Mutex::new(());
677
678 #[test]
679 fn port_from_env() {
680 let _g = ENV_LOCK.lock().unwrap();
681 std::env::set_var("PORT", "9876");
682 std::env::remove_var("HOST");
683 let addr = addr_from_env(3000).unwrap();
684 assert_eq!(addr.port(), 9876);
685 std::env::remove_var("PORT");
686 }
687
688 #[test]
689 fn host_ip_from_env() {
690 let _g = ENV_LOCK.lock().unwrap();
691 std::env::remove_var("PORT");
692 std::env::set_var("HOST", "127.0.0.1");
693 let addr = addr_from_env(3000).unwrap();
694 assert_eq!(addr, "127.0.0.1:3000".parse().unwrap());
695 std::env::remove_var("HOST");
696 }
697}