forge_core_server/
error.rs

1use axum::{
2    Json,
3    extract::multipart::MultipartError,
4    http::StatusCode,
5    response::{IntoResponse, Response},
6};
7use forge_core_db::models::{
8    execution_process::ExecutionProcessError, execution_run::ExecutionRunError,
9    project::ProjectError, task_attempt::TaskAttemptError,
10};
11use forge_core_deployment::DeploymentError;
12use forge_core_executors::executors::ExecutorError;
13use forge_core_services::services::{
14    auth::AuthError, config::ConfigError, container::ContainerError, drafts::DraftsServiceError,
15    git::GitServiceError, github_service::GitHubServiceError, image::ImageError,
16    worktree_manager::WorktreeError,
17};
18use forge_core_utils::response::ApiResponse;
19use git2::Error as Git2Error;
20use thiserror::Error;
21
22#[derive(Debug, Error, ts_rs_forge::TS)]
23#[ts(type = "string")]
24pub enum ApiError {
25    #[error(transparent)]
26    Project(#[from] ProjectError),
27    #[error(transparent)]
28    TaskAttempt(#[from] TaskAttemptError),
29    #[error(transparent)]
30    ExecutionProcess(#[from] ExecutionProcessError),
31    #[error(transparent)]
32    ExecutionRun(#[from] ExecutionRunError),
33    #[error(transparent)]
34    GitService(#[from] GitServiceError),
35    #[error(transparent)]
36    GitHubService(#[from] GitHubServiceError),
37    #[error(transparent)]
38    Auth(#[from] AuthError),
39    #[error(transparent)]
40    Deployment(#[from] DeploymentError),
41    #[error(transparent)]
42    Container(#[from] ContainerError),
43    #[error(transparent)]
44    Executor(#[from] ExecutorError),
45    #[error(transparent)]
46    Database(#[from] sqlx::Error),
47    #[error(transparent)]
48    Worktree(#[from] WorktreeError),
49    #[error(transparent)]
50    Config(#[from] ConfigError),
51    #[error(transparent)]
52    Image(#[from] ImageError),
53    #[error(transparent)]
54    Drafts(#[from] DraftsServiceError),
55    #[error("Multipart error: {0}")]
56    Multipart(#[from] MultipartError),
57    #[error("IO error: {0}")]
58    Io(#[from] std::io::Error),
59    #[error("Conflict: {0}")]
60    Conflict(String),
61}
62
63impl From<Git2Error> for ApiError {
64    fn from(err: Git2Error) -> Self {
65        ApiError::GitService(GitServiceError::from(err))
66    }
67}
68
69impl IntoResponse for ApiError {
70    fn into_response(self) -> Response {
71        let (status_code, error_type) = match &self {
72            ApiError::Project(_) => (StatusCode::INTERNAL_SERVER_ERROR, "ProjectError"),
73            ApiError::TaskAttempt(_) => (StatusCode::INTERNAL_SERVER_ERROR, "TaskAttemptError"),
74            ApiError::ExecutionProcess(err) => match err {
75                ExecutionProcessError::ExecutionProcessNotFound => {
76                    (StatusCode::NOT_FOUND, "ExecutionProcessError")
77                }
78                _ => (StatusCode::INTERNAL_SERVER_ERROR, "ExecutionProcessError"),
79            },
80            ApiError::ExecutionRun(err) => match err {
81                ExecutionRunError::ExecutionRunNotFound => {
82                    (StatusCode::NOT_FOUND, "ExecutionRunError")
83                }
84                ExecutionRunError::ProjectNotFound => (StatusCode::NOT_FOUND, "ProjectNotFound"),
85                _ => (StatusCode::INTERNAL_SERVER_ERROR, "ExecutionRunError"),
86            },
87            // Promote certain GitService errors to conflict status with concise messages
88            ApiError::GitService(git_err) => match git_err {
89                forge_core_services::services::git::GitServiceError::MergeConflicts(_) => {
90                    (StatusCode::CONFLICT, "GitServiceError")
91                }
92                forge_core_services::services::git::GitServiceError::RebaseInProgress => {
93                    (StatusCode::CONFLICT, "GitServiceError")
94                }
95                _ => (StatusCode::INTERNAL_SERVER_ERROR, "GitServiceError"),
96            },
97            ApiError::GitHubService(_) => (StatusCode::INTERNAL_SERVER_ERROR, "GitHubServiceError"),
98            ApiError::Auth(_) => (StatusCode::INTERNAL_SERVER_ERROR, "AuthError"),
99            ApiError::Deployment(_) => (StatusCode::INTERNAL_SERVER_ERROR, "DeploymentError"),
100            ApiError::Container(_) => (StatusCode::INTERNAL_SERVER_ERROR, "ContainerError"),
101            ApiError::Executor(_) => (StatusCode::INTERNAL_SERVER_ERROR, "ExecutorError"),
102            ApiError::Database(_) => (StatusCode::INTERNAL_SERVER_ERROR, "DatabaseError"),
103            ApiError::Worktree(_) => (StatusCode::INTERNAL_SERVER_ERROR, "WorktreeError"),
104            ApiError::Config(_) => (StatusCode::INTERNAL_SERVER_ERROR, "ConfigError"),
105            ApiError::Image(img_err) => match img_err {
106                ImageError::InvalidFormat => (StatusCode::BAD_REQUEST, "InvalidImageFormat"),
107                ImageError::TooLarge(_, _) => (StatusCode::PAYLOAD_TOO_LARGE, "ImageTooLarge"),
108                ImageError::NotFound => (StatusCode::NOT_FOUND, "ImageNotFound"),
109                _ => (StatusCode::INTERNAL_SERVER_ERROR, "ImageError"),
110            },
111            ApiError::Drafts(drafts_err) => match drafts_err {
112                DraftsServiceError::Conflict(_) => (StatusCode::CONFLICT, "ConflictError"),
113                DraftsServiceError::Database(_) => {
114                    (StatusCode::INTERNAL_SERVER_ERROR, "DatabaseError")
115                }
116                DraftsServiceError::Container(_) => {
117                    (StatusCode::INTERNAL_SERVER_ERROR, "ContainerError")
118                }
119                DraftsServiceError::Image(_) => (StatusCode::INTERNAL_SERVER_ERROR, "ImageError"),
120                DraftsServiceError::ExecutionProcess(_) => {
121                    (StatusCode::INTERNAL_SERVER_ERROR, "ExecutionProcessError")
122                }
123            },
124            ApiError::Io(_) => (StatusCode::INTERNAL_SERVER_ERROR, "IoError"),
125            ApiError::Multipart(_) => (StatusCode::BAD_REQUEST, "MultipartError"),
126            ApiError::Conflict(_) => (StatusCode::CONFLICT, "ConflictError"),
127        };
128
129        let error_message = match &self {
130            ApiError::Image(img_err) => match img_err {
131                ImageError::InvalidFormat => "This file type is not supported. Please upload an image file (PNG, JPG, GIF, WebP, or BMP).".to_string(),
132                ImageError::TooLarge(size, max) => format!(
133                    "This image is too large ({:.1} MB). Maximum file size is {:.1} MB.",
134                    *size as f64 / 1_048_576.0,
135                    *max as f64 / 1_048_576.0
136                ),
137                ImageError::NotFound => "Image not found.".to_string(),
138                _ => {
139                    "Failed to process image. Please try again.".to_string()
140                }
141            },
142            ApiError::GitService(git_err) => match git_err {
143                forge_core_services::services::git::GitServiceError::MergeConflicts(msg) => msg.clone(),
144                forge_core_services::services::git::GitServiceError::RebaseInProgress => {
145                    "A rebase is already in progress. Resolve conflicts or abort the rebase, then retry.".to_string()
146                }
147                _ => format!("{}: {}", error_type, self),
148            },
149            ApiError::Multipart(_) => "Failed to upload file. Please ensure the file is valid and try again.".to_string(),
150            ApiError::Conflict(msg) => msg.clone(),
151            ApiError::Drafts(drafts_err) => match drafts_err {
152                DraftsServiceError::Conflict(msg) => msg.clone(),
153                DraftsServiceError::Database(_) => format!("{}: {}", error_type, drafts_err),
154                DraftsServiceError::Container(_) => format!("{}: {}", error_type, drafts_err),
155                DraftsServiceError::Image(_) => format!("{}: {}", error_type, drafts_err),
156                DraftsServiceError::ExecutionProcess(_) => {
157                    format!("{}: {}", error_type, drafts_err)
158                }
159            },
160            _ => format!("{}: {}", error_type, self),
161        };
162        let response = ApiResponse::<()>::error(&error_message);
163        (status_code, Json(response)).into_response()
164    }
165}