torch_web/
error_pages.rs

1use crate::{Request, Response};
2use std::collections::HashMap;
3
4/// Error page configuration and rendering
5#[derive(Clone)]
6pub struct ErrorPages {
7    custom_pages: HashMap<u16, String>,
8    use_default_styling: bool,
9}
10
11impl ErrorPages {
12    /// Create a new error pages handler with default styling
13    pub fn new() -> Self {
14        Self {
15            custom_pages: HashMap::new(),
16            use_default_styling: true,
17        }
18    }
19
20    /// Disable default styling (use plain HTML)
21    pub fn without_default_styling(mut self) -> Self {
22        self.use_default_styling = false;
23        self
24    }
25
26    /// Set a custom error page for a specific status code
27    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    /// Set a custom 404 page
33    pub fn custom_404(self, html: String) -> Self {
34        self.custom_page(404, html)
35    }
36
37    /// Set a custom 500 page
38    pub fn custom_500(self, html: String) -> Self {
39        self.custom_page(500, html)
40    }
41
42    /// Get a random fun 404 message (Sinatra-inspired with Torch flair)
43    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(); // Change every 5 seconds for more variety
63        messages[index]
64    }
65
66    /// Generate an error response for the given status code
67    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        // Check for custom page first
71        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        // Generate default error page
78        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    /// Generate a beautifully styled error page with the Torch logo
90    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    /// Generate a plain error page without styling
255    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    /// Get error information for common status codes
276    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    /// Get a fun, Sinatra-inspired 404 message with Torch flair
295    fn get_404_message(&self) -> &'static str {
296        // Rotate through different fun messages inspired by Sinatra's "Sinatra doesn't know this ditty"
297        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        // Use a simple hash of the current time to pseudo-randomly select a message
311        // This gives variety while being deterministic within the same second
312        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(); // Change every 10 seconds
318        messages[index]
319    }
320
321    /// Get the Torch logo as base64 SVG
322    fn get_torch_logo_base64(&self) -> &'static str {
323        // Base64 encoded SVG version of the Torch logo - beautiful flame design
324        "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}