1
from __future__ import annotations
8
from starlette._utils import is_async_callable
9
from starlette.concurrency import run_in_threadpool
10
from starlette.requests import Request
11
from starlette.responses import HTMLResponse, PlainTextResponse, Response
12
from starlette.types import ASGIApp, ExceptionHandler, Message, Receive, Scope, Send
18
.traceback-container {
19
border: 1px solid #038BB8;
22
background-color: #038BB8;
30
font-family: monospace;
33
font-family: monospace;
36
background-color: #038BB8;
38
padding: 5px 0px 5px 5px;
45
padding: 10px 10px 10px 10px;
46
background-color: #E4F4FD;
50
border: 1px solid #c7dce8;
54
padding: 0px 5px 1px 5px;
55
border: solid 1px #96aebb;
69
<script type="text/javascript">
70
function collapse(element){
71
const frameId = element.getAttribute("data-frame-id");
72
const frame = document.getElementById(frameId);
74
if (frame.classList.contains("collapsed")){
75
element.innerHTML = "‒";
76
frame.classList.remove("collapsed");
78
element.innerHTML = "+";
79
frame.classList.add("collapsed");
88
<style type='text/css'>
91
<title>Starlette Debugger</title>
94
<h1>500 Server Error</h1>
96
<div class="traceback-container">
97
<p class="traceback-title">Traceback</p>
107
<p class="frame-title">File <span class="frame-filename">{frame_filename}</span>,
108
line <i>{frame_lineno}</i>,
109
in <b>{frame_name}</b>
110
<span class="collapse-btn" data-frame-id="{frame_filename}-{frame_lineno}" onclick="collapse(this)">{collapse_button}</span>
112
<div id="{frame_filename}-{frame_lineno}" class="source-code {collapsed}">{code_context}</div>
117
<p><span class="frame-line">
118
<span class="lineno">{lineno}.</span> {line}</span></p>
122
<p class="center-line"><span class="frame-line center-line">
123
<span class="lineno">{lineno}.</span> {line}</span></p>
127
class ServerErrorMiddleware:
129
Handles returning 500 responses when a server error occurs.
131
If 'debug' is set, then traceback responses will be returned,
132
otherwise the designated 'handler' will be called.
134
This middleware class should generally be used to wrap *everything*
135
else up, so that unhandled exceptions anywhere in the stack
136
always result in an appropriate 500 response.
142
handler: ExceptionHandler | None = None,
146
self.handler = handler
149
async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
150
if scope["type"] != "http":
151
await self.app(scope, receive, send)
154
response_started = False
156
async def _send(message: Message) -> None:
157
nonlocal response_started, send
159
if message["type"] == "http.response.start":
160
response_started = True
161
89
await send(message)
164
1
288
await self.app(scope, receive, _send)
165
except Exception as exc:
166
request = Request(scope)
168
# In debug mode, return traceback responses.
169
response = await run_in_threadpool(self.debug_response, request, exc)
170
elif self.handler is None:
171
# Use our default 500 error handler.
172
response = self.error_response(request, exc)
174
# Use an installed 500 error handler.
175
if is_async_callable(self.handler):
176
response = await self.handler(request, exc) # type: ignore[assignment, arg-type]
178
response = await run_in_threadpool(self.handler, request, exc) # type: ignore[arg-type]
180
if not response_started:
181
await response(scope, receive, send)
183
# We always continue to raise the exception.
184
# This allows servers to log the error, or allows test clients
185
# to optionally raise the error within the test case.
188
def format_line(self, index: int, line: str, frame_lineno: int, frame_index: int) -> str:
190
# HTML escape - line could contain < or >
191
"line": html.escape(line).replace(" ", " "),
192
"lineno": (frame_lineno - frame_index) + index,
195
if index != frame_index:
196
return LINE.format(**values)
197
return CENTER_LINE.format(**values)
199
def generate_frame_html(self, frame: inspect.FrameInfo, is_collapsed: bool) -> str:
200
code_context = "".join(
205
frame.index, # type: ignore[arg-type]
207
for index, line in enumerate(frame.code_context or [])
211
# HTML escape - filename could contain < or >, especially if it's a virtual
212
# file e.g. <stdin> in the REPL
213
"frame_filename": html.escape(frame.filename),
214
"frame_lineno": frame.lineno,
215
# HTML escape - if you try very hard it's possible to name a function with <
217
"frame_name": html.escape(frame.function),
218
"code_context": code_context,
219
"collapsed": "collapsed" if is_collapsed else "",
220
"collapse_button": "+" if is_collapsed else "‒",
222
return FRAME_TEMPLATE.format(**values)
224
def generate_html(self, exc: Exception, limit: int = 7) -> str:
225
traceback_obj = traceback.TracebackException.from_exception(exc, capture_locals=True)
229
exc_traceback = exc.__traceback__
230
if exc_traceback is not None:
231
frames = inspect.getinnerframes(exc_traceback, limit)
232
for frame in reversed(frames):
233
exc_html += self.generate_frame_html(frame, is_collapsed)
236
if sys.version_info >= (3, 13): # pragma: no cover
237
exc_type_str = traceback_obj.exc_type_str
238
else: # pragma: no cover
239
exc_type_str = traceback_obj.exc_type.__name__
241
# escape error class and text
242
error = f"{html.escape(exc_type_str)}: {html.escape(str(traceback_obj))}"
244
return TEMPLATE.format(styles=STYLES, js=JS, error=error, exc_html=exc_html)
246
def generate_plain_text(self, exc: Exception) -> str:
247
return "".join(traceback.format_exception(type(exc), exc, exc.__traceback__))
249
def debug_response(self, request: Request, exc: Exception) -> Response:
250
accept = request.headers.get("accept", "")
252
if "text/html" in accept:
253
content = self.generate_html(exc)
254
return HTMLResponse(content, status_code=500)
255
content = self.generate_plain_text(exc)
256
return PlainTextResponse(content, status_code=500)
258
def error_response(self, request: Request, exc: Exception) -> Response:
259
return PlainTextResponse("Internal Server Error", status_code=500)