systemprompt-api 0.49.0

Axum-based HTTP server and API gateway for systemprompt.io AI governance infrastructure. Exposes governed agents, MCP, A2A, and admin endpoints with rate limiting and RBAC.
Documentation
//! Maps context-extraction failures to API errors.
//!
//! Copyright (c) systemprompt.io — Business Source License 1.1.
//! See <https://systemprompt.io> for licensing details.

use axum::response::{IntoResponse, Response};
use systemprompt_identifiers::TraceId;
use systemprompt_models::api::ApiError;
use systemprompt_models::execution::context::ContextExtractionError;

pub fn extraction_error_to_api_error(error: &ContextExtractionError) -> ApiError {
    match error {
        ContextExtractionError::MissingAuthHeader => {
            ApiError::unauthorized("Missing Authorization header")
        },
        ContextExtractionError::InvalidToken(_) => {
            ApiError::unauthorized("Invalid or expired JWT token")
        },
        ContextExtractionError::Revoked => ApiError::unauthorized("Token revoked"),
        ContextExtractionError::UserNotFound(_) => ApiError::unauthorized("User no longer exists"),
        ContextExtractionError::MissingSessionId => {
            ApiError::bad_request("JWT missing required 'session_id' claim")
        },
        ContextExtractionError::MissingUserId => {
            ApiError::bad_request("JWT missing required 'sub' claim")
        },
        ContextExtractionError::MissingContextId => ApiError::bad_request(
            "Missing required 'x-context-id' header (for MCP routes) or contextId in body (for \
             A2A routes)",
        ),
        ContextExtractionError::MissingHeader(header) => {
            ApiError::bad_request(format!("Missing required header: {header}"))
        },
        ContextExtractionError::InvalidHeaderValue { header, reason } => {
            ApiError::bad_request(format!("Invalid header {header}: {reason}"))
        },
        ContextExtractionError::InvalidUserId(reason) => {
            ApiError::bad_request(format!("Invalid user_id: {reason}"))
        },
        ContextExtractionError::DatabaseError { .. } => {
            ApiError::internal_error("Internal server error")
        },
        ContextExtractionError::ForbiddenHeader { header, reason } => ApiError::bad_request(
            format!("Header '{header}' is not allowed: {reason}. Use JWT authentication instead."),
        ),
    }
}

pub(super) fn log_error_response(
    error: &ContextExtractionError,
    trace_id: &TraceId,
    path: &str,
    method: &str,
) -> Response {
    let _span = tracing::error_span!(
        "context_extraction_error",
        trace_id = %trace_id,
        path = %path,
        method = %method,
    )
    .entered();

    match error {
        ContextExtractionError::DatabaseError { message } => {
            tracing::error!(
                error = %message,
                error_type = "database",
                "Context extraction failed due to database error"
            );
        },
        ContextExtractionError::InvalidToken(reason) => {
            tracing::warn!(
                reason = %reason,
                error_type = "invalid_token",
                "Context extraction failed: invalid token"
            );
        },
        ContextExtractionError::UserNotFound(user_id) => {
            tracing::warn!(
                user_id = %user_id,
                error_type = "user_not_found",
                "Context extraction failed: user not found"
            );
        },
        _ => {
            tracing::warn!(
                error = %error,
                error_type = "context_extraction",
                "Context extraction failed"
            );
        },
    }

    extraction_error_to_api_error(error)
        .with_trace_id(trace_id.as_str())
        .with_path(path)
        .into_response()
}