1use axum::Json;
2use axum::http::{HeaderValue, StatusCode};
3use axum::response::{IntoResponse, Response};
4use platform_core::{AppError, ErrorCode, RequestContext};
5use serde::Serialize;
6use utoipa::ToSchema;
7
8#[derive(Debug, Serialize, ToSchema)]
9#[schema(as = ErrorResponse)]
10pub struct ApiErrorBody {
11 pub error: ErrorBody,
12}
13
14#[derive(Debug, Serialize, ToSchema)]
15pub struct ErrorBody {
16 pub code: String,
17 pub message: String,
18 pub request_id: Option<String>,
19 pub correlation_id: Option<String>,
20 pub details: Vec<ValidationErrorDetail>,
21}
22
23#[derive(Debug, Serialize, ToSchema)]
24pub struct ValidationErrorDetail {
25 pub field: Option<String>,
26 pub reason: String,
27}
28
29pub trait IntoApiError {
30 fn status_code(&self) -> StatusCode;
31}
32
33impl IntoApiError for AppError {
34 fn status_code(&self) -> StatusCode {
35 match self.code {
36 ErrorCode::Validation => StatusCode::BAD_REQUEST,
37 ErrorCode::Unauthorized => StatusCode::UNAUTHORIZED,
38 ErrorCode::Forbidden => StatusCode::FORBIDDEN,
39 ErrorCode::NotFound => StatusCode::NOT_FOUND,
40 ErrorCode::Conflict => StatusCode::CONFLICT,
41 ErrorCode::RateLimited => StatusCode::TOO_MANY_REQUESTS,
42 ErrorCode::ExternalDependency => StatusCode::BAD_GATEWAY,
43 ErrorCode::Internal => StatusCode::INTERNAL_SERVER_ERROR,
44 }
45 }
46}
47
48#[derive(Debug)]
49pub struct ApiErrorResponse {
50 pub error: AppError,
51 pub context: Option<RequestContext>,
52}
53
54impl From<AppError> for ApiErrorResponse {
55 fn from(error: AppError) -> Self {
56 Self {
57 error,
58 context: None,
59 }
60 }
61}
62
63impl ApiErrorResponse {
64 pub fn with_context(error: AppError, context: &RequestContext) -> Self {
65 Self {
66 error,
67 context: Some(context.clone()),
68 }
69 }
70}
71
72impl IntoResponse for ApiErrorResponse {
73 fn into_response(self) -> Response {
74 let error = self.error;
75 let status = error.status_code();
76 let request_id = self.context.as_ref().map(|ctx| ctx.request_id.0.clone());
77 let correlation_id = self
78 .context
79 .as_ref()
80 .map(|ctx| ctx.correlation_id.0.clone());
81 tracing::warn!(
82 error_code = error.code.as_str(),
83 status = status.as_u16(),
84 request_id = request_id.as_deref().unwrap_or(""),
85 correlation_id = correlation_id.as_deref().unwrap_or(""),
86 "HTTP request failed"
87 );
88 let body = ApiErrorBody {
89 error: ErrorBody {
90 code: error.code.as_str().to_owned(),
91 message: error.public_message,
92 request_id,
93 correlation_id,
94 details: error
95 .details
96 .into_iter()
97 .map(|detail| ValidationErrorDetail {
98 field: detail.field,
99 reason: detail.reason,
100 })
101 .collect(),
102 },
103 };
104 let mut response = (status, Json(body)).into_response();
105 if let Ok(value) = HeaderValue::from_str(error.code.as_str()) {
106 response.headers_mut().insert("x-lenso-error-code", value);
107 }
108 response
109 }
110}