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