Skip to main content

postrust_core/
error.rs

1//! Error types for Postrust.
2//!
3//! Provides comprehensive error handling with HTTP status code mapping.
4
5use http::StatusCode;
6use thiserror::Error;
7
8/// Result type for Postrust operations.
9pub type Result<T> = std::result::Result<T, Error>;
10
11/// Main error type for Postrust.
12#[derive(Error, Debug)]
13pub enum Error {
14    // ========================================================================
15    // Request Parsing Errors (4xx)
16    // ========================================================================
17    #[error("Invalid path: {0}")]
18    InvalidPath(String),
19
20    #[error("Invalid query parameter: {0}")]
21    InvalidQueryParam(String),
22
23    #[error("Invalid header: {0}")]
24    InvalidHeader(&'static str),
25
26    #[error("Invalid request body: {0}")]
27    InvalidBody(String),
28
29    #[error("Unsupported HTTP method: {0}")]
30    UnsupportedMethod(String),
31
32    #[error("Unacceptable schema: {0}")]
33    UnacceptableSchema(String),
34
35    #[error("Unknown column: {0}")]
36    UnknownColumn(String),
37
38    #[error("Invalid range: {0}")]
39    InvalidRange(String),
40
41    #[error("Invalid media type: {0}")]
42    InvalidMediaType(String),
43
44    #[error("Missing required parameter: {0}")]
45    MissingParameter(String),
46
47    #[error("Ambiguous request: {0}")]
48    AmbiguousRequest(String),
49
50    // ========================================================================
51    // Authentication/Authorization Errors (401/403)
52    // ========================================================================
53    #[error("Invalid JWT: {0}")]
54    InvalidJwt(String),
55
56    #[error("JWT expired")]
57    JwtExpired,
58
59    #[error("Missing authentication")]
60    MissingAuth,
61
62    #[error("Insufficient permissions: {0}")]
63    InsufficientPermissions(String),
64
65    // ========================================================================
66    // Resource Errors (404)
67    // ========================================================================
68    #[error("Resource not found: {0}")]
69    NotFound(String),
70
71    #[error("Table not found: {0}")]
72    TableNotFound(String),
73
74    #[error("Function not found: {0}")]
75    FunctionNotFound(String),
76
77    #[error("Column not found: {0}")]
78    ColumnNotFound(String),
79
80    #[error("Relationship not found: {0}")]
81    RelationshipNotFound(String),
82
83    // ========================================================================
84    // Schema Cache Errors
85    // ========================================================================
86    #[error("Schema cache not loaded")]
87    SchemaCacheNotLoaded,
88
89    #[error("Schema cache load failed: {0}")]
90    SchemaCacheLoadFailed(String),
91
92    // ========================================================================
93    // Database Errors (500/4xx depending on type)
94    // ========================================================================
95    #[error("Database error: {0}")]
96    Database(#[from] DatabaseError),
97
98    #[error("Connection pool error: {0}")]
99    ConnectionPool(String),
100
101    // ========================================================================
102    // Internal Errors (500)
103    // ========================================================================
104    #[error("Internal error: {0}")]
105    Internal(String),
106
107    #[error("Configuration error: {0}")]
108    Config(String),
109
110    // ========================================================================
111    // Plan Errors
112    // ========================================================================
113    #[error("Invalid plan: {0}")]
114    InvalidPlan(String),
115
116    #[error("Embedding error: {0}")]
117    EmbeddingError(String),
118}
119
120impl Error {
121    /// Get the HTTP status code for this error.
122    pub fn status_code(&self) -> StatusCode {
123        match self {
124            // 400 Bad Request
125            Self::InvalidPath(_)
126            | Self::InvalidQueryParam(_)
127            | Self::InvalidHeader(_)
128            | Self::InvalidBody(_)
129            | Self::InvalidRange(_)
130            | Self::InvalidMediaType(_)
131            | Self::MissingParameter(_)
132            | Self::AmbiguousRequest(_)
133            | Self::UnknownColumn(_)
134            | Self::InvalidPlan(_)
135            | Self::EmbeddingError(_) => StatusCode::BAD_REQUEST,
136
137            // 401 Unauthorized
138            Self::InvalidJwt(_) | Self::JwtExpired | Self::MissingAuth => StatusCode::UNAUTHORIZED,
139
140            // 403 Forbidden
141            Self::InsufficientPermissions(_) => StatusCode::FORBIDDEN,
142
143            // 404 Not Found
144            Self::NotFound(_)
145            | Self::TableNotFound(_)
146            | Self::FunctionNotFound(_)
147            | Self::ColumnNotFound(_)
148            | Self::RelationshipNotFound(_) => StatusCode::NOT_FOUND,
149
150            // 405 Method Not Allowed
151            Self::UnsupportedMethod(_) => StatusCode::METHOD_NOT_ALLOWED,
152
153            // 406 Not Acceptable
154            Self::UnacceptableSchema(_) => StatusCode::NOT_ACCEPTABLE,
155
156            // 500 Internal Server Error
157            Self::SchemaCacheNotLoaded
158            | Self::SchemaCacheLoadFailed(_)
159            | Self::ConnectionPool(_)
160            | Self::Internal(_)
161            | Self::Config(_) => StatusCode::INTERNAL_SERVER_ERROR,
162
163            // Database errors map based on type
164            Self::Database(db_err) => db_err.status_code(),
165        }
166    }
167
168    /// Get the error code for API responses.
169    pub fn code(&self) -> &'static str {
170        match self {
171            Self::InvalidPath(_) => "PGRST100",
172            Self::InvalidQueryParam(_) => "PGRST101",
173            Self::InvalidHeader(_) => "PGRST102",
174            Self::InvalidBody(_) => "PGRST103",
175            Self::UnsupportedMethod(_) => "PGRST104",
176            Self::UnacceptableSchema(_) => "PGRST105",
177            Self::UnknownColumn(_) => "PGRST106",
178            Self::InvalidRange(_) => "PGRST107",
179            Self::InvalidMediaType(_) => "PGRST108",
180            Self::MissingParameter(_) => "PGRST109",
181            Self::AmbiguousRequest(_) => "PGRST110",
182
183            Self::InvalidJwt(_) => "PGRST200",
184            Self::JwtExpired => "PGRST201",
185            Self::MissingAuth => "PGRST202",
186            Self::InsufficientPermissions(_) => "PGRST203",
187
188            Self::NotFound(_) => "PGRST300",
189            Self::TableNotFound(_) => "PGRST301",
190            Self::FunctionNotFound(_) => "PGRST302",
191            Self::ColumnNotFound(_) => "PGRST303",
192            Self::RelationshipNotFound(_) => "PGRST304",
193
194            Self::SchemaCacheNotLoaded => "PGRST400",
195            Self::SchemaCacheLoadFailed(_) => "PGRST401",
196
197            Self::Database(e) => e.code(),
198            Self::ConnectionPool(_) => "PGRST500",
199
200            Self::Internal(_) => "PGRST900",
201            Self::Config(_) => "PGRST901",
202
203            Self::InvalidPlan(_) => "PGRST600",
204            Self::EmbeddingError(_) => "PGRST601",
205        }
206    }
207
208    /// Convert to JSON error response.
209    pub fn to_json(&self) -> serde_json::Value {
210        serde_json::json!({
211            "code": self.code(),
212            "message": self.to_string(),
213            "details": self.details(),
214            "hint": self.hint(),
215        })
216    }
217
218    /// Get additional details for the error.
219    fn details(&self) -> Option<String> {
220        match self {
221            Self::Database(db_err) => db_err.details.clone(),
222            _ => None,
223        }
224    }
225
226    /// Get a hint for resolving the error.
227    fn hint(&self) -> Option<String> {
228        match self {
229            Self::InvalidJwt(_) => {
230                Some("Check that the JWT is properly signed and not expired".into())
231            }
232            Self::MissingAuth => Some("Provide a valid JWT in the Authorization header".into()),
233            Self::TableNotFound(_) => Some("Check the table name and schema".into()),
234            Self::UnknownColumn(_) => Some("Check column names against the table schema".into()),
235            Self::Database(db_err) => db_err.hint.clone(),
236            _ => None,
237        }
238    }
239}
240
241/// Database-specific error type.
242#[derive(Error, Debug)]
243#[error("Database error [{code}]: {message}")]
244pub struct DatabaseError {
245    pub code: String,
246    pub message: String,
247    pub details: Option<String>,
248    pub hint: Option<String>,
249    pub constraint: Option<String>,
250    pub table: Option<String>,
251    pub column: Option<String>,
252}
253
254impl DatabaseError {
255    /// Get HTTP status code based on PostgreSQL error code.
256    pub fn status_code(&self) -> StatusCode {
257        // PostgreSQL error codes: https://www.postgresql.org/docs/current/errcodes-appendix.html
258        match self.code.as_str() {
259            // Class 23 - Integrity Constraint Violation
260            c if c.starts_with("23") => StatusCode::CONFLICT,
261            // Class 42 - Syntax Error or Access Rule Violation
262            c if c.starts_with("42") => StatusCode::BAD_REQUEST,
263            // Class 28 - Invalid Authorization Specification
264            c if c.starts_with("28") => StatusCode::FORBIDDEN,
265            // Class 40 - Transaction Rollback
266            c if c.starts_with("40") => StatusCode::CONFLICT,
267            // Class 53 - Insufficient Resources
268            c if c.starts_with("53") => StatusCode::SERVICE_UNAVAILABLE,
269            // Class 54 - Program Limit Exceeded
270            c if c.starts_with("54") => StatusCode::PAYLOAD_TOO_LARGE,
271            // Class P0 - PL/pgSQL Errors (custom raised errors)
272            "P0001" => StatusCode::BAD_REQUEST, // RAISE EXCEPTION
273            // Default to internal server error
274            _ => StatusCode::INTERNAL_SERVER_ERROR,
275        }
276    }
277
278    /// Get error code for API response.
279    pub fn code(&self) -> &'static str {
280        match self.code.as_str() {
281            c if c.starts_with("23") => "PGRST503", // Constraint violation
282            c if c.starts_with("42") => "PGRST504", // SQL error
283            c if c.starts_with("28") => "PGRST505", // Auth error
284            _ => "PGRST500",                        // Generic database error
285        }
286    }
287}
288
289#[cfg(test)]
290mod tests {
291    use super::*;
292
293    #[test]
294    fn test_error_status_codes() {
295        assert_eq!(
296            Error::InvalidQueryParam("test".into()).status_code(),
297            StatusCode::BAD_REQUEST
298        );
299        assert_eq!(Error::MissingAuth.status_code(), StatusCode::UNAUTHORIZED);
300        assert_eq!(
301            Error::TableNotFound("users".into()).status_code(),
302            StatusCode::NOT_FOUND
303        );
304        assert_eq!(
305            Error::UnsupportedMethod("TRACE".into()).status_code(),
306            StatusCode::METHOD_NOT_ALLOWED
307        );
308    }
309
310    #[test]
311    fn test_error_codes() {
312        assert_eq!(Error::InvalidQueryParam("test".into()).code(), "PGRST101");
313        assert_eq!(Error::MissingAuth.code(), "PGRST202");
314        assert_eq!(Error::TableNotFound("users".into()).code(), "PGRST301");
315    }
316
317    #[test]
318    fn test_database_error_status() {
319        let constraint_error = DatabaseError {
320            code: "23505".into(), // unique_violation
321            message: "Duplicate key".into(),
322            details: None,
323            hint: None,
324            constraint: Some("users_pkey".into()),
325            table: Some("users".into()),
326            column: None,
327        };
328        assert_eq!(constraint_error.status_code(), StatusCode::CONFLICT);
329    }
330
331    #[test]
332    fn test_error_to_json() {
333        let error = Error::InvalidQueryParam("bad filter".into());
334        let json = error.to_json();
335        assert_eq!(json["code"], "PGRST101");
336        assert!(json["message"].as_str().unwrap().contains("bad filter"));
337    }
338}