Skip to main content

better_auth_core/config/
api_error.rs

1use serde::{Deserialize, Serialize};
2
3use crate::{AuthContext, AuthError, AuthRequest, AuthResponse, AuthResult, AuthSchema};
4
5/// Error policy for HTTP endpoint invocation and the default error page.
6#[derive(Debug, Clone, Default, Serialize, Deserialize)]
7#[serde(default, rename_all = "camelCase")]
8pub struct ApiErrorConfig {
9    /// Rethrow ordinary errors before invoking the optional error callback.
10    #[serde(rename = "throw", skip_serializing_if = "Option::is_none")]
11    pub throw_errors: Option<bool>,
12    /// Error-page redirect and OAuth's default error destination.
13    #[serde(rename = "errorURL")]
14    pub error_url: Option<String>,
15    /// An explicit empty customization also enables the HTML page in production.
16    pub customize_default_error_page: Option<ErrorPageCustomization>,
17}
18
19/// Trusted application-provided CSS values for the default error page.
20#[derive(Debug, Clone, Default, Serialize, Deserialize)]
21#[serde(default, rename_all = "camelCase")]
22pub struct ErrorPageCustomization {
23    pub colors: ErrorPageColors,
24    pub size: ErrorPageSize,
25    pub font: ErrorPageFont,
26    pub disable_title_border: bool,
27    pub disable_corner_decorations: bool,
28    pub disable_background_grid: bool,
29}
30
31#[derive(Debug, Clone, Default, Serialize, Deserialize)]
32#[serde(default, rename_all = "camelCase")]
33pub struct ErrorPageColors {
34    pub background: Option<String>,
35    pub foreground: Option<String>,
36    pub primary: Option<String>,
37    pub primary_foreground: Option<String>,
38    pub muted_foreground: Option<String>,
39    pub border: Option<String>,
40    pub destructive: Option<String>,
41    pub title_border: Option<String>,
42    pub title_color: Option<String>,
43    pub grid_color: Option<String>,
44    pub card_background: Option<String>,
45    pub corner_border: Option<String>,
46}
47
48#[derive(Debug, Clone, Default, Serialize, Deserialize)]
49#[serde(default, rename_all = "camelCase")]
50pub struct ErrorPageSize {
51    pub radius_sm: Option<String>,
52    /// Accepted by upstream but unused by its current error-page renderer.
53    pub radius_md: Option<String>,
54    /// Accepted by upstream but unused by its current error-page renderer.
55    pub radius_lg: Option<String>,
56    pub text_sm: Option<String>,
57    pub text2xl: Option<String>,
58    pub text4xl: Option<String>,
59    pub text6xl: Option<String>,
60}
61
62#[derive(Debug, Clone, Default, Serialize, Deserialize)]
63#[serde(default, rename_all = "camelCase")]
64pub struct ErrorPageFont {
65    pub default_family: Option<String>,
66    pub mono_family: Option<String>,
67}
68
69impl<S: AuthSchema> AuthContext<S> {
70    /// Render `/error` from the original Request URL, including duplicate-query ordering.
71    pub fn error_page_response(&self, request: &AuthRequest) -> AuthResult<AuthResponse> {
72        let http =
73            crate::hooks::current_request_hook_context().is_some_and(|context| context.is_http);
74        let original = if http {
75            Some(request)
76        } else {
77            request.original_request()
78        };
79        let url = original
80            .and_then(AuthRequest::url)
81            .ok_or_else(|| AuthError::internal("Invalid URL"))?;
82        let code = url
83            .query_pairs()
84            .find(|(name, _)| name == "error")
85            .map(|(_, value)| value.into_owned())
86            .unwrap_or_default();
87        let code = if super::error_page::valid_error_code(&code) {
88            &code
89        } else {
90            "UNKNOWN"
91        };
92        let description = url
93            .query_pairs()
94            .find(|(name, _)| name == "error_description")
95            .map(|(_, value)| value.into_owned())
96            .filter(|value| !value.is_empty());
97        let mut params = url::form_urlencoded::Serializer::new(String::new());
98        let _ = params.append_pair("error", code);
99        if let Some(description) = &description {
100            let _ = params.append_pair("error_description", description);
101        }
102        let params = params.finish();
103        let policy = &self.config.api_error;
104        if let Some(target) = policy
105            .error_url
106            .as_deref()
107            .filter(|target| !target.is_empty())
108        {
109            return Ok(AuthResponse::new(302).with_header(
110                "location",
111                crate::utils::url::append_query_params(target, &params)?,
112            ));
113        }
114        if self.is_production()? && policy.customize_default_error_page.is_none() {
115            return Ok(AuthResponse::new(302).with_header("location", format!("/?{params}")));
116        }
117        Ok(AuthResponse::html(
118            200,
119            super::error_page::render(
120                code,
121                description.as_deref(),
122                policy.customize_default_error_page.as_ref(),
123            ),
124        )
125        .with_header("content-type", "text/html"))
126    }
127}