1use crate::{Request, Response};
2use std::collections::HashMap;
3
4#[derive(Clone)]
6pub struct ErrorPages {
7 custom_pages: HashMap<u16, String>,
8 use_default_styling: bool,
9}
10
11impl ErrorPages {
12 pub fn new() -> Self {
14 Self {
15 custom_pages: HashMap::new(),
16 use_default_styling: true,
17 }
18 }
19
20 pub fn without_default_styling(mut self) -> Self {
22 self.use_default_styling = false;
23 self
24 }
25
26 pub fn custom_page(mut self, status_code: u16, html: String) -> Self {
28 self.custom_pages.insert(status_code, html);
29 self
30 }
31
32 pub fn custom_404(self, html: String) -> Self {
34 self.custom_page(404, html)
35 }
36
37 pub fn custom_500(self, html: String) -> Self {
39 self.custom_page(500, html)
40 }
41
42 pub fn random_404_message() -> &'static str {
44 let messages = [
45 "🔥 Torch doesn't know this route, but the flame burns eternal!",
46 "🔥 This path hasn't been lit by the Torch yet.",
47 "🔥 Torch searched high and low, but this page got extinguished.",
48 "🔥 Even the brightest flame can't illuminate this missing page.",
49 "🔥 Torch doesn't know this ditty, but it's got plenty of other hot tracks!",
50 "🔥 This route went up in smoke before Torch could catch it.",
51 "🔥 Torch's flame doesn't reach this corner of the web.",
52 "🔥 Page not found - looks like this one slipped through the fire.",
53 "🔥 Torch is blazing bright, but this path remains in the dark.",
54 "🔥 This URL got burned in the great page fire of... well, never.",
55 ];
56
57 let now = std::time::SystemTime::now()
58 .duration_since(std::time::UNIX_EPOCH)
59 .unwrap_or_default()
60 .as_secs();
61
62 let index = (now / 5) as usize % messages.len(); messages[index]
64 }
65
66 pub fn render_error(&self, status_code: u16, message: Option<&str>, _req: &Request) -> Response {
68 let status = http::StatusCode::from_u16(status_code).unwrap_or(http::StatusCode::INTERNAL_SERVER_ERROR);
69
70 if let Some(custom_html) = self.custom_pages.get(&status_code) {
72 return Response::with_status(status)
73 .header("Content-Type", "text/html; charset=utf-8")
74 .body(custom_html.clone());
75 }
76
77 let html = if self.use_default_styling {
79 self.generate_styled_error_page(status_code, message)
80 } else {
81 self.generate_plain_error_page(status_code, message)
82 };
83
84 Response::with_status(status)
85 .header("Content-Type", "text/html; charset=utf-8")
86 .body(html)
87 }
88
89 fn generate_styled_error_page(&self, status_code: u16, message: Option<&str>) -> String {
91 let (title, description) = self.get_error_info(status_code);
92 let message = message.unwrap_or(description);
93
94 format!(r#"<!DOCTYPE html>
95<html lang="en">
96<head>
97 <meta charset="UTF-8">
98 <meta name="viewport" content="width=device-width, initial-scale=1.0">
99 <title>{} - Torch</title>
100 <style>
101 * {{
102 margin: 0;
103 padding: 0;
104 box-sizing: border-box;
105 }}
106
107 body {{
108 font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
109 background: linear-gradient(135deg, #1a1a1a 0%, #2d2d2d 100%);
110 color: #ffffff;
111 min-height: 100vh;
112 display: flex;
113 align-items: center;
114 justify-content: center;
115 padding: 20px;
116 }}
117
118 .error-container {{
119 text-align: center;
120 max-width: 600px;
121 width: 100%;
122 }}
123
124 .logo {{
125 width: 120px;
126 height: 120px;
127 margin: 0 auto 30px;
128 background: url('data:image/svg+xml;base64,{}') no-repeat center center;
129 background-size: contain;
130 opacity: 0.9;
131 }}
132
133 .error-code {{
134 font-size: 8rem;
135 font-weight: 300;
136 background: linear-gradient(135deg, #ff6b35, #f7931e);
137 -webkit-background-clip: text;
138 -webkit-text-fill-color: transparent;
139 background-clip: text;
140 margin-bottom: 20px;
141 line-height: 1;
142 }}
143
144 .error-title {{
145 font-size: 2.5rem;
146 font-weight: 600;
147 margin-bottom: 20px;
148 color: #ffffff;
149 }}
150
151 .error-message {{
152 font-size: 1.2rem;
153 color: #cccccc;
154 margin-bottom: 40px;
155 line-height: 1.6;
156 }}
157
158 .actions {{
159 display: flex;
160 gap: 20px;
161 justify-content: center;
162 flex-wrap: wrap;
163 }}
164
165 .btn {{
166 padding: 12px 24px;
167 border: none;
168 border-radius: 8px;
169 font-size: 1rem;
170 font-weight: 500;
171 text-decoration: none;
172 cursor: pointer;
173 transition: all 0.3s ease;
174 display: inline-block;
175 }}
176
177 .btn-primary {{
178 background: linear-gradient(135deg, #ff6b35, #f7931e);
179 color: white;
180 }}
181
182 .btn-primary:hover {{
183 transform: translateY(-2px);
184 box-shadow: 0 8px 25px rgba(255, 107, 53, 0.3);
185 }}
186
187 .btn-secondary {{
188 background: transparent;
189 color: #cccccc;
190 border: 2px solid #555;
191 }}
192
193 .btn-secondary:hover {{
194 background: #555;
195 color: white;
196 }}
197
198 .footer {{
199 margin-top: 60px;
200 color: #888;
201 font-size: 0.9rem;
202 }}
203
204 @media (max-width: 768px) {{
205 .error-code {{
206 font-size: 6rem;
207 }}
208
209 .error-title {{
210 font-size: 2rem;
211 }}
212
213 .error-message {{
214 font-size: 1rem;
215 }}
216
217 .actions {{
218 flex-direction: column;
219 align-items: center;
220 }}
221
222 .btn {{
223 width: 200px;
224 }}
225 }}
226 </style>
227</head>
228<body>
229 <div class="error-container">
230 <div class="logo"></div>
231 <div class="error-code">{}</div>
232 <h1 class="error-title">{}</h1>
233 <p class="error-message">{}</p>
234
235 <div class="actions">
236 <a href="/" class="btn btn-primary">🏠 Go Home</a>
237 <a href="javascript:history.back()" class="btn btn-secondary">← Go Back</a>
238 </div>
239
240 <div class="footer">
241 <p>Powered by <strong>Torch</strong> 🔥</p>
242 </div>
243 </div>
244</body>
245</html>"#,
246 title,
247 self.get_torch_logo_base64(),
248 status_code,
249 title,
250 message
251 )
252 }
253
254 fn generate_plain_error_page(&self, status_code: u16, message: Option<&str>) -> String {
256 let (title, description) = self.get_error_info(status_code);
257 let message = message.unwrap_or(description);
258
259 format!(r#"<!DOCTYPE html>
260<html lang="en">
261<head>
262 <meta charset="UTF-8">
263 <meta name="viewport" content="width=device-width, initial-scale=1.0">
264 <title>{} - Error</title>
265</head>
266<body>
267 <h1>{} {}</h1>
268 <p>{}</p>
269 <hr>
270 <p><a href="/">Go Home</a> | <a href="javascript:history.back()">Go Back</a></p>
271</body>
272</html>"#, title, status_code, title, message)
273 }
274
275 fn get_error_info(&self, status_code: u16) -> (&'static str, &'static str) {
277 match status_code {
278 400 => ("Bad Request", "🔥 Torch couldn't parse that request - try fanning the flames differently!"),
279 401 => ("Unauthorized", "🔥 You need to light your credentials before entering this flame zone."),
280 403 => ("Forbidden", "🔥 This area is protected by firewall - Torch can't let you pass."),
281 404 => ("Page Not Found", self.get_404_message()),
282 405 => ("Method Not Allowed", "🔥 That HTTP method got extinguished - try a different approach."),
283 408 => ("Request Timeout", "🔥 Your request took too long and the flame went out."),
284 418 => ("I'm a teapot", "🫖 Torch is a flame, not a teapot - but we appreciate the confusion!"),
285 429 => ("Too Many Requests", "🔥 Whoa there! You're making Torch burn too bright - slow down a bit."),
286 500 => ("Internal Server Error", "🔥 Torch had a flare-up! Our engineers are working to contain the blaze."),
287 502 => ("Bad Gateway", "🔥 The upstream server sent smoke signals we couldn't decode."),
288 503 => ("Service Unavailable", "🔥 Torch is temporarily dimmed for maintenance - we'll be back blazing soon!"),
289 504 => ("Gateway Timeout", "🔥 The upstream server's flame went out before we could connect."),
290 _ => ("Error", "🔥 Something unexpected happened in the flame chamber."),
291 }
292 }
293
294 fn get_404_message(&self) -> &'static str {
296 let messages = [
298 "🔥 Torch doesn't know this route, but the flame burns eternal!",
299 "🔥 This path hasn't been lit by the Torch yet.",
300 "🔥 Torch searched high and low, but this page got extinguished.",
301 "🔥 Even the brightest flame can't illuminate this missing page.",
302 "🔥 Torch doesn't know this ditty, but it's got plenty of other hot tracks!",
303 "🔥 This route went up in smoke before Torch could catch it.",
304 "🔥 Torch's flame doesn't reach this corner of the web.",
305 "🔥 Page not found - looks like this one slipped through the fire.",
306 "🔥 Torch is blazing bright, but this path remains in the dark.",
307 "🔥 This URL got burned in the great page fire of... well, never.",
308 ];
309
310 let now = std::time::SystemTime::now()
313 .duration_since(std::time::UNIX_EPOCH)
314 .unwrap_or_default()
315 .as_secs();
316
317 let index = (now / 10) as usize % messages.len(); messages[index]
319 }
320
321 fn get_torch_logo_base64(&self) -> &'static str {
323 "PHN2ZyB3aWR0aD0iMTIwIiBoZWlnaHQ9IjEyMCIgdmlld0JveD0iMCAwIDEyMCAxMjAiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CjxkZWZzPgo8bGluZWFyR3JhZGllbnQgaWQ9ImZsYW1lR3JhZGllbnQiIHgxPSIwJSIgeTE9IjAlIiB4Mj0iMTAwJSIgeTI9IjEwMCUiPgo8c3RvcCBvZmZzZXQ9IjAlIiBzdHlsZT0ic3RvcC1jb2xvcjojZmZkNzAwO3N0b3Atb3BhY2l0eToxIiAvPgo8c3RvcCBvZmZzZXQ9IjUwJSIgc3R5bGU9InN0b3AtY29sb3I6I2ZmNmIzNTtzdG9wLW9wYWNpdHk6MSIgLz4KPHN0b3Agb2Zmc2V0PSIxMDAlIiBzdHlsZT0ic3RvcC1jb2xvcjojZjc5MzFlO3N0b3Atb3BhY2l0eToxIiAvPgo8L2xpbmVhckdyYWRpZW50Pgo8bGluZWFyR3JhZGllbnQgaWQ9InRvcmNoR3JhZGllbnQiIHgxPSIwJSIgeTE9IjAlIiB4Mj0iMTAwJSIgeTI9IjEwMCUiPgo8c3RvcCBvZmZzZXQ9IjAlIiBzdHlsZT0ic3RvcC1jb2xvcjojZGRhNTIwO3N0b3Atb3BhY2l0eToxIiAvPgo8c3RvcCBvZmZzZXQ9IjEwMCUiIHN0eWxlPSJzdG9wLWNvbG9yOiNiODg2MWE7c3RvcC1vcGFjaXR5OjEiIC8+CjwvbGluZWFyR3JhZGllbnQ+CjwvZGVmcz4KCjwhLS0gTWFpbiBGbGFtZSAtLT4KPHA+CjxwYXRoIGQ9Ik02MCAyMEM2MCAyMCA0MiAxOCA0MiAzOEM0MiA1OCA2MCA2NSA2MCA2NUM2MCA2NSA3OCA1OCA3OCAzOEM3OCAxOCA2MCAyMCA2MCAyMFoiIGZpbGw9InVybCgjZmxhbWVHcmFkaWVudCkiLz4KPC9wPgoKPCEtLSBJbm5lciBGbGFtZSAtLT4KPHA+CjxwYXRoIGQ9Ik02MCAzMEM2MCAzMCA0OCAyOCA0OCA0NEM0OCA1NiA2MCA2MCA2MCA2MEM2MCA2MCA3MiA1NiA3MiA0NEM3MiAyOCA2MCAzMCA2MCAzMFoiIGZpbGw9InVybCgjZmxhbWVHcmFkaWVudCkiIG9wYWNpdHk9IjAuOCIvPgo8L3A+Cgo8IS0tIFNtYWxsIEZsYW1lIC0tPgo8cD4KPHA+CjxwYXRoIGQ9Ik02MCA0MEM2MCA0MCA1MiAzOCA1MiA0OEM1MiA1NCA2MCA1NiA2MCA1NkM2MCA1NiA2OCA1NCA2OCA0OEM2OCAzOCA2MCA0MCA2MCA0MFoiIGZpbGw9InVybCgjZmxhbWVHcmFkaWVudCkiIG9wYWNpdHk9IjAuNiIvPgo8L3A+Cgo8IS0tIFRvcmNoIEhhbmRsZSAtLT4KPHJlY3QgeD0iNTQiIHk9IjY1IiB3aWR0aD0iMTIiIGhlaWdodD0iMzUiIGZpbGw9InVybCgjdG9yY2hHcmFkaWVudCkiIHJ4PSIyIi8+Cgo8IS0tIEhhbmRsZSBEZXRhaWxzIC0tPgo8cmVjdCB4PSI1NiIgeT0iNzAiIHdpZHRoPSI4IiBoZWlnaHQ9IjIiIGZpbGw9IiNhYTc0MTYiLz4KPHJlY3QgeD0iNTYiIHk9Ijc1IiB3aWR0aD0iOCIgaGVpZ2h0PSIyIiBmaWxsPSIjYWE3NDE2Ii8+CjxyZWN0IHg9IjU2IiB5PSI4MCIgd2lkdGg9IjgiIGhlaWdodD0iMiIgZmlsbD0iI2FhNzQxNiIvPgoKPCEtLSBUb3JjaCBCYXNlIC0tPgo8ZWxsaXBzZSBjeD0iNjAiIGN5PSIxMDAiIHJ4PSIxOCIgcnk9IjgiIGZpbGw9InVybCgjdG9yY2hHcmFkaWVudCkiLz4KPGVsbGlwc2UgY3g9IjYwIiBjeT0iOTgiIHJ4PSIxNCIgcnk9IjYiIGZpbGw9IiNkZGE1MjAiLz4KCjx0ZXh0IHg9IjYwIiB5PSIxMTUiIGZvbnQtZmFtaWx5PSJBcmlhbCwgc2Fucy1zZXJpZiIgZm9udC1zaXplPSIxMCIgZm9udC13ZWlnaHQ9ImJvbGQiIGZpbGw9IiNkZGE1MjAiIHRleHQtYW5jaG9yPSJtaWRkbGUiPlRPUkNIPC90ZXh0Pgo8L3N2Zz4="
325 }
326}
327
328impl Default for ErrorPages {
329 fn default() -> Self {
330 Self::new()
331 }
332}