[go: up one dir, main page]

forge-core-server 0.8.7-rc.35

HTTP server for Forge - REST API, WebSocket streaming, and MCP integration
Documentation
use axum::{
    Json,
    extract::multipart::MultipartError,
    http::StatusCode,
    response::{IntoResponse, Response},
};
use forge_core_db::models::{
    execution_process::ExecutionProcessError, execution_run::ExecutionRunError,
    project::ProjectError, task_attempt::TaskAttemptError,
};
use forge_core_deployment::DeploymentError;
use forge_core_executors::executors::ExecutorError;
use forge_core_services::services::{
    auth::AuthError, config::ConfigError, container::ContainerError, drafts::DraftsServiceError,
    git::GitServiceError, github_service::GitHubServiceError, image::ImageError,
    worktree_manager::WorktreeError,
};
use forge_core_utils::response::ApiResponse;
use git2::Error as Git2Error;
use thiserror::Error;

#[derive(Debug, Error, ts_rs_forge::TS)]
#[ts(type = "string")]
pub enum ApiError {
    #[error(transparent)]
    Project(#[from] ProjectError),
    #[error(transparent)]
    TaskAttempt(#[from] TaskAttemptError),
    #[error(transparent)]
    ExecutionProcess(#[from] ExecutionProcessError),
    #[error(transparent)]
    ExecutionRun(#[from] ExecutionRunError),
    #[error(transparent)]
    GitService(#[from] GitServiceError),
    #[error(transparent)]
    GitHubService(#[from] GitHubServiceError),
    #[error(transparent)]
    Auth(#[from] AuthError),
    #[error(transparent)]
    Deployment(#[from] DeploymentError),
    #[error(transparent)]
    Container(#[from] ContainerError),
    #[error(transparent)]
    Executor(#[from] ExecutorError),
    #[error(transparent)]
    Database(#[from] sqlx::Error),
    #[error(transparent)]
    Worktree(#[from] WorktreeError),
    #[error(transparent)]
    Config(#[from] ConfigError),
    #[error(transparent)]
    Image(#[from] ImageError),
    #[error(transparent)]
    Drafts(#[from] DraftsServiceError),
    #[error("Multipart error: {0}")]
    Multipart(#[from] MultipartError),
    #[error("IO error: {0}")]
    Io(#[from] std::io::Error),
    #[error("Conflict: {0}")]
    Conflict(String),
}

impl From<Git2Error> for ApiError {
    fn from(err: Git2Error) -> Self {
        ApiError::GitService(GitServiceError::from(err))
    }
}

impl IntoResponse for ApiError {
    fn into_response(self) -> Response {
        let (status_code, error_type) = match &self {
            ApiError::Project(_) => (StatusCode::INTERNAL_SERVER_ERROR, "ProjectError"),
            ApiError::TaskAttempt(_) => (StatusCode::INTERNAL_SERVER_ERROR, "TaskAttemptError"),
            ApiError::ExecutionProcess(err) => match err {
                ExecutionProcessError::ExecutionProcessNotFound => {
                    (StatusCode::NOT_FOUND, "ExecutionProcessError")
                }
                _ => (StatusCode::INTERNAL_SERVER_ERROR, "ExecutionProcessError"),
            },
            ApiError::ExecutionRun(err) => match err {
                ExecutionRunError::ExecutionRunNotFound => {
                    (StatusCode::NOT_FOUND, "ExecutionRunError")
                }
                ExecutionRunError::ProjectNotFound => (StatusCode::NOT_FOUND, "ProjectNotFound"),
                _ => (StatusCode::INTERNAL_SERVER_ERROR, "ExecutionRunError"),
            },
            // Promote certain GitService errors to conflict status with concise messages
            ApiError::GitService(git_err) => match git_err {
                forge_core_services::services::git::GitServiceError::MergeConflicts(_) => {
                    (StatusCode::CONFLICT, "GitServiceError")
                }
                forge_core_services::services::git::GitServiceError::RebaseInProgress => {
                    (StatusCode::CONFLICT, "GitServiceError")
                }
                _ => (StatusCode::INTERNAL_SERVER_ERROR, "GitServiceError"),
            },
            ApiError::GitHubService(_) => (StatusCode::INTERNAL_SERVER_ERROR, "GitHubServiceError"),
            ApiError::Auth(_) => (StatusCode::INTERNAL_SERVER_ERROR, "AuthError"),
            ApiError::Deployment(_) => (StatusCode::INTERNAL_SERVER_ERROR, "DeploymentError"),
            ApiError::Container(_) => (StatusCode::INTERNAL_SERVER_ERROR, "ContainerError"),
            ApiError::Executor(_) => (StatusCode::INTERNAL_SERVER_ERROR, "ExecutorError"),
            ApiError::Database(_) => (StatusCode::INTERNAL_SERVER_ERROR, "DatabaseError"),
            ApiError::Worktree(_) => (StatusCode::INTERNAL_SERVER_ERROR, "WorktreeError"),
            ApiError::Config(_) => (StatusCode::INTERNAL_SERVER_ERROR, "ConfigError"),
            ApiError::Image(img_err) => match img_err {
                ImageError::InvalidFormat => (StatusCode::BAD_REQUEST, "InvalidImageFormat"),
                ImageError::TooLarge(_, _) => (StatusCode::PAYLOAD_TOO_LARGE, "ImageTooLarge"),
                ImageError::NotFound => (StatusCode::NOT_FOUND, "ImageNotFound"),
                _ => (StatusCode::INTERNAL_SERVER_ERROR, "ImageError"),
            },
            ApiError::Drafts(drafts_err) => match drafts_err {
                DraftsServiceError::Conflict(_) => (StatusCode::CONFLICT, "ConflictError"),
                DraftsServiceError::Database(_) => {
                    (StatusCode::INTERNAL_SERVER_ERROR, "DatabaseError")
                }
                DraftsServiceError::Container(_) => {
                    (StatusCode::INTERNAL_SERVER_ERROR, "ContainerError")
                }
                DraftsServiceError::Image(_) => (StatusCode::INTERNAL_SERVER_ERROR, "ImageError"),
                DraftsServiceError::ExecutionProcess(_) => {
                    (StatusCode::INTERNAL_SERVER_ERROR, "ExecutionProcessError")
                }
            },
            ApiError::Io(_) => (StatusCode::INTERNAL_SERVER_ERROR, "IoError"),
            ApiError::Multipart(_) => (StatusCode::BAD_REQUEST, "MultipartError"),
            ApiError::Conflict(_) => (StatusCode::CONFLICT, "ConflictError"),
        };

        let error_message = match &self {
            ApiError::Image(img_err) => match img_err {
                ImageError::InvalidFormat => "This file type is not supported. Please upload an image file (PNG, JPG, GIF, WebP, or BMP).".to_string(),
                ImageError::TooLarge(size, max) => format!(
                    "This image is too large ({:.1} MB). Maximum file size is {:.1} MB.",
                    *size as f64 / 1_048_576.0,
                    *max as f64 / 1_048_576.0
                ),
                ImageError::NotFound => "Image not found.".to_string(),
                _ => {
                    "Failed to process image. Please try again.".to_string()
                }
            },
            ApiError::GitService(git_err) => match git_err {
                forge_core_services::services::git::GitServiceError::MergeConflicts(msg) => msg.clone(),
                forge_core_services::services::git::GitServiceError::RebaseInProgress => {
                    "A rebase is already in progress. Resolve conflicts or abort the rebase, then retry.".to_string()
                }
                _ => format!("{}: {}", error_type, self),
            },
            ApiError::Multipart(_) => "Failed to upload file. Please ensure the file is valid and try again.".to_string(),
            ApiError::Conflict(msg) => msg.clone(),
            ApiError::Drafts(drafts_err) => match drafts_err {
                DraftsServiceError::Conflict(msg) => msg.clone(),
                DraftsServiceError::Database(_) => format!("{}: {}", error_type, drafts_err),
                DraftsServiceError::Container(_) => format!("{}: {}", error_type, drafts_err),
                DraftsServiceError::Image(_) => format!("{}: {}", error_type, drafts_err),
                DraftsServiceError::ExecutionProcess(_) => {
                    format!("{}: {}", error_type, drafts_err)
                }
            },
            _ => format!("{}: {}", error_type, self),
        };
        let response = ApiResponse::<()>::error(&error_message);
        (status_code, Json(response)).into_response()
    }
}