Coverage for nexios\middlewares\errors\server_error_handler.py: 30%
151 statements
« prev ^ index » next coverage.py v7.8.0, created at 2025-05-21 20:31 +0100
« prev ^ index » next coverage.py v7.8.0, created at 2025-05-21 20:31 +0100
1from nexios.middlewares.base import BaseMiddleware
2from nexios.http import Request, Response
3from nexios.config import get_config
4import traceback, html, sys, inspect, typing, platform, json, datetime, uuid, os
5from nexios.logging import DEBUG, create_logger
6from nexios.__main__ import __version__ as nexios_version
8logger = create_logger(__name__, log_level=DEBUG)
9STYLES = """
11:root {
12 --primary: #10b981;
13 --primary-dark: #059669;
14 --primary-light: #d1fae5;
15 --secondary: #14b8a6;
16 --background: #0f172a;
17 --surface: #1e293b;
18 --surface-light: #334155;
19 --error: #ef4444;
20 --text: #f1f5f9;
21 --text-secondary: #94a3b8;
22 --text-tertiary: #64748b;
23 --border: #334155;
24 --code-bg: #1e293b;
25 --code-fg: #a5f3fc;
26 --highlight: #eab308;
27 --highlight-bg: rgba(234, 179, 8, 0.1);
28}
30* {
31 box-sizing: border-box;
32 margin: 0;
33 padding: 0;
34}
36body {
37 font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
38 background-color: var(--background);
39 color: var(--text);
40 margin: 0;
41 padding: 0;
42 line-height: 1.6;
43 font-size: 15px;
44}
46h1, h2, h3, h4, h5, h6 {
47 font-weight: 600;
48 line-height: 1.4;
49}
51h1 {
52 color: var(--primary);
53 font-size: 24px;
54 margin-bottom: 4px;
55}
57h2 {
58 color: var(--text);
59 font-size: 18px;
60 margin-top: 4px;
61 margin-bottom: 16px;
62 font-weight: 500;
63}
65h3 {
66 color: var(--primary);
67 font-size: 16px;
68 margin-top: 16px;
69 margin-bottom: 10px;
70 border-bottom: 1px solid var(--border);
71 padding-bottom: 5px;
72}
74.container {
75 max-width: 1200px;
76 margin: 0 auto;
77 padding: 24px 16px;
78}
80.section {
81 margin-bottom: 24px;
82 border: 1px solid var(--border);
83 background: var(--surface);
84 border-radius: 8px;
85 overflow: hidden;
86 box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
87}
89.section-title {
90 background-color: var(--primary);
91 color: var(--background);
92 padding: 12px 16px;
93 font-size: 16px;
94 font-weight: 600;
95 display: flex;
96 justify-content: space-between;
97 align-items: center;
98}
100.section-content {
101 padding: 16px;
102}
104.traceback-container {
105 background: var(--surface);
106 border-radius: 8px;
107 overflow: hidden;
108}
110.traceback-title {
111 background-color: var(--primary);
112 color: var(--background);
113 padding: 12px 16px;
114 font-size: 16px;
115 font-weight: 600;
116 display: flex;
117 justify-content: space-between;
118 align-items: center;
119}
121.frame-line {
122 padding-left: 12px;
123 font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace;
124 color: var(--text);
125}
127.frame-filename {
128 font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace;
129 font-weight: 600;
130 color: var(--primary);
131}
133.center-line {
134 background-color: var(--primary-dark);
135 color: white;
136 padding: 6px 12px;
137 font-weight: 600;
138 border-radius: 4px;
139}
141.lineno {
142 margin-right: 8px;
143 color: var(--text-tertiary);
144 user-select: none;
145}
147.frame-title {
148 font-weight: 500;
149 padding: 12px 16px;
150 background-color: var(--surface-light);
151 color: var(--text);
152 font-size: 14px;
153 border-radius: 4px;
154 margin-bottom: 8px;
155 display: flex;
156 justify-content: space-between;
157 align-items: center;
158 border-left: 4px solid var(--primary);
159}
161.collapse-btn {
162 background: var(--primary);
163 color: var(--background);
164 border: none;
165 width: 24px;
166 height: 24px;
167 font-size: 14px;
168 cursor: pointer;
169 border-radius: 4px;
170 display: flex;
171 align-items: center;
172 justify-content: center;
173 margin-left: 10px;
174 transition: background-color 0.2s;
175}
177.collapse-btn:hover {
178 backgroun
179"""
181JS = """
182<script type="text/javascript">
183 function collapse(element){
184 const targetId = element.getAttribute("data-target-id");
185 const target = document.getElementById(targetId);
187 if (target.classList.contains("collapsed")){
188 element.innerHTML = "‒"; // Minus symbol
189 target.classList.remove("collapsed");
190 } else {
191 element.innerHTML = "+"; // Plus symbol
192 target.classList.add("collapsed");
193 }
194 }
196 function toggleSection(sectionId) {
197 const section = document.getElementById(sectionId);
198 const button = document.querySelector(`[data-section="${sectionId}"]`);
200 if (section.classList.contains("collapsed")) {
201 section.classList.remove("collapsed");
202 button.innerHTML = "‒"; // Minus symbol
203 } else {
204 section.classList.add("collapsed");
205 button.innerHTML = "+"; // Plus symbol
206 }
207 }
209 document.addEventListener('DOMContentLoaded', function() {
210 // Initialize all sections as expanded
211 const sections = document.querySelectorAll('.section-content');
212 sections.forEach(section => {
213 if (section.id !== 'traceback-section') {
214 section.classList.add('collapsed');
215 const button = document.querySelector(`[data-section="${section.id}"]`);
216 if (button) button.innerHTML = "+";
217 }
218 });
219 });
220</script>
221"""
222TEMPLATE = """
223<!DOCTYPE html>
224<html lang="en">
225 <head>
226 <meta charset="UTF-8">
227 <meta name="viewport" content="width=device-width, initial-scale=1.0">
228 <style type='text/css'>
229 {styles}
230 </style>
231 <title>Nexios Debug - {error_type}</title>
232 </head>
233 <body>
234 <div class="container">
235 <h1>Server Error</h1>
236 <h1>Nexios Debug - {error_type}</h1>
237 <!-- Traceback Section -->
239 <div class="section">
240 <div class="section-title">
241 <span>Request Information</span>
242 <button class="collapse-btn" data-section="request-section" onclick="toggleSection('request-section')">+</button>
243 </div>
244 <div id="request-section" class="section-content">
245 {request_info}
246 </div>
247 </div>
249 <div class="section">
250 <div class="section-title">
251 <span>Traceback</span>
252 <button class="collapse-btn" data-section="traceback-section" onclick="toggleSection('traceback-section')">‒</button>
253 </div>
254 <div id="traceback-section" class="section-content">
255 <div>{exc_html}</div>
256 </div>
257 </div>
259 <!-- Request Information Section -->
262 <!-- System Information Section -->
263 <div class="section">
264 <div class="section-title">
265 <span>System Information</span>
266 <button class="collapse-btn" data-section="system-section" onclick="toggleSection('system-section')">+</button>
267 </div>
268 <div id="system-section" class="section-content">
269 {system_info}
270 </div>
271 </div>
273 <!-- Suggestions Section -->
274 <div class="section">
275 <div class="section-title">
276 <span>Debugging Suggestions</span>
277 <button class="collapse-btn" data-section="suggestions-section" onclick="toggleSection('suggestions-section')">+</button>
278 </div>
279 <div id="suggestions-section" class="section-content">
280 {debugging_suggestions}
281 </div>
282 </div>
284 <!-- JSON Data Section -->
285 <div class="section">
286 <div class="section-title">
287 <span>Error JSON Data</span>
288 <button class="collapse-btn" data-section="json-section" onclick="toggleSection('json-section')">+</button>
289 </div>
290 <div id="json-section" class="section-content">
291 <div class="code-box">
292 <div class="code-header">Error Data (JSON)</div>
293 <pre class="code-content">{error_json}</pre>
294 </div>
295 </div>
296 </div>
297 </div>
298 {js}
299 </body>
300</html>
301"""
302FRAME_TEMPLATE = """
303<div>
304 <p class="frame-title">
305 File <span class="frame-filename">{frame_filename}</span>,
306 line <i>{frame_lineno}</i>,
307 in <b>{frame_name}</b>
308 <button class="collapse-btn" data-target-id="{frame_filename}-{frame_lineno}" onclick="collapse(this)">
309 {collapse_button}
310 </button>
311 </p>
312 <div id="{frame_filename}-{frame_lineno}" class="source-code {collapsed}">{code_context}</div>
313 {locals_html}
314</div>
315"""
317LINE = """
318<p><span class="frame-line">
319<span class="lineno">{lineno}.</span> {line}</span></p>
320"""
322CENTER_LINE = """
323<p class="center-line"><span class="frame-line">
324<span class="lineno">{lineno}.</span> {line}</span></p>
325"""
328ServerErrHandlerType = typing.Callable[[Request, Response, Exception], typing.Any]
331class ServerErrorMiddleware(BaseMiddleware):
332 def __init__(self, handler: typing.Optional[ServerErrHandlerType] = None):
333 self.handler = handler
335 async def __call__(
336 self,
337 request: Request,
338 response: Response,
339 next_middleware: typing.Callable[[], typing.Awaitable[Response]],
340 ) -> typing.Any:
341 # Store the current request for error context
342 self.current_request = request
343 # Get debug mode from config
344 self.debug = get_config().debug or True
346 try:
347 return await next_middleware()
348 except Exception as exc:
349 if self.handler:
350 response = await self.handler(request, response, exc)
351 if self.debug:
352 response = self.get_debug_response(request, response, exc)
353 else:
354 response = self.error_response(response)
356 err = traceback.format_exc()
357 logger.error(err)
358 return response
360 def error_response(self, res: Response):
361 return res.text("Internal Server Error", status_code=500)
363 def get_debug_response(
364 self, request: Request, response: Response, exc: Exception
365 ) -> Response:
366 accept = request.headers.get("accept", "")
367 if "text/html" in accept:
368 content: str = self.generate_html(exc)
369 return response.html(content, status_code=500)
370 content = self.generate_plain_text(exc)
371 return response.text(content, status_code=500)
373 def format_line(
374 self, index: int, line: str, frame_lineno: int, frame_index: int
375 ) -> str:
376 values: typing.Dict[str, typing.Any] = {
377 # HTML escape - line could contain < or >
378 "line": html.escape(line).replace(" ", " "),
379 "lineno": (frame_lineno - frame_index) + index,
380 }
382 if index != frame_index:
383 return LINE.format(**values)
384 return CENTER_LINE.format(**values)
386 def _format_locals(self, frame_locals: typing.Dict[str, typing.Any]) -> str:
387 """Format local variables for display in the error template."""
388 if not frame_locals:
389 return ""
391 locals_html = "<div class='stack-locals'><h4>Local Variables:</h4>\n"
392 for var_name, var_value in frame_locals.items():
393 try:
394 # Skip internal variables
395 if var_name.startswith("__") and var_name.endswith("__"):
396 continue
398 # Format value safely
399 value_str = html.escape(repr(var_value))
400 if len(value_str) > 500: # Truncate long values
401 value_str = value_str[:500] + "..."
403 locals_html += f"<div><span style='color: #f39c12;'>{html.escape(var_name)}</span> = {value_str}</div>\n"
404 except Exception:
405 locals_html += f"<div><span style='color: #f39c12;'>{html.escape(var_name)}</span> = <error displaying value></div>\n"
407 locals_html += "</div>"
408 return locals_html
410 def generate_frame_html(self, frame: inspect.FrameInfo, is_collapsed: bool) -> str:
411 code_context: str = "".join( # type:ignore
412 self.format_line(
413 index,
414 line,
415 frame.lineno,
416 frame.index, # type:ignore
417 )
418 for index, line in enumerate(frame.code_context or []) # type:ignore
419 )
421 # Format local variables if available
422 locals_html = (
423 self._format_locals(frame.frame.f_locals) if hasattr(frame, "frame") else ""
424 )
426 values: typing.Dict[str, typing.Any] = {
427 "frame_filename": html.escape(frame.filename),
428 "frame_lineno": frame.lineno,
429 "frame_name": html.escape(frame.function),
430 "code_context": code_context,
431 "collapsed": "collapsed" if is_collapsed else "",
432 "collapse_button": "+" if is_collapsed else "‒",
433 "locals_html": locals_html,
434 }
435 return FRAME_TEMPLATE.format(**values)
437 def generate_plain_text(self, exc: Exception) -> str:
438 return "".join(traceback.format_exception(type(exc), exc, exc.__traceback__))
440 def _format_request_info(self, request: Request) -> str:
441 """Format request information for display in the error template."""
442 method = request.method
443 url = str(request.url)
445 # General request info
446 _html = f"""
447 <div class="info-grid">
448 <div class="info-block">
449 <h3>Request Details</h3>
450 <div class="info-item">
451 <div class="info-label">Method:</div>
452 <div class="info-value">{html.escape(method)}</div>
453 </div>
454 <div class="info-item">
455 <div class="info-label">URL:</div>
456 <div class="info-value">{html.escape(url)}</div>
457 </div>
458 <div class="info-item">
459 <div class="info-label">Path:</div>
460 <div class="info-value">{html.escape(request.path)}</div>
461 </div>
463 </div>
465 <div class="info-block">
466 <h3>Headers</h3>
467 <table class="key-value-table">
468 """
470 # Add headers
471 for name, value in request.headers.items():
472 _html += f"""
473 <tr>
474 <td>{html.escape(name)}</td>
475 <td>{html.escape(value)}</td>
476 </tr>
477 """
479 _html += """
480 </table>
481 </div>
482 </div>
483 """
485 # Add query parameters if available
486 if hasattr(request, "query_params") and request.query_params:
487 _html += """
488 <div class="info-block">
489 <h3>Query Parameters</h3>
490 <table class="key-value-table">
491 """
493 for name, value in request.query_params.items():
494 _html += f"""
495 <tr>
496 <td>{html.escape(name)}</td>
497 <td>{html.escape(str(value))}</td>
498 </tr>
499 """
501 _html += """
502 </table>
503 </div>
504 """
506 return _html
508 def _format_system_info(self) -> str:
509 """Format system information for display in the error template."""
510 python_version = f"{sys.version_info.major}.{sys.version_info.minor}.{sys.version_info.micro}"
512 _html = f"""
513 <div class="info-grid">
514 <div class="info-block">
515 <h3>Nexios</h3>
516 <div class="info-item">
517 <div class="info-label">Nexios Version:</div>
518 <div class="info-value">{html.escape(nexios_version)}</div>
519 </div>
520 <div class="info-item">
521 <div class="info-label">Debug Mode:</div>
522 <div class="info-value">{self.debug}</div>
523 </div>
524 </div>
526 <div class="info-block">
527 <h3>Python</h3>
528 <div class="info-item">
529 <div class="info-label">Python Version:</div>
530 <div class="info-value">{html.escape(python_version)}</div>
531 </div>
532 <div class="info-item">
533 <div class="info-label">Python Path:</div>
534 <div class="info-value">{html.escape(sys.executable)}</div>
535 </div>
536 <div class="info-item">
537 <div class="info-label">Python Implementation:</div>
538 <div class="info-value">{html.escape(platform.python_implementation())}</div>
539 </div>
540 </div>
541 </div>
543 <div class="info-grid">
544 <div class="info-block">
545 <h3>System</h3>
546 <div class="info-item">
547 <div class="info-label">Platform:</div>
548 <div class="info-value">{html.escape(platform.platform())}</div>
549 </div>
550 <div class="info-item">
551 <div class="info-label">OS:</div>
552 <div class="info-value">{html.escape(platform.system())} {html.escape(platform.release())}</div>
553 </div>
554 <div class="info-item">
555 <div class="info-label">Architecture:</div>
556 <div class="info-value">{html.escape(platform.machine())}</div>
557 </div>
558 </div>
560 <div class="info-block">
561 <h3>Environment</h3>
562 <div class="info-item">
563 <div class="info-label">Process ID:</div>
564 <div class="info-value">{os.getpid()}</div>
565 </div>
566 <div class="info-item">
567 <div class="info-label">Current Directory:</div>
568 <div class="info-value">{html.escape(os.getcwd())}</div>
569 </div>
570 <div class="info-item">
571 <div class="info-label">Python Path:</div>
572 <div class="info-value">{html.escape(str(sys.path))}</div>
573 </div>
574 </div>
575 </div>
576 """
578 return _html
580 def _generate_error_json(self, exc: Exception, exc_type_str: str) -> str:
581 """Generate a JSON representation of the error for debugging."""
582 error_data: typing.Dict[str, typing.Any] = {
583 "error": {
584 "type": exc_type_str,
585 "message": str(exc),
586 "traceback": traceback.format_exc(),
587 },
588 "system": {
589 "nexios_version": nexios_version,
590 "python_version": f"{sys.version_info.major}.{sys.version_info.minor}.{sys.version_info.micro}",
591 "platform": platform.platform(),
592 "debug_mode": self.debug,
593 },
594 "timestamp": datetime.datetime.now().isoformat(),
595 "error_id": str(uuid.uuid4()),
596 }
598 try:
599 return json.dumps(error_data, indent=2)
600 except:
601 # If JSON serialization fails, provide a simplified version
602 return json.dumps(
603 {
604 "error": {
605 "type": exc_type_str,
606 "message": str(exc),
607 "note": "Full error data could not be serialized to JSON",
608 }
609 },
610 indent=2,
611 )
613 def _generate_debugging_suggestions(self, exc: Exception, exc_type_str: str) -> str:
614 """Generate debugging suggestions based on the error type."""
615 suggestions: typing.List[typing.Dict[str, str]] = []
617 # Common error types and suggestions
618 if "ImportError" in exc_type_str or "ModuleNotFoundError" in exc_type_str:
619 suggestions.append(
620 {
621 "title": "Missing Module",
622 "text": "This error typically occurs when Python cannot find a required module. Check that all dependencies are installed correctly. Try running 'pip install -r requirements.txt'.",
623 }
624 )
626 elif "SyntaxError" in exc_type_str:
627 suggestions.append(
628 {
629 "title": "Syntax Error",
630 "text": "There's a syntax error in your code. Check the line indicated in the traceback for mismatched parentheses, missing colons, or incorrect indentation.",
631 }
632 )
634 elif "AttributeError" in exc_type_str:
635 suggestions.append(
636 {
637 "title": "Attribute Error",
638 "text": "You're trying to access an attribute or method that doesn't exist on the object. Check for typos or make sure the object is of the expected type before accessing its attributes.",
639 }
640 )
642 elif "KeyError" in exc_type_str:
643 suggestions.append(
644 {
645 "title": "Key Error",
646 "text": "You're trying to access a dictionary key that doesn't exist. Make sure the key exists before trying to access it, or use dictionary.get(key) method with a default value.",
647 }
648 )
650 elif "NameError" in exc_type_str:
651 suggestions.append(
652 {
653 "title": "Name Error",
654 "text": "You're trying to use a variable that hasn't been defined. Check for typos or make sure to define the variable before using it.",
655 }
656 )
658 elif "TypeError" in exc_type_str:
659 suggestions.append(
660 {
661 "title": "Type Error",
662 "text": "An operation is being performed on an object of an inappropriate type. Check the types of your variables and make sure they match what the operation expects.",
663 }
664 )
666 elif "ValueError" in exc_type_str:
667 suggestions.append(
668 {
669 "title": "Value Error",
670 "text": "An operation is receiving an argument with the right type but an inappropriate value. Check the value of the arguments you're passing to functions.",
671 }
672 )
674 elif "IndexError" in exc_type_str:
675 suggestions.append(
676 {
677 "title": "Index Error",
678 "text": "You're trying to access an index that's out of range. Make sure the index is valid before accessing it, or use a try/except block to handle the error.",
679 }
680 )
682 elif "FileNotFoundError" in exc_type_str:
683 suggestions.append(
684 {
685 "title": "File Not Found",
686 "text": "The system cannot find the file specified. Check the file path and make sure the file exists.",
687 }
688 )
690 elif "PermissionError" in exc_type_str:
691 suggestions.append(
692 {
693 "title": "Permission Error",
694 "text": "You don't have permission to access the specified file or directory. Check the file permissions or run the application with higher privileges.",
695 }
696 )
698 # Add a general debugging strategy for all errors
699 suggestions.append(
700 {
701 "title": "General Debugging Steps",
702 "text": "1. Check the traceback to find where the error occurred.<br>2. Review the variables at that point using the local variables section.<br>3. Add logging statements around the error to track variable values.<br>4. Use a debugger to step through the code execution.",
703 }
704 )
706 # Format the suggestions as HTML
707 _html = ""
708 for suggestion in suggestions:
709 _html += f"""
710 <div class="suggestion">
711 <div class="suggestion-title">{html.escape(suggestion["title"])}</div>
712 <div>{suggestion["text"]}</div>
713 </div>
714 """
716 return _html
718 def generate_html(self, exc: Exception, limit: int = 7) -> str:
719 """Generate an enhanced HTML page for displaying error information."""
720 # Generate a unique error ID for tracking
721 error_id = str(uuid.uuid4())
722 timestamp = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
724 # Get exception information
725 traceback_obj = traceback.TracebackException.from_exception(
726 exc, capture_locals=True
727 )
729 # Get exception type name
730 if sys.version_info >= (3, 13):
731 exc_type_str = traceback_obj.exc_type_str
732 else:
733 exc_type_str = traceback_obj.exc_type.__name__
735 # Format the error message
736 error = f"{html.escape(exc_type_str)}: {html.escape(str(traceback_obj))}"
738 # Generate traceback HTML
739 exc_html = ""
740 is_collapsed = False
741 exc_traceback = exc.__traceback__
742 if exc_traceback is not None:
743 frames = inspect.getinnerframes(exc_traceback, limit)
744 for frame in reversed(frames):
745 exc_html += self.generate_frame_html(frame, is_collapsed)
746 is_collapsed = True
748 # Get request information if available
749 try:
750 request_info = self._format_request_info(self.current_request)
751 except Exception as e:
752 request_info = f"<div class='info-block'><h3>Error retrieving request information</h3><p>{html.escape(str(e))}</p></div>"
754 # Get system information
755 try:
756 system_info = self._format_system_info()
757 except Exception as e:
758 system_info = f"<div class='info-block'><h3>Error retrieving system information</h3><p>{html.escape(str(e))}</p></div>"
760 # Generate debugging suggestions
761 try:
762 debugging_suggestions = self._generate_debugging_suggestions(
763 exc, exc_type_str
764 )
765 except Exception as e:
766 debugging_suggestions = f"<div class='info-block'><h3>Error generating debugging suggestions</h3><p>{html.escape(str(e))}</p></div>"
768 # Generate JSON representation of the error
769 try:
770 error_json = html.escape(self._generate_error_json(exc, exc_type_str))
771 except Exception as e:
772 error_json = html.escape(f"Error generating JSON data: {str(e)}")
774 # Put everything together in the template
775 return TEMPLATE.format(
776 styles=STYLES,
777 js=JS,
778 error=error,
779 error_type=html.escape(exc_type_str),
780 error_id=error_id,
781 timestamp=timestamp,
782 exc_html=exc_html,
783 request_info=request_info,
784 system_info=system_info,
785 debugging_suggestions=debugging_suggestions,
786 error_json=error_json,
787 )