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, StatusCode, Uri};
9use axum::response::{IntoResponse, Json};
10use serde_json::json;
11use systemprompt_models::modules::ApiPaths;
12
13use super::static_files::StaticContentState;
14
15pub async fn smart_fallback_handler(
16    State(state): State<StaticContentState>,
17    uri: Uri,
18    method: Method,
19    headers: HeaderMap,
20    req_ctx: Option<axum::Extension<systemprompt_models::RequestContext>>,
21) -> impl IntoResponse {
22    let path = uri.path();
23
24    if is_api_path(path) {
25        return (
26            StatusCode::NOT_FOUND,
27            Json(json!({
28                "error": "Not Found",
29                "message": format!("No route matches {method} {path}"),
30                "path": path,
31                "suggestions": get_api_suggestions(path)
32            })),
33        )
34            .into_response();
35    }
36
37    super::serve_static_content(State(state), uri, headers, req_ctx)
38        .await
39        .into_response()
40}
41
42#[cfg(feature = "test-api")]
43pub mod test_api {
44    #[must_use]
45    pub fn is_api_path(path: &str) -> bool {
46        super::is_api_path(path)
47    }
48
49    #[must_use]
50    pub fn get_api_suggestions(path: &str) -> Vec<String> {
51        super::get_api_suggestions(path)
52    }
53}
54
55fn is_api_path(path: &str) -> bool {
56    path.starts_with(ApiPaths::API_BASE)
57        || path.starts_with(ApiPaths::WELLKNOWN_BASE)
58        || path.starts_with("/server/")
59        || path.starts_with("/mcp/")
60        || path.starts_with("/health")
61        || path.starts_with(ApiPaths::OPENAPI_BASE)
62        || path.starts_with(ApiPaths::DOCS_BASE)
63        || path.starts_with(ApiPaths::SWAGGER_BASE)
64        || path.starts_with("/v1/")
65        || path.starts_with("/auth/")
66        || path.starts_with("/oauth/")
67}
68
69fn get_api_suggestions(path: &str) -> Vec<String> {
70    if path.starts_with(ApiPaths::API_BASE) {
71        vec![
72            format!("{} - API discovery endpoint", ApiPaths::DISCOVERY),
73            format!("{} - Health check", ApiPaths::HEALTH),
74            format!(
75                "{} - Core services (contexts, tasks, artifacts)",
76                ApiPaths::CORE_BASE
77            ),
78            format!("{} - Agent registry", ApiPaths::AGENTS_REGISTRY),
79            format!("{} - MCP server registry", ApiPaths::MCP_REGISTRY),
80        ]
81    } else if path.starts_with(ApiPaths::WELLKNOWN_BASE) {
82        vec![
83            format!("{} - OAuth metadata", ApiPaths::WELLKNOWN_OAUTH_SERVER),
84            format!("{} - Agent card", ApiPaths::WELLKNOWN_AGENT_CARD),
85        ]
86    } else if path.contains("health") {
87        vec![format!("{} - Health check endpoint", ApiPaths::HEALTH)]
88    } else if path.contains("openapi") || path.contains("swagger") {
89        vec![format!(
90            "{} - API discovery (OpenAPI not yet available)",
91            ApiPaths::DISCOVERY
92        )]
93    } else {
94        vec![
95            format!("{} - Start here for API discovery", ApiPaths::DISCOVERY),
96            "/ - Frontend application".to_owned(),
97        ]
98    }
99}