Coverage for nexios\middlewares\errors\server_error_handler.py: 30%

1 from nexios.middlewares.base import BaseMiddleware

2 from nexios.http import Request, Response

3 from nexios.config import get_config

4 import traceback, html, sys, inspect, typing, platform, json, datetime, uuid, os

5 from nexios.logging import DEBUG, create_logger

6 from nexios.__main__ import __version__ as nexios_version

7

8 logger = create_logger(__name__, log_level=DEBUG)

9 STYLES = """

10

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}

29

30* {

31 box-sizing: border-box;

32 margin: 0;

33 padding: 0;

34}

35

36 body {

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}

45

46 h1, h2, h3, h4, h5, h6 {

47 font-weight: 600;

48 line-height: 1.4;

49}

50

51 h1 {

52 color: var(--primary);

53 font-size: 24px;

54 margin-bottom: 4px;

55}

56

57 h2 {

58 color: var(--text);

59 font-size: 18px;

60 margin-top: 4px;

61 margin-bottom: 16px;

62 font-weight: 500;

63}

64

65 h3 {

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}

73

74.container {

75 max-width: 1200px;

76 margin: 0 auto;

77 padding: 24px 16px;

78}

79

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}

88

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}

99

100.section-content {

101 padding: 16px;

102}

103

104.traceback-container {

105 background: var(--surface);

106 border-radius: 8px;

107 overflow: hidden;

108}

109

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}

120

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}

126

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}

132

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}

140

141.lineno {

142 margin-right: 8px;

143 color: var(--text-tertiary);

144 user-select: none;

145}

146

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}

160

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}

176

177.collapse-btn:hover {

178 backgroun

179"""

180

181 JS = """

182<script type="text/javascript">

183 function collapse(element){

184 const targetId = element.getAttribute("data-target-id");

185 const target = document.getElementById(targetId);

186

187 if (target.classList.contains("collapsed")){

188 element.innerHTML = "&#8210;"; // Minus symbol

189 target.classList.remove("collapsed");

190 } else {

191 element.innerHTML = "+"; // Plus symbol

192 target.classList.add("collapsed");

193 }

194 }

195

196 function toggleSection(sectionId) {

197 const section = document.getElementById(sectionId);

198 const button = document.querySelector(`[data-section="${sectionId}"]`);

199

200 if (section.classList.contains("collapsed")) {

201 section.classList.remove("collapsed");

202 button.innerHTML = "&#8210;"; // Minus symbol

203 } else {

204 section.classList.add("collapsed");

205 button.innerHTML = "+"; // Plus symbol

206 }

207 }

208

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"""

222 TEMPLATE = """

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 -->

238

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>

248

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')">&#8210;</button>

253 </div>

254 <div id="traceback-section" class="section-content">

255 <div>{exc_html}</div>

256 </div>

257 </div>

258

259 <!-- Request Information Section -->

260

261

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>

272

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>

283

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"""

302 FRAME_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"""

316

317 LINE = """

318<p><span class="frame-line">

319<span class="lineno">{lineno}.</span> {line}</span></p>

320"""

321

322 CENTER_LINE = """

323<p class="center-line"><span class="frame-line">

324<span class="lineno">{lineno}.</span> {line}</span></p>

325"""

326

327

328 ServerErrHandlerType = typing.Callable[[Request, Response, Exception], typing.Any]

329

330

331 class ServerErrorMiddleware(BaseMiddleware):

332 def __init__(self, handler: typing.Optional[ServerErrHandlerType] = None):

333 self.handler = handler

334

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

345

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)

355

356 err = traceback.format_exc()

357 logger.error(err)

358 return response

359

360 def error_response(self, res: Response):

361 return res.text("Internal Server Error", status_code=500)

362

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)

372

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(" ", "&nbsp"),

379 "lineno": (frame_lineno - frame_index) + index,

380 }

381

382 if index != frame_index:

383 return LINE.format(**values)

384 return CENTER_LINE.format(**values)

385

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 ""

390

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

397

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] + "..."

402

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"

406

407 locals_html += "</div>"

408 return locals_html

409

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 )

420

421 # Format local variables if available

422 locals_html = (

423 self._format_locals(frame.frame.f_locals) if hasattr(frame, "frame") else ""

424 )

425

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 "&#8210;",

433 "locals_html": locals_html,

434 }

435 return FRAME_TEMPLATE.format(**values)

436

437 def generate_plain_text(self, exc: Exception) -> str:

438 return "".join(traceback.format_exception(type(exc), exc, exc.__traceback__))

439

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)

444

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>

462

463 </div>

464

465 <div class="info-block">

466 <h3>Headers</h3>

467 <table class="key-value-table">

468 """

469

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 """

478

479 _html += """

480 </table>

481 </div>

482 </div>

483 """

484

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 """

492

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 """

500

501 _html += """

502 </table>

503 </div>

504 """

505

506 return _html

507

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}"

511

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>

525

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>

542

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>

559

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 """

577

578 return _html

579

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 }

597

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 )

612

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]] = []

616

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 )

625

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 )

633

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 )

641

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 )

649

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 )

657

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 )

665

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 )

673

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 )

681

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 )

689

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 )

697

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 )

705

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 """

715

716 return _html

717

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")

723

724 # Get exception information

725 traceback_obj = traceback.TracebackException.from_exception(

726 exc, capture_locals=True

727 )

728

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__

734

735 # Format the error message

736 error = f"{html.escape(exc_type_str)}: {html.escape(str(traceback_obj))}"

737

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

747

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>"

753

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>"

759

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>"

767

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)}")

773

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 )