Skip to main content

arc_web/http/
errors.rs

1use actix_web::{HttpResponse, ResponseError};
2use arc_core::command_bus::CommandBusError;
3use arc_core::event_store::EventStoreError;
4
5#[derive(Debug, thiserror::Error)]
6pub enum AppError {
7    #[error("Command failed: {0}")]
8    CommandFailed(#[from] CommandBusError),
9    #[error("Audit failed: {message}")]
10    AuditFailed {
11        status: actix_web::http::StatusCode,
12        message: String,
13    },
14}
15
16// AppError is already handled by derive(thiserror::Error) for Display
17impl ResponseError for AppError {
18    fn error_response(&self) -> HttpResponse {
19        match self {
20            AppError::AuditFailed { status, message } => {
21                HttpResponse::build(*status).json(serde_json::json!({
22                    "error": "AuditFailed",
23                    "message": message
24                }))
25            }
26            AppError::CommandFailed(err) => match err {
27                CommandBusError::HandleFailed { message, .. } => {
28                    HttpResponse::UnprocessableEntity().json(serde_json::json!({
29                        "error": "ValidationFailed",
30                        "message": message
31                    }))
32                }
33                CommandBusError::AppendFailed { source, .. } => match source {
34                    EventStoreError::ConcurrencyConflict { .. } => {
35                        HttpResponse::Conflict().json(serde_json::json!({
36                            "error": "ConcurrencyConflict",
37                            "message": "Resource was modified by another request. Please try again."
38                        }))
39                    }
40                    _ => HttpResponse::InternalServerError().json(serde_json::json!({
41                        "error": "InternalServerError",
42                        "message": "Storage error."
43                    })),
44                },
45                CommandBusError::LoadFailed { .. } => {
46                    HttpResponse::NotFound().json(serde_json::json!({
47                        "error": "NotFound",
48                        "message": "Resource not found."
49                    }))
50                }
51                CommandBusError::InvalidAudit { .. } => {
52                    HttpResponse::BadRequest().json(serde_json::json!({
53                        "error": "InvalidAudit",
54                        "message": "Audit metadata for this request is missing or malformed."
55                    }))
56                }
57                _ => HttpResponse::InternalServerError().json(serde_json::json!({
58                    "error": "InternalServerError",
59                    "message": "An unexpected error occurred."
60                })),
61            },
62        }
63    }
64}
65
66#[cfg(test)]
67mod tests {
68    use super::*;
69    use actix_web::http::StatusCode;
70    use arc_core::audit::AuditError;
71
72    #[test]
73    fn test_invalid_audit_maps_to_bad_request() {
74        let err = AppError::CommandFailed(CommandBusError::InvalidAudit {
75            aggregate_id: "u-1".to_string(),
76            source: AuditError::EmptyActorId,
77        });
78        let resp = err.error_response();
79        assert_eq!(resp.status(), StatusCode::BAD_REQUEST);
80    }
81
82    #[test]
83    fn test_handle_failed_maps_to_unprocessable_entity() {
84        let err = AppError::CommandFailed(CommandBusError::handle_failed("u-1", "bad email"));
85        assert_eq!(
86            err.error_response().status(),
87            StatusCode::UNPROCESSABLE_ENTITY
88        );
89    }
90
91    #[test]
92    fn test_concurrency_conflict_maps_to_conflict() {
93        let err = AppError::CommandFailed(CommandBusError::AppendFailed {
94            aggregate_id: "u-1".to_string(),
95            source: EventStoreError::ConcurrencyConflict {
96                aggregate_id: "u-1".to_string(),
97                expected: 1,
98                actual: 2,
99            },
100        });
101        assert_eq!(err.error_response().status(), StatusCode::CONFLICT);
102    }
103}