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 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}