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_chatbot::prelude::ChatbotError;
14use headless_lms_models::{ModelError, ModelErrorType};
15use headless_lms_utils::error::{
16    backend_error::BackendError, backtrace_formatter::format_backtrace, util_error::UtilError,
17};
18use serde::{Deserialize, Serialize};
19use tracing_error::SpanTrace;
20#[cfg(feature = "ts_rs")]
21use ts_rs::TS;
22use uuid::Uuid;
23
24/**
25Used as the result types for all controllers.
26Only put information here that you want to be visible to users.
27
28See also [ControllerError] for documentation on how to return errors from controllers.
29*/
30pub type ControllerResult<T, E = ControllerError> = std::result::Result<AuthorizedResponse<T>, E>;
31
32/// The type of [ControllerError] that occured.
33#[derive(Debug, Display, Serialize, Deserialize)]
34pub enum ControllerErrorType {
35    /// HTTP status code 500.
36    #[display("Internal server error")]
37    InternalServerError,
38
39    /// HTTP status code 400.
40    #[display("Bad request")]
41    BadRequest,
42
43    /// HTTP status code 400.
44    #[display("Bad request")]
45    BadRequestWithData(ErrorData),
46
47    /// HTTP status code 404.
48    #[display("Not found")]
49    NotFound,
50
51    /// HTTP status code 401. Needs to log in.
52    #[display("Unauthorized")]
53    Unauthorized,
54
55    /// HTTP status code 403. Is logged in but is not allowed to access the resource.
56    #[display("Forbidden")]
57    Forbidden,
58
59    /// Varied response based on error
60    #[display("OAuthError")]
61    OAuthError(Box<OAuthErrorData>),
62}
63
64/**
65Represents error messages that are sent in responses. Used as the error type in [ControllerError], which is used by all the controllers in the application.
66
67All 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.
68
69## Examples
70
71### Usage without source error
72
73```no_run
74# use headless_lms_server::prelude::*;
75# fn random_function() -> ControllerResult<web::Json<()>> {
76#    let token = skip_authorize();
77#    let erroneous_condition = 1 == 1;
78if erroneous_condition {
79    return Err(ControllerError::new(
80        ControllerErrorType::BadRequest,
81        "Cannot create a new account when signed in.".to_string(),
82        None,
83    ));
84}
85# token.authorized_ok(web::Json(()))
86# }
87```
88
89### Usage with a source error
90
91Used when calling a function that returns an error that cannot be automatically converted to an ControllerError. (See `impl From<X>` implementations on this struct.)
92
93```no_run
94# use headless_lms_server::prelude::*;
95# fn some_function_returning_an_error() -> ControllerResult<web::Json<()>> {
96#    return Err(ControllerError::new(
97#         ControllerErrorType::BadRequest,
98#         "Cannot create a new account when signed in.".to_string(),
99#         None,
100#     ));
101# }
102#
103# fn random_function() -> ControllerResult<web::Json<()>> {
104#    let token = skip_authorize();
105#    let erroneous_condition = 1 == 1;
106some_function_returning_an_error().map_err(|original_error| {
107    ControllerError::new(
108        ControllerErrorType::InternalServerError,
109        "Could not read file".to_string(),
110        Some(original_error.into()),
111    )
112})?;
113# token.authorized_ok(web::Json(()))
114# }
115```
116
117### Example HTTP response from an error
118
119```json
120{
121    "title": "Internal Server Error",
122    "message": "pool timed out while waiting for an open connection",
123    "source": "source of error"
124}
125```
126*/
127pub struct ControllerError {
128    error_type: <ControllerError as BackendError>::ErrorType,
129    message: String,
130    /// Original error that caused this error.
131    source: Option<anyhow::Error>,
132    /// A trace of tokio tracing spans, generated automatically when the error is generated.
133    span_trace: Box<SpanTrace>,
134    /// Stack trace, generated automatically when the error is created.
135    backtrace: Box<Backtrace>,
136}
137
138/// Custom formatter so that errors that get printed to the console are easy-to-read with proper context where the error is coming from.
139impl std::fmt::Debug for ControllerError {
140    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
141        f.debug_struct("ControllerError")
142            .field("error_type", &self.error_type)
143            .field("message", &self.message)
144            .field("source", &self.source)
145            .finish()?;
146
147        f.write_str("\n\nOperating system thread stack backtrace:\n")?;
148        format_backtrace(&self.backtrace, f)?;
149
150        f.write_str("\n\nTokio tracing span trace:\n")?;
151        f.write_fmt(format_args!("{}\n", &self.span_trace))?;
152
153        Ok(())
154    }
155}
156
157impl std::error::Error for ControllerError {
158    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
159        self.source.as_ref().and_then(|o| o.source())
160    }
161
162    fn cause(&self) -> Option<&dyn std::error::Error> {
163        self.source()
164    }
165}
166
167impl std::fmt::Display for ControllerError {
168    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
169        write!(
170            f,
171            "ControllerError {:?} {:?}",
172            self.error_type, self.message
173        )
174    }
175}
176
177impl BackendError for ControllerError {
178    type ErrorType = ControllerErrorType;
179
180    fn new<M: Into<String>, S: Into<Option<anyhow::Error>>>(
181        error_type: Self::ErrorType,
182        message: M,
183        source_error: S,
184    ) -> Self {
185        Self::new_with_traces(
186            error_type,
187            message,
188            source_error,
189            Backtrace::new(),
190            SpanTrace::capture(),
191        )
192    }
193
194    fn backtrace(&self) -> Option<&Backtrace> {
195        Some(&self.backtrace)
196    }
197
198    fn error_type(&self) -> &Self::ErrorType {
199        &self.error_type
200    }
201
202    fn message(&self) -> &str {
203        &self.message
204    }
205
206    fn span_trace(&self) -> &SpanTrace {
207        &self.span_trace
208    }
209
210    fn new_with_traces<M: Into<String>, S: Into<Option<anyhow::Error>>>(
211        error_type: Self::ErrorType,
212        message: M,
213        source_error: S,
214        backtrace: Backtrace,
215        span_trace: SpanTrace,
216    ) -> Self {
217        Self {
218            error_type,
219            message: message.into(),
220            source: source_error.into(),
221            span_trace: Box::new(span_trace),
222            backtrace: Box::new(backtrace),
223        }
224    }
225}
226
227#[derive(Debug, Serialize, Deserialize, Clone)]
228#[cfg_attr(feature = "ts_rs", derive(TS))]
229#[serde(rename_all = "snake_case")]
230pub enum ErrorData {
231    BlockId(Uuid),
232}
233
234/// The format all error messages from the API is in
235#[derive(Debug, Serialize, Deserialize)]
236#[cfg_attr(feature = "ts_rs", derive(TS))]
237pub struct ErrorResponse {
238    pub title: String,
239    pub message: String,
240    pub source: Option<String>,
241    pub data: Option<ErrorData>,
242}
243
244impl error::ResponseError for ControllerError {
245    fn error_response(&self) -> HttpResponse {
246        if let ControllerErrorType::InternalServerError = &self.error_type {
247            use std::fmt::Write as _;
248            let mut err_string = String::new();
249            let mut source = Some(self as &dyn std::error::Error);
250            while let Some(err) = source {
251                let _ = write!(err_string, "{}\n    ", err);
252                source = err.source();
253            }
254            error!("Internal server error: {}", err_string);
255        }
256        if let ControllerErrorType::OAuthError(data) = &self.error_type {
257            if let Some(uri) = &data.redirect_uri
258                && let Ok(mut url) = url::Url::parse(uri)
259            {
260                {
261                    let mut qp = url.query_pairs_mut();
262                    qp.append_pair("error", &data.error);
263                    qp.append_pair("error_description", &data.error_description);
264                    if let Some(state) = &data.state {
265                        qp.append_pair("state", state);
266                    }
267                }
268                let loc = url.to_string();
269                return HttpResponse::Found()
270                    .append_header(("Location", loc))
271                    .finish();
272            }
273
274            let status = match data.error.as_str() {
275                "invalid_client" => StatusCode::UNAUTHORIZED,     // 401
276                "invalid_token" => StatusCode::UNAUTHORIZED,      // 401 (bearer)
277                "invalid_dpop_proof" => StatusCode::UNAUTHORIZED, // 401 (dpop)
278                "use_dpop_nonce" => StatusCode::UNAUTHORIZED,     // 401 (dpop)
279                "insufficient_scope" => StatusCode::FORBIDDEN,    // 403
280                _ => StatusCode::BAD_REQUEST,
281            };
282
283            let mut res = HttpResponse::build(status);
284            // Small helper to safely embed values in WWW-Authenticate auth-param strings.
285            fn escape_auth_param(s: &str) -> String {
286                s.replace('\\', "\\\\").replace('"', "\\\"")
287            }
288
289            match data.error.as_str() {
290                // OAuth2 Bearer challenges (RFC 6750 §3)
291                "invalid_client" | "invalid_token" | "insufficient_scope" | "invalid_request" => {
292                    let err = escape_auth_param(&data.error);
293                    let desc = escape_auth_param(&data.error_description);
294                    let hdr = format!(r#"Bearer error="{}", error_description="{}""#, err, desc);
295                    res.append_header(("WWW-Authenticate", hdr));
296                }
297
298                // DPoP auth challenges (RFC 9449 §12.2)
299                "invalid_dpop_proof" => {
300                    let err = escape_auth_param(&data.error);
301                    let desc = escape_auth_param(&data.error_description);
302                    let hdr = format!(r#"DPoP error="{}", error_description="{}""#, err, desc);
303                    res.append_header(("WWW-Authenticate", hdr));
304                }
305
306                "use_dpop_nonce" => {
307                    let err = escape_auth_param(&data.error);
308                    let desc = escape_auth_param(&data.error_description);
309                    let hdr = format!(r#"DPoP error="{}", error_description="{}""#, err, desc);
310                    res.append_header(("WWW-Authenticate", hdr));
311
312                    // Provide the server-generated nonce (clients must echo it in the next proof)
313                    if let Some(nonce) = &data.nonce {
314                        res.append_header(("DPoP-Nonce", nonce.clone()));
315                    }
316                }
317
318                _ => {}
319            }
320
321            // Prevent caching per RFC 6749 §5.1 (common practice for error responses too)
322            res.append_header(("Cache-Control", "no-store"))
323                .append_header(("Pragma", "no-cache"));
324
325            return res.json(serde_json::json!({
326                "error": data.error,
327                "error_description": data.error_description
328            }));
329        }
330
331        let status = self.status_code();
332
333        let error_data = match &self.error_type {
334            ControllerErrorType::BadRequestWithData(data) => Some(data.clone()),
335            _ => None,
336        };
337
338        let source_message = self.source.as_ref().map(|anyhow_err| {
339            if let Some(controller_err) = anyhow_err.downcast_ref::<ControllerError>() {
340                controller_err.message.clone()
341            } else {
342                anyhow_err.to_string()
343            }
344        });
345
346        let error_response = ErrorResponse {
347            title: status
348                .canonical_reason()
349                .map(str::to_string)
350                .unwrap_or_else(|| status.to_string()),
351            message: self.message.clone(),
352            source: source_message,
353            data: error_data,
354        };
355
356        HttpResponseBuilder::new(status)
357            .append_header(ContentType::json())
358            .body(
359                serde_json::to_string(&error_response).unwrap_or_else(|_| {
360                    r#"{"title":"Internal Server Error","message":"Error occurred while formatting error message."}"#.to_string()
361                }),
362            )
363    }
364
365    fn status_code(&self) -> StatusCode {
366        match self.error_type {
367            ControllerErrorType::InternalServerError => StatusCode::INTERNAL_SERVER_ERROR,
368            ControllerErrorType::BadRequest => StatusCode::BAD_REQUEST,
369            ControllerErrorType::BadRequestWithData(_) => StatusCode::BAD_REQUEST,
370            ControllerErrorType::NotFound => StatusCode::NOT_FOUND,
371            ControllerErrorType::Unauthorized => StatusCode::UNAUTHORIZED,
372            ControllerErrorType::Forbidden => StatusCode::FORBIDDEN,
373            ControllerErrorType::OAuthError(_) => StatusCode::OK,
374        }
375    }
376}
377
378#[derive(Debug, Serialize, Deserialize, Clone)]
379#[serde(rename_all = "snake_case")]
380pub struct OAuthErrorData {
381    pub error: String,
382    pub error_description: String,
383    pub redirect_uri: Option<String>,
384    pub state: Option<String>,
385    pub nonce: Option<String>,
386}
387
388pub enum OAuthErrorCode {
389    InvalidGrant,
390    InvalidRequest,
391    InvalidClient,
392    InvalidToken,
393    InsufficientScope,
394    UnsupportedGrantType,
395    UnsupportedResponseType,
396    ServerError,
397    InvalidDpopProof,
398    UseDpopNonce,
399}
400
401impl OAuthErrorCode {
402    pub fn as_str(&self) -> &'static str {
403        match self {
404            Self::InvalidGrant => "invalid_grant",
405            Self::InvalidRequest => "invalid_request",
406            Self::InvalidClient => "invalid_client",
407            Self::InvalidToken => "invalid_token",
408            Self::InsufficientScope => "insufficient_scope",
409            Self::UnsupportedGrantType => "unsupported_grant_type",
410            Self::UnsupportedResponseType => "unsupported_response_type",
411            Self::ServerError => "server_error",
412            Self::InvalidDpopProof => "invalid_dpop_proof",
413            Self::UseDpopNonce => "use_dpop_nonce",
414        }
415    }
416}
417
418impl From<anyhow::Error> for ControllerError {
419    fn from(err: anyhow::Error) -> ControllerError {
420        if let Some(sqlx::Error::RowNotFound) = err.downcast_ref::<sqlx::Error>() {
421            return Self::new(ControllerErrorType::NotFound, err.to_string(), Some(err));
422        }
423
424        Self::new(
425            ControllerErrorType::InternalServerError,
426            err.to_string(),
427            Some(err),
428        )
429    }
430}
431
432impl From<uuid::Error> for ControllerError {
433    fn from(err: uuid::Error) -> ControllerError {
434        Self::new(
435            ControllerErrorType::BadRequest,
436            err.to_string(),
437            Some(err.into()),
438        )
439    }
440}
441
442impl From<sqlx::Error> for ControllerError {
443    fn from(err: sqlx::Error) -> ControllerError {
444        Self::new(
445            ControllerErrorType::InternalServerError,
446            err.to_string(),
447            Some(err.into()),
448        )
449    }
450}
451
452impl From<git2::Error> for ControllerError {
453    fn from(err: git2::Error) -> ControllerError {
454        Self::new(
455            ControllerErrorType::InternalServerError,
456            err.to_string(),
457            Some(err.into()),
458        )
459    }
460}
461
462impl From<actix_web::Error> for ControllerError {
463    fn from(err: actix_web::Error) -> Self {
464        Self::new(
465            ControllerErrorType::InternalServerError,
466            err.to_string(),
467            None,
468        )
469    }
470}
471
472impl From<actix_multipart::MultipartError> for ControllerError {
473    fn from(err: actix_multipart::MultipartError) -> Self {
474        Self::new(
475            ControllerErrorType::InternalServerError,
476            err.to_string(),
477            None,
478        )
479    }
480}
481
482impl From<jsonwebtoken::errors::Error> for ControllerError {
483    fn from(err: jsonwebtoken::errors::Error) -> Self {
484        Self::new(
485            ControllerErrorType::InternalServerError,
486            err.to_string(),
487            None,
488        )
489    }
490}
491
492impl From<ModelError> for ControllerError {
493    fn from(err: ModelError) -> Self {
494        let backtrace: Backtrace =
495            match headless_lms_utils::error::backend_error::BackendError::backtrace(&err) {
496                Some(backtrace) => backtrace.clone(),
497                _ => Backtrace::new(),
498            };
499        let span_trace = err.span_trace().clone();
500        match err.error_type() {
501            ModelErrorType::RecordNotFound => Self::new_with_traces(
502                ControllerErrorType::NotFound,
503                err.to_string(),
504                Some(err.into()),
505                backtrace,
506                span_trace,
507            ),
508            ModelErrorType::NotFound => Self::new_with_traces(
509                ControllerErrorType::NotFound,
510                err.to_string(),
511                Some(err.into()),
512                backtrace,
513                span_trace,
514            ),
515            ModelErrorType::PreconditionFailed => Self::new_with_traces(
516                ControllerErrorType::BadRequest,
517                err.message().to_string(),
518                Some(err.into()),
519                backtrace,
520                span_trace,
521            ),
522            ModelErrorType::PreconditionFailedWithCMSAnchorBlockId { description, id } => {
523                Self::new_with_traces(
524                    ControllerErrorType::BadRequestWithData(ErrorData::BlockId(*id)),
525                    description.to_string(),
526                    Some(err.into()),
527                    backtrace,
528                    span_trace,
529                )
530            }
531            ModelErrorType::DatabaseConstraint { description, .. } => Self::new_with_traces(
532                ControllerErrorType::BadRequest,
533                description.to_string(),
534                Some(err.into()),
535                backtrace,
536                span_trace,
537            ),
538            ModelErrorType::InvalidRequest => Self::new_with_traces(
539                ControllerErrorType::BadRequest,
540                err.message().to_string(),
541                Some(err.into()),
542                backtrace,
543                span_trace,
544            ),
545            _ => Self::new_with_traces(
546                ControllerErrorType::InternalServerError,
547                err.to_string(),
548                Some(err.into()),
549                backtrace,
550                span_trace,
551            ),
552        }
553    }
554}
555
556impl From<UtilError> for ControllerError {
557    fn from(err: UtilError) -> Self {
558        let backtrace: Backtrace =
559            match headless_lms_utils::error::backend_error::BackendError::backtrace(&err) {
560                Some(backtrace) => backtrace.clone(),
561                _ => Backtrace::new(),
562            };
563        let span_trace = err.span_trace().clone();
564        Self::new_with_traces(
565            ControllerErrorType::InternalServerError,
566            err.to_string(),
567            Some(err.into()),
568            backtrace,
569            span_trace,
570        )
571    }
572}
573
574impl From<serde_json::Error> for ControllerError {
575    fn from(err: serde_json::Error) -> Self {
576        Self::new(
577            ControllerErrorType::InternalServerError,
578            err.to_string(),
579            Some(err.into()),
580        )
581    }
582}
583
584impl From<base64::DecodeError> for ControllerError {
585    fn from(err: base64::DecodeError) -> Self {
586        Self::new(
587            ControllerErrorType::InternalServerError,
588            err.to_string(),
589            Some(err.into()),
590        )
591    }
592}
593
594impl From<std::string::FromUtf8Error> for ControllerError {
595    fn from(err: std::string::FromUtf8Error) -> Self {
596        Self::new(
597            ControllerErrorType::InternalServerError,
598            err.to_string(),
599            Some(err.into()),
600        )
601    }
602}
603
604impl From<pkcs8::spki::Error> for ControllerError {
605    fn from(err: pkcs8::spki::Error) -> Self {
606        Self::new(
607            ControllerErrorType::InternalServerError,
608            err.to_string(),
609            Some(err.into()),
610        )
611    }
612}
613
614impl From<dpop_verifier::error::DpopError> for ControllerError {
615    fn from(err: DpopError) -> Self {
616        let oauth_error = match &err {
617            DpopError::MultipleDpopHeaders
618            | DpopError::InvalidDpopHeader
619            | DpopError::MissingDpopHeader
620            | DpopError::MalformedJws
621            | DpopError::InvalidAlg(_)
622            | DpopError::UnsupportedAlg(_)
623            | DpopError::InvalidSignature
624            | DpopError::BadJwk(_)
625            | DpopError::MissingClaim(_)
626            | DpopError::InvalidMethod
627            | DpopError::HtmMismatch
628            | DpopError::MalformedHtu
629            | DpopError::HtuMismatch
630            | DpopError::AthMalformed
631            | DpopError::MissingAth
632            | DpopError::AthMismatch
633            | DpopError::FutureSkew
634            | DpopError::Stale
635            | DpopError::Replay
636            | DpopError::JtiTooLong
637            | DpopError::NonceMismatch
638            | DpopError::NonceStale
639            | DpopError::InvalidHmacConfig
640            | DpopError::MissingNonce => OAuthErrorData {
641                error: OAuthErrorCode::InvalidDpopProof.as_str().into(),
642                error_description: err.to_string(),
643                redirect_uri: None,
644                state: None,
645                nonce: None,
646            },
647
648            DpopError::Store(e) => OAuthErrorData {
649                error: OAuthErrorCode::ServerError.as_str().into(),
650                error_description: format!("DPoP storage error: {e}"),
651                redirect_uri: None,
652                state: None,
653                nonce: None,
654            },
655
656            DpopError::UseDpopNonce { nonce } => OAuthErrorData {
657                error: OAuthErrorCode::UseDpopNonce.as_str().into(), // per RFC 9449 §12.2
658                error_description: "Server requires DPoP nonce".into(),
659                redirect_uri: None,
660                state: None,
661                nonce: Some(nonce.clone()),
662            },
663        };
664
665        ControllerError::new(
666            ControllerErrorType::OAuthError(Box::new(oauth_error)),
667            err.to_string(),
668            Some(err.into()),
669        )
670    }
671}
672
673#[derive(Debug, thiserror::Error)]
674pub enum PkceFlowError {
675    /// Request is malformed or missing a required PKCE parameter
676    #[error("{0}")]
677    InvalidRequest(&'static str),
678
679    /// PKCE check failed (e.g., code_verifier doesn't match stored challenge)
680    #[error("{0}")]
681    InvalidGrant(&'static str),
682
683    /// Server-side (DB/state) problem
684    #[error("{0}")]
685    ServerError(&'static str),
686}
687
688impl From<PkceFlowError> for ControllerError {
689    fn from(err: PkceFlowError) -> Self {
690        let data = match &err {
691            PkceFlowError::InvalidRequest(msg) => OAuthErrorData {
692                error: OAuthErrorCode::InvalidRequest.as_str().into(),
693                error_description: (*msg).into(),
694                redirect_uri: None,
695                state: None,
696                nonce: None,
697            },
698            PkceFlowError::InvalidGrant(msg) => OAuthErrorData {
699                error: OAuthErrorCode::InvalidGrant.as_str().into(),
700                error_description: (*msg).into(),
701                redirect_uri: None,
702                state: None,
703                nonce: None,
704            },
705            PkceFlowError::ServerError(msg) => OAuthErrorData {
706                error: OAuthErrorCode::ServerError.as_str().into(),
707                error_description: (*msg).into(),
708                redirect_uri: None,
709                state: None,
710                nonce: None,
711            },
712        };
713
714        ControllerError::new(
715            ControllerErrorType::OAuthError(Box::new(data)),
716            err.to_string(),
717            Some(anyhow::anyhow!(err)),
718        )
719    }
720}
721
722impl From<crate::domain::oauth::pkce::PkceError> for PkceFlowError {
723    fn from(_err: crate::domain::oauth::pkce::PkceError) -> Self {
724        // Both BadLength and BadCharset are "invalid_request" per OAuth spec
725        PkceFlowError::InvalidRequest("invalid code_verifier")
726    }
727}
728
729impl From<crate::domain::oauth::pkce::PkceError> for ControllerError {
730    fn from(err: crate::domain::oauth::pkce::PkceError) -> Self {
731        PkceFlowError::from(err).into()
732    }
733}
734
735impl From<ChatbotError> for ControllerError {
736    fn from(err: ChatbotError) -> Self {
737        ControllerError::new(
738            ControllerErrorType::InternalServerError,
739            err.message().to_string(),
740            Some(err.into()),
741        )
742    }
743}