1fn safe_error_code(input: &str) -> &str {
4 if !input.is_empty()
5 && input
6 .chars()
7 .all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '-' || c == '\'')
8 {
9 input
10 } else {
11 "UNKNOWN"
12 }
13}
14
15fn is_preserved_entity(input: &str) -> bool {
16 input.starts_with("amp;")
17 || input.starts_with("lt;")
18 || input.starts_with("gt;")
19 || input.starts_with("quot;")
20 || input.starts_with("#39;")
21 || input
22 .strip_prefix("#x")
23 .and_then(|rest| rest.split_once(';'))
24 .is_some_and(|(hex, _)| !hex.is_empty() && hex.chars().all(|c| c.is_ascii_hexdigit()))
25 || input
26 .strip_prefix('#')
27 .and_then(|rest| rest.split_once(';'))
28 .is_some_and(|(digits, _)| {
29 !digits.is_empty() && digits.chars().all(|c| c.is_ascii_digit())
30 })
31}
32
33fn sanitize_html(input: &str) -> String {
34 let mut out = String::with_capacity(input.len());
35
36 for (idx, ch) in input.char_indices() {
37 match ch {
38 '<' => out.push_str("<"),
39 '>' => out.push_str(">"),
40 '"' => out.push_str("""),
41 '\'' => out.push_str("'"),
42 '&' => {
43 let rest = input.get(idx + ch.len_utf8()..).unwrap_or_default();
44 if is_preserved_entity(rest) {
45 out.push('&');
46 } else {
47 out.push_str("&");
48 }
49 }
50 _ => out.push(ch),
51 }
52 }
53
54 out
55}
56
57fn default_error_description(code: &str) -> String {
58 format!(
59 "We encountered an unexpected error. Please try again or return to the home page. If you're a developer, you can find <a href='https://better-auth.com/docs/reference/errors/{code}' target='_blank' rel=\"noopener noreferrer\" style='color: var(--foreground); text-decoration: underline;'>more information about the error</a>."
60 )
61}
62
63#[must_use]
67pub fn error_page_html(error_code: &str) -> String {
68 error_page_html_with_description(error_code, None)
69}
70
71pub fn error_page_html_with_description(
74 error_code: &str,
75 error_description: Option<&str>,
76) -> String {
77 let safe_code = safe_error_code(error_code);
78 let description =
79 error_description.map_or_else(|| default_error_description(safe_code), sanitize_html);
80 let ask_ai_query = format!("What%20does%20the%20error%20code%20{safe_code}%20mean%3F");
81
82 format!(
83 include_str!("page.html"),
84 safe_code = safe_code,
85 description = description,
86 ask_ai_query = ask_ai_query,
87 )
88}
89
90#[must_use]
92pub fn error_page_redirect_location(error_code: &str, error_description: Option<&str>) -> String {
93 let mut query = url::form_urlencoded::Serializer::new(String::new());
94 _ = query.append_pair("error", safe_error_code(error_code));
95 if let Some(description) = error_description.filter(|value| !value.is_empty()) {
96 _ = query.append_pair("error_description", description);
97 }
98 format!("/?{}", query.finish())
99}
100
101pub fn error_page_response(
106 config: &crate::AuthConfig,
107 error_code: &str,
108 error_description: Option<&str>,
109) -> crate::AuthResult<crate::AuthResponse> {
110 use crate::{AuthError, AuthResponse};
111 if let Some(target) = config
112 .api_error_url
113 .as_deref()
114 .filter(|target| !target.is_empty())
115 {
116 let relative = target.starts_with('/');
117 let origin = url::Url::parse("https://better-auth.invalid")
118 .map_err(|error| AuthError::internal(error.to_string()))?;
119 let mut location = if relative {
120 origin.join(target)
121 } else {
122 url::Url::parse(target)
123 }
124 .map_err(|error| {
125 AuthError::CallbackFailure(Box::new(AuthError::internal(error.to_string())))
126 })?;
127 if target.starts_with("//")
128 || target.starts_with("/\\")
129 || (relative && location.origin() != origin.origin())
130 {
131 return Err(AuthError::CallbackFailure(Box::new(AuthError::internal(
132 "Invalid error URL",
133 ))));
134 }
135 let generated = error_page_redirect_location(error_code, error_description);
136 let query = generated.strip_prefix("/?").unwrap_or_default();
137 let existing = location.query().unwrap_or_default();
138 let separator = if existing.is_empty() || existing.ends_with('&') {
139 ""
140 } else {
141 "&"
142 };
143 let combined = format!("{existing}{separator}{query}");
144 location.set_query(Some(&combined));
145 let location = if relative {
146 location[url::Position::BeforePath..].to_owned()
147 } else {
148 location.to_string()
149 };
150 return Ok(AuthResponse::new(302).with_header("location", location));
151 }
152 if !config.render_error_page {
153 return Ok(AuthResponse::new(302).with_header(
154 "location",
155 error_page_redirect_location(error_code, error_description),
156 ));
157 }
158 let html = error_page_html_with_description(error_code, error_description);
159 Ok(AuthResponse::html(200, html).with_header("content-type", "text/html"))
160}