Skip to main content

systemprompt_api/services/static_content/
fallback.rs

1//! Fallback handler returning JSON 404s for unmatched API paths and static
2//! content otherwise.
3//!
4//! Copyright (c) systemprompt.io — Business Source License 1.1.
5//! See <https://systemprompt.io> for licensing details.
6
7use axum::extract::State;
8use axum::http::{HeaderMap, Method, Uri};
9use axum::response::IntoResponse;
10use systemprompt_models::api::ApiError;
11use systemprompt_models::modules::ApiPaths;
12
13use super::static_files::StaticContentState;
14use crate::error::ApiHttpError;
15
16pub async fn smart_fallback_handler(
17    State(state): State<StaticContentState>,
18    uri: Uri,
19    method: Method,
20    headers: HeaderMap,
21    req_ctx: Option<axum::Extension<systemprompt_models::RequestContext>>,
22) -> impl IntoResponse {
23    let path = uri.path();
24
25    if is_api_path(path) {
26        return ApiHttpError::from(
27            ApiError::not_found(format!("No route matches {method} {path}"))
28                .with_path(path)
29                .with_details(get_api_suggestions(path).join("; ")),
30        )
31        .into_response();
32    }
33
34    super::serve_static_content(State(state), uri, headers, req_ctx)
35        .await
36        .into_response()
37}
38
39pub fn is_api_path(path: &str) -> bool {
40    path.starts_with(ApiPaths::API_BASE)
41        || path.starts_with(ApiPaths::WELLKNOWN_BASE)
42        || path.starts_with("/server/")
43        || path.starts_with("/mcp/")
44        || path.starts_with("/health")
45        || path.starts_with(ApiPaths::OPENAPI_BASE)
46        || path.starts_with(ApiPaths::DOCS_BASE)
47        || path.starts_with(ApiPaths::SWAGGER_BASE)
48        || path.starts_with("/v1/")
49        || path.starts_with("/auth/")
50        || path.starts_with("/oauth/")
51}
52
53pub fn get_api_suggestions(path: &str) -> Vec<String> {
54    if path.starts_with(ApiPaths::API_BASE) {
55        vec![
56            format!("{} - API discovery endpoint", ApiPaths::DISCOVERY),
57            format!("{} - Health check", ApiPaths::HEALTH),
58            format!(
59                "{} - Core services (contexts, tasks, artifacts)",
60                ApiPaths::CORE_BASE
61            ),
62            format!("{} - Agent registry", ApiPaths::AGENTS_REGISTRY),
63            format!("{} - MCP server registry", ApiPaths::MCP_REGISTRY),
64        ]
65    } else if path.starts_with(ApiPaths::WELLKNOWN_BASE) {
66        vec![
67            format!("{} - OAuth metadata", ApiPaths::WELLKNOWN_OAUTH_SERVER),
68            format!("{} - Agent card", ApiPaths::WELLKNOWN_AGENT_CARD),
69        ]
70    } else if path.contains("health") {
71        vec![format!("{} - Health check endpoint", ApiPaths::HEALTH)]
72    } else if path.contains("openapi") || path.contains("swagger") {
73        vec![format!(
74            "{} - API discovery (OpenAPI not yet available)",
75            ApiPaths::DISCOVERY
76        )]
77    } else {
78        vec![
79            format!("{} - Start here for API discovery", ApiPaths::DISCOVERY),
80            "/ - Frontend application".to_owned(),
81        ]
82    }
83}