headless_lms_server/domain/
error.rs

1/*!
2Contains error and result types for all the controllers.
3*/
4
5use crate::domain::authorization::AuthorizedResponse;
6use actix_web::{
7    HttpResponse, HttpResponseBuilder, error,
8    http::{StatusCode, header::ContentType},
9};
10use backtrace::Backtrace;
11use derive_more::Display;
12use dpop_verifier::error::DpopError;
13use headless_lms_base::error::{
14    backend_error::BackendError, backtrace_formatter::format_backtrace,
15};
16use headless_lms_chatbot::prelude::ChatbotError;
17use headless_lms_models::{ModelError, ModelErrorType};
18use headless_lms_utils::error::util_error::UtilError;
19use serde::{Deserialize, Serialize};
20use tracing_error::SpanTrace;
21
22use uuid::Uuid;
23
24const MISSING_EXERCISE_TYPE_DESCRIPTION: &str = "Missing exercise type for exercise task.";
25
26/**
27Used as the result types for all controllers.
28Only put information here that you want to be visible to users.
29
30See also [ControllerError] for documentation on how to return errors from controllers.
31*/
32pub type ControllerResult<T, E = ControllerError> = std::result::Result<AuthorizedResponse<T>, E>;
33
34/// The type of [ControllerError] that occured.
35#[derive(Debug, Display, Serialize, Deserialize)]
36pub enum ControllerErrorType {
37    /// HTTP status code 500.
38    #[display("Internal server error")]
39    InternalServerError,
40
41    /// HTTP status code 400.
42    #[display("Bad request")]
43    BadRequest,
44
45    /// HTTP status code 400.
46    #[display("Bad request")]
47    BadRequestWithData(ErrorMetadata),
48
49    /// HTTP status code 404.
50    #[display("Not found")]
51    NotFound,
52
53    /// HTTP status code 401. Needs to log in.
54    #[display("Unauthorized")]
55    Unauthorized,
56
57    /// HTTP status code 401 with a specific domain reason.
58    #[display("Unauthorized")]
59    UnauthorizedWithReason(UnauthorizedReason),
60
61    /// HTTP status code 403. Is logged in but is not allowed to access the resource.
62    #[display("Forbidden")]
63    Forbidden,
64
65    /// Varied response based on error
66    #[display("OAuthError")]
67    OAuthError(Box<OAuthErrorData>),
68}
69
70#[derive(Debug, Display, Serialize, Deserialize, Clone, Copy, PartialEq, Eq)]
71#[serde(rename_all = "snake_case")]
72pub enum UnauthorizedReason {
73    #[display("Chapter not open yet")]
74    ChapterNotOpenYet,
75    #[display("Authentication required for exam exercise")]
76    AuthenticationRequiredForExamExercise,
77}
78
79impl UnauthorizedReason {
80    /// Returns the stable message key for this unauthorized reason.
81    fn message_key(self) -> &'static str {
82        match self {
83            Self::ChapterNotOpenYet => "chapter_not_open_yet",
84            Self::AuthenticationRequiredForExamExercise => {
85                "authentication_required_for_exam_exercise"
86            }
87        }
88    }
89}
90
91/**
92Represents error messages that are sent in responses. Used as the error type in [ControllerError], which is used by all the controllers in the application.
93
94All the information in the error is meant to be seen by the user. The type of error is determined by the [ControllerErrorType] enum, which is stored inside this struct. The type of the error determines which HTTP status code will be sent to the user.
95
96## Examples
97
98### Usage without source error
99
100```no_run
101# use headless_lms_server::prelude::*;
102# fn random_function() -> ControllerResult<web::Json<()>> {
103#    let token = skip_authorize();
104#    let erroneous_condition = 1 == 1;
105if erroneous_condition {
106    return Err(ControllerError::new(
107        ControllerErrorType::BadRequest,
108        "Cannot create a new account when signed in.".to_string(),
109        None,
110    ));
111}
112# token.authorized_ok(web::Json(()))
113# }
114```
115
116### Usage with a source error
117
118Used when calling a function that returns an error that cannot be automatically converted to an ControllerError. (See `impl From<X>` implementations on this struct.)
119
120```no_run
121# use headless_lms_server::prelude::*;
122# fn some_function_returning_an_error() -> ControllerResult<web::Json<()>> {
123#    return Err(ControllerError::new(
124#         ControllerErrorType::BadRequest,
125#         "Cannot create a new account when signed in.".to_string(),
126#         None,
127#     ));
128# }
129#
130# fn random_function() -> ControllerResult<web::Json<()>> {
131#    let token = skip_authorize();
132#    let erroneous_condition = 1 == 1;
133some_function_returning_an_error().map_err(|original_error| {
134    ControllerError::new(
135        ControllerErrorType::InternalServerError,
136        "Could not read file".to_string(),
137        Some(original_error.into()),
138    )
139})?;
140# token.authorized_ok(web::Json(()))
141# }
142```
143
144### Example HTTP response from an error
145
146```json
147{
148    "title": "Internal Server Error",
149    "message": "pool timed out while waiting for an open connection",
150    "source": "source of error"
151}
152```
153*/
154pub struct ControllerError {
155    error_type: <ControllerError as BackendError>::ErrorType,
156    message: String,
157    /// Original error that caused this error.
158    source: Option<anyhow::Error>,
159    /// A trace of tokio tracing spans, generated automatically when the error is generated.
160    span_trace: Box<SpanTrace>,
161    /// Stack trace, generated automatically when the error is created.
162    backtrace: Box<Backtrace>,
163}
164
165/// Custom formatter so that errors that get printed to the console are easy-to-read with proper context where the error is coming from.
166impl std::fmt::Debug for ControllerError {
167    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
168        f.debug_struct("ControllerError")
169            .field("error_type", &self.error_type)
170            .field("message", &self.message)
171            .field("source", &self.source)
172            .finish()?;
173
174        f.write_str("\n\nOperating system thread stack backtrace:\n")?;
175        format_backtrace(&self.backtrace, f)?;
176
177        f.write_str("\n\nTokio tracing span trace:\n")?;
178        f.write_fmt(format_args!("{}\n", &self.span_trace))?;
179
180        Ok(())
181    }
182}
183
184impl std::error::Error for ControllerError {
185    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
186        self.source.as_ref().and_then(|o| o.source())
187    }
188
189    fn cause(&self) -> Option<&dyn std::error::Error> {
190        self.source()
191    }
192}
193
194impl std::fmt::Display for ControllerError {
195    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
196        write!(
197            f,
198            "ControllerError {:?} {:?}",
199            self.error_type, self.message
200        )
201    }
202}
203
204impl BackendError for ControllerError {
205    type ErrorType = ControllerErrorType;
206
207    fn new<M: Into<String>, S: Into<Option<anyhow::Error>>>(
208        error_type: Self::ErrorType,
209        message: M,
210        source_error: S,
211    ) -> Self {
212        Self::new_with_traces(
213            error_type,
214            message,
215            source_error,
216            Backtrace::new(),
217            SpanTrace::capture(),
218        )
219    }
220
221    fn backtrace(&self) -> Option<&Backtrace> {
222        Some(&self.backtrace)
223    }
224
225    fn error_type(&self) -> &Self::ErrorType {
226        &self.error_type
227    }
228
229    fn message(&self) -> &str {
230        &self.message
231    }
232
233    fn span_trace(&self) -> &SpanTrace {
234        &self.span_trace
235    }
236
237    fn new_with_traces<M: Into<String>, S: Into<Option<anyhow::Error>>>(
238        error_type: Self::ErrorType,
239        message: M,
240        source_error: S,
241        backtrace: Backtrace,
242        span_trace: SpanTrace,
243    ) -> Self {
244        Self {
245            error_type,
246            message: message.into(),
247            source: source_error.into(),
248            span_trace: Box::new(span_trace),
249            backtrace: Box::new(backtrace),
250        }
251    }
252}
253
254#[derive(Debug, Serialize, Deserialize, Clone)]
255#[serde(rename_all = "snake_case")]
256pub enum ErrorMetadata {
257    BlockId(Uuid),
258}
259
260#[derive(Debug, Serialize, Deserialize)]
261pub struct ApiErrorIssue {
262    pub path: Option<String>,
263    pub code: Option<String>,
264    pub message: String,
265}
266
267#[derive(Debug, Serialize, Deserialize, Clone, Copy)]
268#[serde(rename_all = "snake_case")]
269pub enum ValidationIssueCode {
270    MissingExerciseType,
271}
272
273impl ValidationIssueCode {
274    /// Returns stable API-facing code value for validation issues.
275    fn as_api_code(self) -> String {
276        serde_json::to_value(self)
277            .ok()
278            .and_then(|v| v.as_str().map(|code| code.to_string()))
279            .unwrap_or_else(|| "unknown_validation_issue".to_string())
280    }
281}
282
283/// Canonical API error envelope returned for controlled application errors.
284#[derive(Debug, Serialize, Deserialize)]
285pub struct ApiErrorResponse {
286    #[serde(rename = "type")]
287    #[serde(skip_serializing_if = "Option::is_none")]
288    pub error_type: Option<String>,
289    #[serde(skip_serializing_if = "Option::is_none")]
290    pub message_key: Option<String>,
291    #[serde(skip_serializing_if = "Option::is_none")]
292    pub message: Option<String>,
293    #[serde(default, skip_serializing_if = "Vec::is_empty")]
294    pub errors: Vec<ApiErrorIssue>,
295    #[serde(skip_serializing_if = "Option::is_none")]
296    pub metadata: Option<serde_json::Value>,
297}
298
299impl error::ResponseError for ControllerError {
300    fn error_response(&self) -> HttpResponse {
301        if let ControllerErrorType::InternalServerError = &self.error_type {
302            use std::fmt::Write as _;
303            let mut err_string = String::new();
304            let mut source = Some(self as &dyn std::error::Error);
305            while let Some(err) = source {
306                let _ = write!(err_string, "{}\n    ", err);
307                source = err.source();
308            }
309            error!("Internal server error: {}", err_string);
310        }
311        if let ControllerErrorType::OAuthError(data) = &self.error_type {
312            if let Some(uri) = &data.redirect_uri
313                && let Ok(mut url) = url::Url::parse(uri)
314            {
315                {
316                    let mut qp = url.query_pairs_mut();
317                    qp.append_pair("error", &data.error);
318                    qp.append_pair("error_description", &data.error_description);
319                    if let Some(state) = &data.state {
320                        qp.append_pair("state", state);
321                    }
322                }
323                let loc = url.to_string();
324                return HttpResponse::Found()
325                    .append_header(("Location", loc))
326                    .finish();
327            }
328
329            let status = match data.error.as_str() {
330                "invalid_client" => StatusCode::UNAUTHORIZED,     // 401
331                "invalid_token" => StatusCode::UNAUTHORIZED,      // 401 (bearer)
332                "invalid_dpop_proof" => StatusCode::UNAUTHORIZED, // 401 (dpop)
333                "use_dpop_nonce" => StatusCode::UNAUTHORIZED,     // 401 (dpop)
334                "insufficient_scope" => StatusCode::FORBIDDEN,    // 403
335                _ => StatusCode::BAD_REQUEST,
336            };
337
338            let mut res = HttpResponse::build(status);
339            // Small helper to safely embed values in WWW-Authenticate auth-param strings.
340            fn escape_auth_param(s: &str) -> String {
341                s.replace('\\', "\\\\").replace('"', "\\\"")
342            }
343
344            match data.error.as_str() {
345                // OAuth2 Bearer challenges (RFC 6750 §3)
346                "invalid_client" | "invalid_token" | "insufficient_scope" | "invalid_request" => {
347                    let err = escape_auth_param(&data.error);
348                    let desc = escape_auth_param(&data.error_description);
349                    let hdr = format!(r#"Bearer error="{}", error_description="{}""#, err, desc);
350                    res.append_header(("WWW-Authenticate", hdr));
351                }
352
353                // DPoP auth challenges (RFC 9449 §12.2)
354                "invalid_dpop_proof" => {
355                    let err = escape_auth_param(&data.error);
356                    let desc = escape_auth_param(&data.error_description);
357                    let hdr = format!(r#"DPoP error="{}", error_description="{}""#, err, desc);
358                    res.append_header(("WWW-Authenticate", hdr));
359                }
360
361                "use_dpop_nonce" => {
362                    let err = escape_auth_param(&data.error);
363                    let desc = escape_auth_param(&data.error_description);
364                    let hdr = format!(r#"DPoP error="{}", error_description="{}""#, err, desc);
365                    res.append_header(("WWW-Authenticate", hdr));
366
367                    // Provide the server-generated nonce (clients must echo it in the next proof)
368                    if let Some(nonce) = &data.nonce {
369                        res.append_header(("DPoP-Nonce", nonce.clone()));
370                    }
371                }
372
373                _ => {}
374            }
375
376            // Prevent caching per RFC 6749 §5.1 (common practice for error responses too)
377            res.append_header(("Cache-Control", "no-store"))
378                .append_header(("Pragma", "no-cache"));
379
380            // OAuth token/introspection semantics are standardized around `error` and
381            // `error_description`; keep compatibility for protocol clients.
382            return res.json(serde_json::json!({
383                "error": data.error,
384                "error_description": data.error_description
385            }));
386        }
387
388        let status = self.status_code();
389
390        let metadata = match &self.error_type {
391            ControllerErrorType::BadRequestWithData(data) => Some(data.clone()),
392            _ => None,
393        };
394
395        let metadata_json =
396            metadata.map(|ErrorMetadata::BlockId(id)| serde_json::json!({ "block_id": id }));
397        let (error_type, message_key) = self.error_type_and_message_key();
398        let errors = self.validation_issues();
399        let message = match self.error_type {
400            ControllerErrorType::InternalServerError => None,
401            _ => Some(self.message.clone()),
402        };
403
404        let error_response = ApiErrorResponse {
405            error_type: Some(error_type.to_string()),
406            message_key: Some(message_key.to_string()),
407            message,
408            errors,
409            metadata: metadata_json,
410        };
411
412        HttpResponseBuilder::new(status)
413            .append_header(ContentType::json())
414            .body(serde_json::to_string(&error_response).unwrap_or_else(|_| {
415                r#"{"type":"internal_error","message_key":"internal_error"}"#.to_string()
416            }))
417    }
418
419    fn status_code(&self) -> StatusCode {
420        match self.error_type {
421            ControllerErrorType::InternalServerError => StatusCode::INTERNAL_SERVER_ERROR,
422            ControllerErrorType::BadRequest => StatusCode::UNPROCESSABLE_ENTITY,
423            ControllerErrorType::BadRequestWithData(_) => StatusCode::UNPROCESSABLE_ENTITY,
424            ControllerErrorType::NotFound => StatusCode::NOT_FOUND,
425            ControllerErrorType::Unauthorized => StatusCode::UNAUTHORIZED,
426            ControllerErrorType::UnauthorizedWithReason(_) => StatusCode::UNAUTHORIZED,
427            ControllerErrorType::Forbidden => StatusCode::FORBIDDEN,
428            ControllerErrorType::OAuthError(_) => StatusCode::OK,
429        }
430    }
431}
432
433impl ControllerError {
434    fn error_type_and_message_key(&self) -> (&'static str, &'static str) {
435        match self.error_type {
436            ControllerErrorType::InternalServerError => ("internal_error", "internal_error"),
437            ControllerErrorType::BadRequest => ("validation_error", "validation_error"),
438            ControllerErrorType::BadRequestWithData(_) => {
439                ("validation_error", "validation_error_with_metadata")
440            }
441            ControllerErrorType::NotFound => ("not_found", "not_found"),
442            ControllerErrorType::Unauthorized => ("unauthorized", "unauthorized"),
443            ControllerErrorType::UnauthorizedWithReason(reason) => {
444                ("unauthorized", reason.message_key())
445            }
446            ControllerErrorType::Forbidden => ("forbidden", "forbidden"),
447            ControllerErrorType::OAuthError(_) => ("oauth_error", "oauth_error"),
448        }
449    }
450
451    /// Derives issue-level validation details from known backend validation cases.
452    fn validation_issues(&self) -> Vec<ApiErrorIssue> {
453        match &self.error_type {
454            ControllerErrorType::BadRequestWithData(_)
455                if self.message == MISSING_EXERCISE_TYPE_DESCRIPTION =>
456            {
457                vec![ApiErrorIssue {
458                    path: Some("exercise_type".to_string()),
459                    code: Some(ValidationIssueCode::MissingExerciseType.as_api_code()),
460                    message: self.message.clone(),
461                }]
462            }
463            _ => Vec::new(),
464        }
465    }
466}
467
468#[derive(Debug, Serialize, Deserialize, Clone)]
469#[serde(rename_all = "snake_case")]
470pub struct OAuthErrorData {
471    pub error: String,
472    pub error_description: String,
473    pub redirect_uri: Option<String>,
474    pub state: Option<String>,
475    pub nonce: Option<String>,
476}
477
478pub enum OAuthErrorCode {
479    InvalidGrant,
480    InvalidRequest,
481    InvalidClient,
482    InvalidToken,
483    InsufficientScope,
484    UnsupportedGrantType,
485    UnsupportedResponseType,
486    ServerError,
487    InvalidDpopProof,
488    UseDpopNonce,
489}
490
491impl OAuthErrorCode {
492    pub fn as_str(&self) -> &'static str {
493        match self {
494            Self::InvalidGrant => "invalid_grant",
495            Self::InvalidRequest => "invalid_request",
496            Self::InvalidClient => "invalid_client",
497            Self::InvalidToken => "invalid_token",
498            Self::InsufficientScope => "insufficient_scope",
499            Self::UnsupportedGrantType => "unsupported_grant_type",
500            Self::UnsupportedResponseType => "unsupported_response_type",
501            Self::ServerError => "server_error",
502            Self::InvalidDpopProof => "invalid_dpop_proof",
503            Self::UseDpopNonce => "use_dpop_nonce",
504        }
505    }
506}
507
508impl From<anyhow::Error> for ControllerError {
509    fn from(err: anyhow::Error) -> ControllerError {
510        if let Some(sqlx::Error::RowNotFound) = err.downcast_ref::<sqlx::Error>() {
511            return Self::new(ControllerErrorType::NotFound, err.to_string(), Some(err));
512        }
513
514        Self::new(
515            ControllerErrorType::InternalServerError,
516            err.to_string(),
517            Some(err),
518        )
519    }
520}
521
522impl From<uuid::Error> for ControllerError {
523    fn from(err: uuid::Error) -> ControllerError {
524        Self::new(
525            ControllerErrorType::BadRequest,
526            err.to_string(),
527            Some(err.into()),
528        )
529    }
530}
531
532impl From<sqlx::Error> for ControllerError {
533    fn from(err: sqlx::Error) -> ControllerError {
534        Self::new(
535            ControllerErrorType::InternalServerError,
536            err.to_string(),
537            Some(err.into()),
538        )
539    }
540}
541
542impl From<git2::Error> for ControllerError {
543    fn from(err: git2::Error) -> ControllerError {
544        Self::new(
545            ControllerErrorType::InternalServerError,
546            err.to_string(),
547            Some(err.into()),
548        )
549    }
550}
551
552impl From<actix_web::Error> for ControllerError {
553    fn from(err: actix_web::Error) -> Self {
554        Self::new(
555            ControllerErrorType::InternalServerError,
556            err.to_string(),
557            None,
558        )
559    }
560}
561
562impl From<actix_multipart::MultipartError> for ControllerError {
563    fn from(err: actix_multipart::MultipartError) -> Self {
564        Self::new(
565            ControllerErrorType::InternalServerError,
566            err.to_string(),
567            None,
568        )
569    }
570}
571
572impl From<jsonwebtoken::errors::Error> for ControllerError {
573    fn from(err: jsonwebtoken::errors::Error) -> Self {
574        Self::new(
575            ControllerErrorType::InternalServerError,
576            err.to_string(),
577            None,
578        )
579    }
580}
581
582impl From<ModelError> for ControllerError {
583    fn from(err: ModelError) -> Self {
584        let backtrace: Backtrace =
585            match headless_lms_base::error::backend_error::BackendError::backtrace(&err) {
586                Some(backtrace) => backtrace.clone(),
587                _ => Backtrace::new(),
588            };
589        let span_trace = err.span_trace().clone();
590        match err.error_type() {
591            ModelErrorType::RecordNotFound => Self::new_with_traces(
592                ControllerErrorType::NotFound,
593                err.to_string(),
594                Some(err.into()),
595                backtrace,
596                span_trace,
597            ),
598            ModelErrorType::NotFound => Self::new_with_traces(
599                ControllerErrorType::NotFound,
600                err.to_string(),
601                Some(err.into()),
602                backtrace,
603                span_trace,
604            ),
605            ModelErrorType::PreconditionFailed => Self::new_with_traces(
606                ControllerErrorType::BadRequest,
607                err.message().to_string(),
608                Some(err.into()),
609                backtrace,
610                span_trace,
611            ),
612            ModelErrorType::PreconditionFailedWithCMSAnchorBlockId { description, id } => {
613                Self::new_with_traces(
614                    ControllerErrorType::BadRequestWithData(ErrorMetadata::BlockId(*id)),
615                    description.to_string(),
616                    Some(err.into()),
617                    backtrace,
618                    span_trace,
619                )
620            }
621            ModelErrorType::DatabaseConstraint { description, .. } => Self::new_with_traces(
622                ControllerErrorType::BadRequest,
623                description.to_string(),
624                Some(err.into()),
625                backtrace,
626                span_trace,
627            ),
628            ModelErrorType::InvalidRequest => Self::new_with_traces(
629                ControllerErrorType::BadRequest,
630                err.message().to_string(),
631                Some(err.into()),
632                backtrace,
633                span_trace,
634            ),
635            _ => Self::new_with_traces(
636                ControllerErrorType::InternalServerError,
637                err.to_string(),
638                Some(err.into()),
639                backtrace,
640                span_trace,
641            ),
642        }
643    }
644}
645
646impl From<UtilError> for ControllerError {
647    fn from(err: UtilError) -> Self {
648        let backtrace: Backtrace =
649            match headless_lms_base::error::backend_error::BackendError::backtrace(&err) {
650                Some(backtrace) => backtrace.clone(),
651                _ => Backtrace::new(),
652            };
653        let span_trace = err.span_trace().clone();
654        Self::new_with_traces(
655            ControllerErrorType::InternalServerError,
656            err.to_string(),
657            Some(err.into()),
658            backtrace,
659            span_trace,
660        )
661    }
662}
663
664impl From<serde_json::Error> for ControllerError {
665    fn from(err: serde_json::Error) -> Self {
666        Self::new(
667            ControllerErrorType::InternalServerError,
668            err.to_string(),
669            Some(err.into()),
670        )
671    }
672}
673
674impl From<base64::DecodeError> for ControllerError {
675    fn from(err: base64::DecodeError) -> Self {
676        Self::new(
677            ControllerErrorType::InternalServerError,
678            err.to_string(),
679            Some(err.into()),
680        )
681    }
682}
683
684impl From<std::string::FromUtf8Error> for ControllerError {
685    fn from(err: std::string::FromUtf8Error) -> Self {
686        Self::new(
687            ControllerErrorType::InternalServerError,
688            err.to_string(),
689            Some(err.into()),
690        )
691    }
692}
693
694impl From<pkcs8::spki::Error> for ControllerError {
695    fn from(err: pkcs8::spki::Error) -> Self {
696        Self::new(
697            ControllerErrorType::InternalServerError,
698            err.to_string(),
699            Some(err.into()),
700        )
701    }
702}
703
704impl From<dpop_verifier::error::DpopError> for ControllerError {
705    fn from(err: DpopError) -> Self {
706        let oauth_error = match &err {
707            DpopError::MultipleDpopHeaders
708            | DpopError::InvalidDpopHeader
709            | DpopError::MissingDpopHeader
710            | DpopError::MalformedJws
711            | DpopError::InvalidAlg(_)
712            | DpopError::UnsupportedAlg(_)
713            | DpopError::InvalidSignature
714            | DpopError::BadJwk(_)
715            | DpopError::MissingClaim(_)
716            | DpopError::InvalidMethod
717            | DpopError::HtmMismatch
718            | DpopError::MalformedHtu
719            | DpopError::HtuMismatch
720            | DpopError::AthMalformed
721            | DpopError::MissingAth
722            | DpopError::AthMismatch
723            | DpopError::FutureSkew
724            | DpopError::Stale
725            | DpopError::Replay
726            | DpopError::JtiTooLong
727            | DpopError::NonceMismatch
728            | DpopError::NonceStale
729            | DpopError::InvalidHmacConfig
730            | DpopError::MissingNonce => OAuthErrorData {
731                error: OAuthErrorCode::InvalidDpopProof.as_str().into(),
732                error_description: err.to_string(),
733                redirect_uri: None,
734                state: None,
735                nonce: None,
736            },
737
738            DpopError::Store(e) => OAuthErrorData {
739                error: OAuthErrorCode::ServerError.as_str().into(),
740                error_description: format!("DPoP storage error: {e}"),
741                redirect_uri: None,
742                state: None,
743                nonce: None,
744            },
745
746            DpopError::UseDpopNonce { nonce } => OAuthErrorData {
747                error: OAuthErrorCode::UseDpopNonce.as_str().into(), // per RFC 9449 §12.2
748                error_description: "Server requires DPoP nonce".into(),
749                redirect_uri: None,
750                state: None,
751                nonce: Some(nonce.clone()),
752            },
753        };
754
755        ControllerError::new(
756            ControllerErrorType::OAuthError(Box::new(oauth_error)),
757            err.to_string(),
758            Some(err.into()),
759        )
760    }
761}
762
763#[derive(Debug, thiserror::Error)]
764pub enum PkceFlowError {
765    /// Request is malformed or missing a required PKCE parameter
766    #[error("{0}")]
767    InvalidRequest(&'static str),
768
769    /// PKCE check failed (e.g., code_verifier doesn't match stored challenge)
770    #[error("{0}")]
771    InvalidGrant(&'static str),
772
773    /// Server-side (DB/state) problem
774    #[error("{0}")]
775    ServerError(&'static str),
776}
777
778impl From<PkceFlowError> for ControllerError {
779    fn from(err: PkceFlowError) -> Self {
780        let data = match &err {
781            PkceFlowError::InvalidRequest(msg) => OAuthErrorData {
782                error: OAuthErrorCode::InvalidRequest.as_str().into(),
783                error_description: (*msg).into(),
784                redirect_uri: None,
785                state: None,
786                nonce: None,
787            },
788            PkceFlowError::InvalidGrant(msg) => OAuthErrorData {
789                error: OAuthErrorCode::InvalidGrant.as_str().into(),
790                error_description: (*msg).into(),
791                redirect_uri: None,
792                state: None,
793                nonce: None,
794            },
795            PkceFlowError::ServerError(msg) => OAuthErrorData {
796                error: OAuthErrorCode::ServerError.as_str().into(),
797                error_description: (*msg).into(),
798                redirect_uri: None,
799                state: None,
800                nonce: None,
801            },
802        };
803
804        ControllerError::new(
805            ControllerErrorType::OAuthError(Box::new(data)),
806            err.to_string(),
807            Some(anyhow::anyhow!(err)),
808        )
809    }
810}
811
812impl From<crate::domain::oauth::pkce::PkceError> for PkceFlowError {
813    fn from(_err: crate::domain::oauth::pkce::PkceError) -> Self {
814        // Both BadLength and BadCharset are "invalid_request" per OAuth spec
815        PkceFlowError::InvalidRequest("invalid code_verifier")
816    }
817}
818
819impl From<crate::domain::oauth::pkce::PkceError> for ControllerError {
820    fn from(err: crate::domain::oauth::pkce::PkceError) -> Self {
821        PkceFlowError::from(err).into()
822    }
823}
824
825impl From<ChatbotError> for ControllerError {
826    fn from(err: ChatbotError) -> Self {
827        ControllerError::new(
828            ControllerErrorType::InternalServerError,
829            err.message().to_string(),
830            Some(err.into()),
831        )
832    }
833}
834
835// Generate error creation macros for ControllerError
836headless_lms_utils::define_err_macro!(
837    controller_err,
838    ControllerError,
839    ControllerErrorType,
840    "Create a ControllerError with less boilerplate."
841);
842
843/// Helper function for `.map_err()` chains to wrap any error as ControllerError.
844///
845/// This function creates a closure that converts any error into a `ControllerError`
846/// with the specified error type and message, including the original error as the source.
847///
848/// # Examples
849///
850/// ```ignore
851/// // Instead of:
852/// .map_err(|e| ControllerError::new(ControllerErrorType::BadRequest, e.to_string(), Some(e.into())))?
853///
854/// // You can write:
855/// .map_err(as_controller_error(ControllerErrorType::BadRequest, "Failed to process".to_string()))?
856/// ```
857pub fn as_controller_error<E>(
858    error_type: ControllerErrorType,
859    message: impl Into<String>,
860) -> impl FnOnce(E) -> ControllerError
861where
862    E: Into<anyhow::Error>,
863{
864    let msg = message.into();
865    move |e| ControllerError::new(error_type, msg, Some(e.into()))
866}
867
868/// Helper function for `.ok_or_else()` to create ControllerError on None.
869///
870/// This function creates a closure that generates a `ControllerError` with the
871/// specified error type and message when called.
872///
873/// # Examples
874///
875/// ```ignore
876/// // Instead of:
877/// .ok_or_else(|| ControllerError::new(ControllerErrorType::NotFound, "Item not found".to_string(), None))
878///
879/// // You can write:
880/// .ok_or_else(missing_controller_error(ControllerErrorType::NotFound, "Item not found".to_string()))
881/// ```
882pub fn missing_controller_error(
883    error_type: ControllerErrorType,
884    message: impl Into<String>,
885) -> impl FnOnce() -> ControllerError {
886    let msg = message.into();
887    move || ControllerError::new(error_type, msg, None)
888}
889
890#[cfg(test)]
891mod tests {
892    use super::*;
893    use actix_web::ResponseError;
894    use futures_util::FutureExt;
895
896    #[test]
897    fn test_controller_err_macro_without_source() {
898        let err = controller_err!(BadRequest, "Test error message".to_string());
899        assert_eq!(err.message(), "Test error message");
900        assert!(matches!(err.error_type(), ControllerErrorType::BadRequest));
901    }
902
903    #[test]
904    fn test_controller_err_macro_with_source() {
905        let source_err = std::io::Error::new(std::io::ErrorKind::NotFound, "file not found");
906        let err = controller_err!(InternalServerError, "Wrapped error".to_string(), source_err);
907        assert_eq!(err.message(), "Wrapped error");
908    }
909
910    #[test]
911    fn test_as_controller_error_helper() {
912        let result: Result<(), std::io::Error> = Err(std::io::Error::new(
913            std::io::ErrorKind::NotFound,
914            "test error",
915        ));
916        let controller_result = result.map_err(as_controller_error(
917            ControllerErrorType::BadRequest,
918            "Invalid input".to_string(),
919        ));
920
921        assert!(controller_result.is_err());
922        let err = controller_result.unwrap_err();
923        assert_eq!(err.message(), "Invalid input");
924        assert!(matches!(err.error_type(), ControllerErrorType::BadRequest));
925    }
926
927    #[test]
928    fn test_missing_controller_error_helper() {
929        let option: Option<String> = None;
930        let result = option.ok_or_else(missing_controller_error(
931            ControllerErrorType::NotFound,
932            "Resource not found".to_string(),
933        ));
934
935        assert!(result.is_err());
936        let err = result.unwrap_err();
937        assert_eq!(err.message(), "Resource not found");
938        assert!(matches!(err.error_type(), ControllerErrorType::NotFound));
939    }
940
941    #[test]
942    fn test_controller_err_with_format() {
943        let user_id = 42;
944        let err = controller_err!(Unauthorized, format!("User {} is not authorized", user_id));
945        assert_eq!(err.message(), "User 42 is not authorized");
946    }
947
948    #[test]
949    fn test_controller_err_all_variants() {
950        // Test that macros work with all standard error type variants
951        let _ = controller_err!(InternalServerError, "test".to_string());
952        let _ = controller_err!(BadRequest, "test".to_string());
953        let _ = controller_err!(NotFound, "test".to_string());
954        let _ = controller_err!(Unauthorized, "test".to_string());
955        let _ = ControllerError::new(
956            ControllerErrorType::UnauthorizedWithReason(UnauthorizedReason::ChapterNotOpenYet),
957            "test".to_string(),
958            None,
959        );
960        let _ = controller_err!(Forbidden, "test".to_string());
961    }
962
963    #[test]
964    fn test_canonical_error_envelope_shape() {
965        let err = controller_err!(BadRequest, "Validation failed".to_string());
966        let response = err.error_response();
967        assert_eq!(response.status(), StatusCode::UNPROCESSABLE_ENTITY);
968
969        let bytes = actix_web::body::to_bytes(response.into_body())
970            .now_or_never()
971            .expect("response should resolve immediately")
972            .expect("body bytes");
973        let value: serde_json::Value = serde_json::from_slice(&bytes).expect("json");
974        assert_eq!(value["type"], "validation_error");
975        assert_eq!(value["message_key"], "validation_error");
976        assert_eq!(value["message"], "Validation failed");
977        assert!(value.get("status").is_none());
978        assert!(value.get("request_id").is_none());
979    }
980
981    #[test]
982    fn test_validation_issue_code_is_serialized_for_missing_exercise_type() {
983        let err = ControllerError::new(
984            ControllerErrorType::BadRequestWithData(ErrorMetadata::BlockId(Uuid::nil())),
985            MISSING_EXERCISE_TYPE_DESCRIPTION.to_string(),
986            None,
987        );
988        let response = err.error_response();
989        let bytes = actix_web::body::to_bytes(response.into_body())
990            .now_or_never()
991            .expect("response should resolve immediately")
992            .expect("body bytes");
993        let value: serde_json::Value = serde_json::from_slice(&bytes).expect("json");
994
995        assert_eq!(value["type"], "validation_error");
996        assert_eq!(value["message_key"], "validation_error_with_metadata");
997        assert_eq!(value["errors"][0]["code"], "missing_exercise_type");
998        assert_eq!(value["errors"][0]["path"], "exercise_type");
999    }
1000
1001    #[test]
1002    fn test_chapter_not_open_uses_dedicated_message_key() {
1003        let err = ControllerError::new(
1004            ControllerErrorType::UnauthorizedWithReason(UnauthorizedReason::ChapterNotOpenYet),
1005            "Chapter is not open yet.".to_string(),
1006            None,
1007        );
1008        let response = err.error_response();
1009        let bytes = actix_web::body::to_bytes(response.into_body())
1010            .now_or_never()
1011            .expect("response should resolve immediately")
1012            .expect("body bytes");
1013        let value: serde_json::Value = serde_json::from_slice(&bytes).expect("json");
1014
1015        assert_eq!(value["type"], "unauthorized");
1016        assert_eq!(value["message_key"], "chapter_not_open_yet");
1017        assert_eq!(value["message"], "Chapter is not open yet.");
1018    }
1019
1020    #[test]
1021    fn test_exam_exercise_auth_requirement_uses_dedicated_message_key() {
1022        let err = ControllerError::new(
1023            ControllerErrorType::UnauthorizedWithReason(
1024                UnauthorizedReason::AuthenticationRequiredForExamExercise,
1025            ),
1026            "User must be authenticated to view exam exercises".to_string(),
1027            None,
1028        );
1029        let response = err.error_response();
1030        let bytes = actix_web::body::to_bytes(response.into_body())
1031            .now_or_never()
1032            .expect("response should resolve immediately")
1033            .expect("body bytes");
1034        let value: serde_json::Value = serde_json::from_slice(&bytes).expect("json");
1035
1036        assert_eq!(value["type"], "unauthorized");
1037        assert_eq!(
1038            value["message_key"],
1039            "authentication_required_for_exam_exercise"
1040        );
1041        assert_eq!(
1042            value["message"],
1043            "User must be authenticated to view exam exercises"
1044        );
1045    }
1046
1047    #[test]
1048    fn test_generic_unauthorized_uses_unauthorized_message_key() {
1049        let err = ControllerError::new(
1050            ControllerErrorType::Unauthorized,
1051            "Unauthorized".to_string(),
1052            None,
1053        );
1054        let response = err.error_response();
1055        let bytes = actix_web::body::to_bytes(response.into_body())
1056            .now_or_never()
1057            .expect("response should resolve immediately")
1058            .expect("body bytes");
1059        let value: serde_json::Value = serde_json::from_slice(&bytes).expect("json");
1060
1061        assert_eq!(value["type"], "unauthorized");
1062        assert_eq!(value["message_key"], "unauthorized");
1063        assert_eq!(value["message"], "Unauthorized");
1064    }
1065}