1pub mod controllers;
2mod routes;
3
4pub use controllers::{
5 A2aController, AppsController, ArtifactsController, DebugController, RuntimeController,
6 SessionController,
7};
8
9use crate::{
10 ServerConfig,
11 auth_bridge::{RequestContext, RequestContextError, RequestContextExtractor},
12 web_ui,
13};
14use axum::{
15 Json, Router,
16 body::Body,
17 extract::{DefaultBodyLimit, State},
18 http::{HeaderMap, HeaderName, HeaderValue, Method, Request, StatusCode, header},
19 middleware::{self, Next},
20 response::{IntoResponse, Response},
21 routing::{get, post},
22};
23use serde::Serialize;
24use std::sync::Arc;
25use tokio_util::sync::CancellationToken;
26use tower::ServiceBuilder;
27use tower_http::{
28 cors::{AllowOrigin, CorsLayer},
29 set_header::SetResponseHeaderLayer,
30 timeout::TimeoutLayer,
31 trace::TraceLayer,
32};
33
34const REQUEST_ID_HEADER: &str = "x-request-id";
35
36#[derive(Clone)]
37struct HealthController {
38 session_service: Arc<dyn adk_session::SessionService>,
39 artifact_service: Option<Arc<dyn adk_artifact::ArtifactService>>,
40 memory_service: Option<Arc<dyn adk_core::Memory>>,
41}
42
43impl HealthController {
44 fn new(config: &ServerConfig) -> Self {
45 Self {
46 session_service: config.session_service.clone(),
47 artifact_service: config.artifact_service.clone(),
48 memory_service: config.memory_service.clone(),
49 }
50 }
51}
52
53#[derive(Clone, Debug)]
54struct RequestId(String);
55
56impl RequestId {
57 fn as_str(&self) -> &str {
58 &self.0
59 }
60}
61
62#[derive(Serialize)]
63#[serde(rename_all = "camelCase")]
64struct HealthResponse {
65 status: &'static str,
66 components: HealthComponents,
67}
68
69#[derive(Serialize)]
70#[serde(rename_all = "camelCase")]
71struct HealthComponents {
72 session: ComponentHealth,
73 memory: ComponentHealth,
74 artifact: ComponentHealth,
75}
76
77#[derive(Serialize)]
78#[serde(rename_all = "camelCase")]
79struct ComponentHealth {
80 status: &'static str,
81 #[serde(skip_serializing_if = "Option::is_none")]
82 error: Option<String>,
83}
84
85impl ComponentHealth {
86 fn healthy() -> Self {
87 Self { status: "healthy", error: None }
88 }
89
90 fn unhealthy(error: impl Into<String>) -> Self {
91 Self { status: "unhealthy", error: Some(error.into()) }
92 }
93
94 fn not_configured() -> Self {
95 Self { status: "not_configured", error: None }
96 }
97}
98
99fn build_cors_layer(config: &ServerConfig) -> CorsLayer {
101 let cors = CorsLayer::new()
102 .allow_methods([Method::GET, Method::POST, Method::PUT, Method::DELETE, Method::OPTIONS])
103 .allow_headers([
104 header::CONTENT_TYPE,
105 header::AUTHORIZATION,
106 HeaderName::from_static(REQUEST_ID_HEADER),
107 HeaderName::from_static("x-adk-ui-protocol"),
108 HeaderName::from_static("x-adk-ui-transport"),
109 ]);
110
111 if config.security.allowed_origins.is_empty() {
112 cors.allow_origin(AllowOrigin::any())
113 } else {
114 let origins: Vec<HeaderValue> = config
115 .security
116 .allowed_origins
117 .iter()
118 .filter_map(|origin| origin.parse().ok())
119 .collect();
120 cors.allow_origin(origins)
121 }
122}
123
124fn validate_request_id(headers: &HeaderMap) -> Option<String> {
125 let value = headers.get(REQUEST_ID_HEADER)?;
126 let raw = value.to_str().ok()?;
127 if raw.len() > 128 {
128 return None;
129 }
130 uuid::Uuid::parse_str(raw).ok()?;
131 Some(raw.to_string())
132}
133
134async fn request_id_middleware(mut request: Request<Body>, next: Next) -> Response {
135 let request_id =
136 validate_request_id(request.headers()).unwrap_or_else(|| uuid::Uuid::new_v4().to_string());
137
138 request.extensions_mut().insert(RequestId(request_id.clone()));
139
140 let mut response = next.run(request).await;
141 if let Ok(value) = HeaderValue::from_str(&request_id) {
142 response.headers_mut().insert(HeaderName::from_static(REQUEST_ID_HEADER), value);
143 }
144 response
145}
146
147async fn auth_middleware(
148 request: Request<Body>,
149 next: Next,
150 extractor: Option<Arc<dyn RequestContextExtractor>>,
151) -> Response {
152 let (mut parts, body) = request.into_parts();
153
154 let request_context = match extractor {
155 Some(extractor) => match extractor.extract(&parts).await {
156 Ok(context) => Some(context),
157 Err(RequestContextError::MissingAuth) => {
158 return (
159 StatusCode::UNAUTHORIZED,
160 Json(serde_json::json!({ "error": "missing authorization" })),
161 )
162 .into_response();
163 }
164 Err(RequestContextError::InvalidToken(message)) => {
165 return (
166 StatusCode::UNAUTHORIZED,
167 Json(serde_json::json!({ "error": format!("invalid token: {message}") })),
168 )
169 .into_response();
170 }
171 Err(RequestContextError::ExtractionFailed(message)) => {
172 return (
173 StatusCode::INTERNAL_SERVER_ERROR,
174 Json(serde_json::json!({
175 "error": format!("auth extraction failed: {message}")
176 })),
177 )
178 .into_response();
179 }
180 },
181 None => None,
182 };
183
184 parts.extensions.insert::<Option<RequestContext>>(request_context);
185 next.run(Request::from_parts(parts, body)).await
186}
187
188async fn health_check(State(controller): State<HealthController>) -> impl IntoResponse {
189 let session = match controller.session_service.health_check().await {
190 Ok(()) => ComponentHealth::healthy(),
191 Err(error) => ComponentHealth::unhealthy(error.to_string()),
192 };
193
194 let memory = match controller.memory_service.as_ref() {
195 Some(service) => match service.health_check().await {
196 Ok(()) => ComponentHealth::healthy(),
197 Err(error) => ComponentHealth::unhealthy(error.to_string()),
198 },
199 None => ComponentHealth::not_configured(),
200 };
201
202 let artifact = match controller.artifact_service.as_ref() {
203 Some(service) => match service.health_check().await {
204 Ok(()) => ComponentHealth::healthy(),
205 Err(error) => ComponentHealth::unhealthy(error.to_string()),
206 },
207 None => ComponentHealth::not_configured(),
208 };
209
210 let healthy = session.status == "healthy"
211 && memory.status != "unhealthy"
212 && artifact.status != "unhealthy";
213
214 (
215 if healthy { StatusCode::OK } else { StatusCode::SERVICE_UNAVAILABLE },
216 Json(HealthResponse {
217 status: if healthy { "healthy" } else { "unhealthy" },
218 components: HealthComponents { session, memory, artifact },
219 }),
220 )
221}
222
223pub fn create_app(config: ServerConfig) -> Router {
225 create_app_with_a2a(config, None)
226}
227
228#[cfg(feature = "yaml-agent")]
238async fn start_yaml_agent_watchers(
239 dirs: &[std::path::PathBuf],
240) -> Vec<Arc<crate::yaml_agent::HotReloadWatcher>> {
241 use crate::yaml_agent::{AgentConfigLoader, HotReloadWatcher};
242
243 let mut watchers = Vec::new();
244
245 for dir in dirs {
246 let registry: Arc<dyn adk_core::ToolRegistry> = Arc::new(EmptyToolRegistry);
251 let factory: Arc<dyn crate::yaml_agent::ModelFactory> = Arc::new(NoOpModelFactory);
252 let loader = Arc::new(AgentConfigLoader::new(registry, factory));
253 let watcher = Arc::new(HotReloadWatcher::new(loader));
254
255 match watcher.watch(dir).await {
256 Ok(handle) => {
257 tracing::info!("started YAML agent hot reload watcher for {}", dir.display());
258 drop(handle);
260 watchers.push(watcher);
261 }
262 Err(e) => {
263 tracing::warn!("failed to start YAML agent watcher for {}: {e}", dir.display());
264 }
265 }
266 }
267
268 watchers
269}
270
271#[cfg(feature = "yaml-agent")]
273struct EmptyToolRegistry;
274
275#[cfg(feature = "yaml-agent")]
276impl adk_core::ToolRegistry for EmptyToolRegistry {
277 fn resolve(&self, _tool_name: &str) -> Option<Arc<dyn adk_core::Tool>> {
278 None
279 }
280
281 fn available_tools(&self) -> Vec<String> {
282 vec![]
283 }
284}
285
286#[cfg(feature = "yaml-agent")]
292struct NoOpModelFactory;
293
294#[cfg(feature = "yaml-agent")]
295#[async_trait::async_trait]
296impl crate::yaml_agent::ModelFactory for NoOpModelFactory {
297 async fn create_model(
298 &self,
299 provider: &str,
300 model_id: &str,
301 ) -> adk_core::Result<Arc<dyn adk_core::Llm>> {
302 Err(adk_core::AdkError::config(format!(
303 "no model factory configured for YAML agent loading \
304 (requested provider='{provider}', model_id='{model_id}'). \
305 Configure a ModelFactory on the server to enable YAML agent model creation."
306 )))
307 }
308}
309
310pub fn create_app_with_a2a(config: ServerConfig, a2a_base_url: Option<&str>) -> Router {
312 let session_controller = SessionController::new(config.session_service.clone());
313 let runtime_controller = RuntimeController::new(config.clone());
314 let apps_controller = AppsController::new(config.clone());
315 let artifacts_controller = ArtifactsController::new(config.clone());
316 let debug_controller = DebugController::new(config.clone());
317 let health_controller = HealthController::new(&config);
318
319 #[cfg(feature = "yaml-agent")]
321 {
322 let dirs = config.yaml_agent_dirs.clone();
323 if !dirs.is_empty() {
324 tokio::spawn(async move {
325 let _watchers = start_yaml_agent_watchers(&dirs).await;
326 std::future::pending::<()>().await;
330 });
331 }
332 }
333
334 let auth_layer = middleware::from_fn({
335 let extractor = config.request_context_extractor.clone();
336 move |request: Request<Body>, next: Next| {
337 let extractor = extractor.clone();
338 async move { auth_middleware(request, next, extractor).await }
339 }
340 });
341
342 let health_router =
343 Router::new().route("/health", get(health_check)).with_state(health_controller);
344
345 let ui_api_router = Router::new()
346 .route("/apps", get(controllers::apps::list_apps))
347 .route("/list-apps", get(controllers::apps::list_apps_compat))
348 .with_state(apps_controller)
349 .route("/ui/capabilities", get(controllers::ui::ui_capabilities))
350 .route("/ui/initialize", post(controllers::ui::ui_initialize))
351 .route("/ui/message", post(controllers::ui::ui_message))
352 .route("/ui/update-model-context", post(controllers::ui::ui_update_model_context))
353 .route("/ui/notifications/poll", post(controllers::ui::ui_poll_notifications))
354 .route(
355 "/ui/notifications/resources-list-changed",
356 post(controllers::ui::ui_notify_resources_list_changed),
357 )
358 .route(
359 "/ui/notifications/tools-list-changed",
360 post(controllers::ui::ui_notify_tools_list_changed),
361 )
362 .route("/ui/resources", get(controllers::ui::list_ui_resources))
363 .route("/ui/resources/read", get(controllers::ui::read_ui_resource))
364 .route("/ui/resources/register", post(controllers::ui::register_ui_resource))
365 .layer(auth_layer.clone());
368
369 let session_router = Router::new()
370 .route("/sessions", post(controllers::session::create_session))
371 .route(
372 "/sessions/{app_name}/{user_id}/{session_id}",
373 get(controllers::session::get_session).delete(controllers::session::delete_session),
374 )
375 .route(
376 "/apps/{app_name}/users/{user_id}/sessions",
377 get(controllers::session::list_sessions)
378 .post(controllers::session::create_session_from_path),
379 )
380 .route(
381 "/apps/{app_name}/users/{user_id}/sessions/{session_id}",
382 get(controllers::session::get_session_from_path)
383 .post(controllers::session::create_session_from_path)
384 .delete(controllers::session::delete_session_from_path),
385 )
386 .with_state(session_controller)
387 .layer(auth_layer.clone());
388
389 let runtime_router = Router::new()
390 .route("/run/{app_name}/{user_id}/{session_id}", post(controllers::runtime::run_sse))
391 .route("/run_sse", post(controllers::runtime::run_sse_compat))
392 .with_state(runtime_controller);
393
394 let artifacts_router = Router::new()
395 .route(
396 "/sessions/{app_name}/{user_id}/{session_id}/artifacts",
397 get(controllers::artifacts::list_artifacts),
398 )
399 .route(
400 "/sessions/{app_name}/{user_id}/{session_id}/artifacts/{artifact_name}",
401 get(controllers::artifacts::get_artifact),
402 )
403 .with_state(artifacts_controller)
404 .layer(auth_layer.clone());
405
406 let mut debug_router = Router::new()
407 .route("/debug/trace/session/{session_id}", get(controllers::debug::get_session_traces))
408 .route(
409 "/debug/graph/{app_name}/{user_id}/{session_id}/{event_id}",
410 get(controllers::debug::get_graph),
411 )
412 .route(
413 "/apps/{app_name}/users/{user_id}/sessions/{session_id}/events/{event_id}/graph",
414 get(controllers::debug::get_graph),
415 )
416 .route("/apps/{app_name}/eval_sets", get(controllers::debug::get_eval_sets))
417 .route(
418 "/apps/{app_name}/users/{user_id}/sessions/{session_id}/events/{event_id}",
419 get(controllers::debug::get_event),
420 );
421
422 if config.request_context_extractor.is_none() || config.security.expose_admin_debug {
423 debug_router = debug_router
424 .route("/debug/trace/{event_id}", get(controllers::debug::get_trace_by_event_id));
425 }
426
427 let debug_router = debug_router.with_state(debug_controller.clone()).layer(auth_layer.clone());
428
429 let api_router = Router::new()
430 .merge(health_router)
431 .merge(ui_api_router)
432 .merge(session_router)
433 .merge(runtime_router)
434 .merge(artifacts_router)
435 .merge(debug_router);
436
437 let ui_router = Router::new()
438 .route("/", get(web_ui::root_redirect))
439 .route("/ui/", get(web_ui::serve_ui_index))
440 .route("/ui/assets/config/runtime-config.json", get(web_ui::serve_runtime_config))
441 .with_state(config.clone())
442 .route("/ui/{*path}", get(web_ui::serve_ui_assets));
443
444 let mut app = Router::new().nest("/api", api_router).merge(ui_router);
445
446 if let Some(base_url) = a2a_base_url {
447 let a2a_controller = A2aController::new(config.clone(), base_url);
448 let a2a_discovery = Router::new()
454 .route("/.well-known/agent.json", get(controllers::a2a::get_agent_card))
455 .with_state(a2a_controller.clone());
456 let a2a_rpc = Router::new()
457 .route("/a2a", post(controllers::a2a::handle_jsonrpc))
458 .route("/a2a/stream", post(controllers::a2a::handle_jsonrpc_stream))
459 .with_state(a2a_controller)
460 .layer(auth_layer.clone());
461 app = app.merge(a2a_discovery).merge(a2a_rpc);
462 }
463
464 let cors_layer = build_cors_layer(&config);
465 let trace_layer = TraceLayer::new_for_http().make_span_with(|request: &Request<Body>| {
466 let request_id =
467 request.extensions().get::<RequestId>().map(RequestId::as_str).unwrap_or("");
468 tracing::info_span!(
469 "http.request",
470 request.id = %request_id,
471 http.method = %request.method(),
472 http.path = %request.uri().path()
473 )
474 });
475
476 app.layer(
477 ServiceBuilder::new()
478 .layer(middleware::from_fn(request_id_middleware))
479 .layer(trace_layer)
480 .layer(TimeoutLayer::with_status_code(
481 StatusCode::REQUEST_TIMEOUT,
482 config.security.request_timeout,
483 ))
484 .layer(DefaultBodyLimit::max(config.security.max_body_size))
485 .layer(cors_layer)
486 .layer(SetResponseHeaderLayer::if_not_present(
487 header::X_CONTENT_TYPE_OPTIONS,
488 HeaderValue::from_static("nosniff"),
489 ))
490 .layer(SetResponseHeaderLayer::if_not_present(
491 header::X_FRAME_OPTIONS,
492 HeaderValue::from_static("DENY"),
493 ))
494 .layer(SetResponseHeaderLayer::if_not_present(
495 header::X_XSS_PROTECTION,
496 HeaderValue::from_static("1; mode=block"),
497 )),
498 )
499}
500
501pub struct ServerBuilder {
537 config: ServerConfig,
538 a2a_base_url: Option<String>,
539 api_routes: Vec<Router>,
540 root_routes: Vec<Router>,
541 shutdown_endpoint: bool,
542}
543
544impl ServerBuilder {
545 pub fn new(config: ServerConfig) -> Self {
547 Self {
548 config,
549 a2a_base_url: None,
550 api_routes: Vec::new(),
551 root_routes: Vec::new(),
552 shutdown_endpoint: false,
553 }
554 }
555
556 pub fn add_api_routes(mut self, routes: Router) -> Self {
571 self.api_routes.push(routes);
572 self
573 }
574
575 pub fn add_root_routes(mut self, routes: Router) -> Self {
583 self.root_routes.push(routes);
584 self
585 }
586
587 pub fn with_a2a(mut self, base_url: impl Into<String>) -> Self {
591 self.a2a_base_url = Some(base_url.into());
592 self
593 }
594
595 pub fn enable_shutdown_endpoint(mut self) -> Self {
605 self.shutdown_endpoint = true;
606 self
607 }
608
609 pub fn build(self) -> Router {
611 self.build_inner().0
612 }
613
614 pub fn build_with_shutdown(self) -> (Router, ShutdownHandle) {
632 let (router, handle) = self.build_inner();
633 (router, handle.expect("build_with_shutdown requires enable_shutdown_endpoint()"))
634 }
635
636 fn build_inner(self) -> (Router, Option<ShutdownHandle>) {
637 let config = &self.config;
638 let session_controller = SessionController::new(config.session_service.clone());
639 let runtime_controller = RuntimeController::new(config.clone());
640 let apps_controller = AppsController::new(config.clone());
641 let artifacts_controller = ArtifactsController::new(config.clone());
642 let debug_controller = DebugController::new(config.clone());
643 let health_controller = HealthController::new(config);
644
645 #[cfg(feature = "yaml-agent")]
647 {
648 let dirs = config.yaml_agent_dirs.clone();
649 if !dirs.is_empty() {
650 tokio::spawn(async move {
651 let _watchers = start_yaml_agent_watchers(&dirs).await;
652 std::future::pending::<()>().await;
653 });
654 }
655 }
656
657 let auth_layer = middleware::from_fn({
658 let extractor = config.request_context_extractor.clone();
659 move |request: Request<Body>, next: Next| {
660 let extractor = extractor.clone();
661 async move { auth_middleware(request, next, extractor).await }
662 }
663 });
664
665 let health_router =
666 Router::new().route("/health", get(health_check)).with_state(health_controller);
667
668 let ui_api_router = Router::new()
669 .route("/apps", get(controllers::apps::list_apps))
670 .route("/list-apps", get(controllers::apps::list_apps_compat))
671 .with_state(apps_controller)
672 .route("/ui/capabilities", get(controllers::ui::ui_capabilities))
673 .route("/ui/initialize", post(controllers::ui::ui_initialize))
674 .route("/ui/message", post(controllers::ui::ui_message))
675 .route("/ui/update-model-context", post(controllers::ui::ui_update_model_context))
676 .route("/ui/notifications/poll", post(controllers::ui::ui_poll_notifications))
677 .route(
678 "/ui/notifications/resources-list-changed",
679 post(controllers::ui::ui_notify_resources_list_changed),
680 )
681 .route(
682 "/ui/notifications/tools-list-changed",
683 post(controllers::ui::ui_notify_tools_list_changed),
684 )
685 .route("/ui/resources", get(controllers::ui::list_ui_resources))
686 .route("/ui/resources/read", get(controllers::ui::read_ui_resource))
687 .route("/ui/resources/register", post(controllers::ui::register_ui_resource))
688 .layer(auth_layer.clone());
691
692 let session_router = Router::new()
693 .route("/sessions", post(controllers::session::create_session))
694 .route(
695 "/sessions/{app_name}/{user_id}/{session_id}",
696 get(controllers::session::get_session).delete(controllers::session::delete_session),
697 )
698 .route(
699 "/apps/{app_name}/users/{user_id}/sessions",
700 get(controllers::session::list_sessions)
701 .post(controllers::session::create_session_from_path),
702 )
703 .route(
704 "/apps/{app_name}/users/{user_id}/sessions/{session_id}",
705 get(controllers::session::get_session_from_path)
706 .post(controllers::session::create_session_from_path)
707 .delete(controllers::session::delete_session_from_path),
708 )
709 .with_state(session_controller)
710 .layer(auth_layer.clone());
711
712 let runtime_router = Router::new()
713 .route("/run/{app_name}/{user_id}/{session_id}", post(controllers::runtime::run_sse))
714 .route("/run_sse", post(controllers::runtime::run_sse_compat))
715 .with_state(runtime_controller);
716
717 let artifacts_router = Router::new()
718 .route(
719 "/sessions/{app_name}/{user_id}/{session_id}/artifacts",
720 get(controllers::artifacts::list_artifacts),
721 )
722 .route(
723 "/sessions/{app_name}/{user_id}/{session_id}/artifacts/{artifact_name}",
724 get(controllers::artifacts::get_artifact),
725 )
726 .with_state(artifacts_controller)
727 .layer(auth_layer.clone());
728
729 let mut debug_router = Router::new()
730 .route("/debug/trace/session/{session_id}", get(controllers::debug::get_session_traces))
731 .route(
732 "/debug/graph/{app_name}/{user_id}/{session_id}/{event_id}",
733 get(controllers::debug::get_graph),
734 )
735 .route(
736 "/apps/{app_name}/users/{user_id}/sessions/{session_id}/events/{event_id}/graph",
737 get(controllers::debug::get_graph),
738 )
739 .route("/apps/{app_name}/eval_sets", get(controllers::debug::get_eval_sets))
740 .route(
741 "/apps/{app_name}/users/{user_id}/sessions/{session_id}/events/{event_id}",
742 get(controllers::debug::get_event),
743 );
744
745 if config.request_context_extractor.is_none() || config.security.expose_admin_debug {
746 debug_router = debug_router
747 .route("/debug/trace/{event_id}", get(controllers::debug::get_trace_by_event_id));
748 }
749
750 let debug_router =
751 debug_router.with_state(debug_controller.clone()).layer(auth_layer.clone());
752
753 let mut api_router = Router::new()
755 .merge(health_router)
756 .merge(ui_api_router)
757 .merge(session_router)
758 .merge(runtime_router)
759 .merge(artifacts_router)
760 .merge(debug_router);
761
762 for custom_routes in self.api_routes {
764 api_router = api_router.merge(custom_routes.layer(auth_layer.clone()));
765 }
766
767 let shutdown_handle = if self.shutdown_endpoint {
769 let handle = ShutdownHandle::new();
770 let shutdown_router = Router::new()
771 .route("/shutdown", post(handle_shutdown))
772 .with_state(handle.token.clone())
773 .layer(auth_layer.clone());
774 api_router = api_router.merge(shutdown_router);
775 Some(handle)
776 } else {
777 None
778 };
779
780 let ui_router = Router::new()
781 .route("/", get(web_ui::root_redirect))
782 .route("/ui/", get(web_ui::serve_ui_index))
783 .route("/ui/assets/config/runtime-config.json", get(web_ui::serve_runtime_config))
784 .with_state(config.clone())
785 .route("/ui/{*path}", get(web_ui::serve_ui_assets));
786
787 let mut app = Router::new().nest("/api", api_router).merge(ui_router);
788
789 for custom_routes in self.root_routes {
791 app = app.merge(custom_routes);
792 }
793
794 if let Some(base_url) = &self.a2a_base_url {
795 let a2a_controller = A2aController::new(config.clone(), base_url);
796 let a2a_discovery = Router::new()
798 .route("/.well-known/agent.json", get(controllers::a2a::get_agent_card))
799 .with_state(a2a_controller.clone());
800 let a2a_rpc = Router::new()
801 .route("/a2a", post(controllers::a2a::handle_jsonrpc))
802 .route("/a2a/stream", post(controllers::a2a::handle_jsonrpc_stream))
803 .with_state(a2a_controller)
804 .layer(auth_layer.clone());
805 app = app.merge(a2a_discovery).merge(a2a_rpc);
806 }
807
808 let cors_layer = build_cors_layer(config);
809 let trace_layer = TraceLayer::new_for_http().make_span_with(|request: &Request<Body>| {
810 let request_id =
811 request.extensions().get::<RequestId>().map(RequestId::as_str).unwrap_or("");
812 tracing::info_span!(
813 "http.request",
814 request.id = %request_id,
815 http.method = %request.method(),
816 http.path = %request.uri().path()
817 )
818 });
819
820 (
821 app.layer(
822 ServiceBuilder::new()
823 .layer(middleware::from_fn(request_id_middleware))
824 .layer(trace_layer)
825 .layer(TimeoutLayer::with_status_code(
826 StatusCode::REQUEST_TIMEOUT,
827 config.security.request_timeout,
828 ))
829 .layer(DefaultBodyLimit::max(config.security.max_body_size))
830 .layer(cors_layer)
831 .layer(SetResponseHeaderLayer::if_not_present(
832 header::X_CONTENT_TYPE_OPTIONS,
833 HeaderValue::from_static("nosniff"),
834 ))
835 .layer(SetResponseHeaderLayer::if_not_present(
836 header::X_FRAME_OPTIONS,
837 HeaderValue::from_static("DENY"),
838 ))
839 .layer(SetResponseHeaderLayer::if_not_present(
840 header::X_XSS_PROTECTION,
841 HeaderValue::from_static("1; mode=block"),
842 )),
843 ),
844 shutdown_handle,
845 )
846 }
847}
848
849pub async fn shutdown_signal() {
851 let ctrl_c = async {
852 let _ = tokio::signal::ctrl_c().await;
853 };
854
855 #[cfg(unix)]
856 let terminate = async {
857 if let Ok(mut signal) =
858 tokio::signal::unix::signal(tokio::signal::unix::SignalKind::terminate())
859 {
860 let _ = signal.recv().await;
861 }
862 };
863
864 #[cfg(not(unix))]
865 let terminate = std::future::pending::<()>();
866
867 tokio::select! {
868 _ = ctrl_c => {}
869 _ = terminate => {}
870 }
871}
872
873#[derive(Clone)]
898pub struct ShutdownHandle {
899 token: CancellationToken,
900}
901
902impl ShutdownHandle {
903 fn new() -> Self {
905 Self { token: CancellationToken::new() }
906 }
907
908 pub fn shutdown(&self) {
913 tracing::info!("graceful shutdown triggered programmatically");
914 self.token.cancel();
915 }
916
917 pub async fn signal(self) {
922 let token = self.token.clone();
923
924 let ctrl_c = async {
925 let _ = tokio::signal::ctrl_c().await;
926 };
927
928 #[cfg(unix)]
929 let terminate = async {
930 if let Ok(mut signal) =
931 tokio::signal::unix::signal(tokio::signal::unix::SignalKind::terminate())
932 {
933 let _ = signal.recv().await;
934 }
935 };
936
937 #[cfg(not(unix))]
938 let terminate = std::future::pending::<()>();
939
940 tokio::select! {
941 _ = ctrl_c => {
942 tracing::info!("received Ctrl+C, initiating graceful shutdown");
943 }
944 _ = terminate => {
945 tracing::info!("received SIGTERM, initiating graceful shutdown");
946 }
947 _ = token.cancelled() => {
948 }
950 }
951 }
952
953 pub fn is_shutdown(&self) -> bool {
955 self.token.is_cancelled()
956 }
957}
958
959async fn handle_shutdown(State(token): State<CancellationToken>) -> impl IntoResponse {
964 tracing::info!("POST /api/shutdown received, initiating graceful shutdown");
965 token.cancel();
966 (StatusCode::OK, Json(serde_json::json!({ "status": "shutting_down" })))
967}