Debugging¶
Veloce has two development conveniences for unhandled errors: a rich HTML
traceback page served when debug is on, and
a PROPAGATE_EXCEPTIONS switch that re-raises exceptions out of dispatch so a
test sees the real traceback instead of a 500. Both ship with Veloce — there
is nothing to install.
Enabling debug mode¶
Pass debug=True to the constructor, or set app.debug (it is bound to
app.config["DEBUG"]).
from veloce import Veloce
app = Veloce(debug=True)
@app.get("/boom")
async def boom():
raise ValueError("something went wrong")
if __name__ == "__main__":
app.run(port=8000)
Visit http://127.0.0.1:8000/boom in a browser and Veloce renders the
exception type, message, and every traceback frame with a window of source
around the failing line. Chained exceptions (raise ... from), __notes__
(PEP 678), and exception groups (PEP 654) are all shown.
Note
app.run() is the built-in development server. It is for local use only;
run under a hardened ASGI server (uvicorn module:app) in production. See
Deployment.
HTML for browsers, plain text for tools¶
The response format follows the request's Accept header. A browser sending
Accept: text/html gets the styled HTML page; curl, CLI tools, and
programmatic clients (no Accept, */*, or a text/plain preference) get the
plain-text traceback instead.
Never enable debug in production
The traceback page exposes source code, file paths, and local framework
internals. Keep debug=False for anything reachable beyond localhost.
Binding the dev server to a non-local host with debug=True logs a warning
for exactly this reason.
The debug page is read-only¶
Veloce's traceback page is a navigable HTML view, not an interactive in-browser console. It contains no form, no input, no JavaScript that posts back, and no endpoint that evaluates user-supplied code. Live frame evaluation is intentionally unimplemented because it turns a debug page into a remote-code-execution surface.
Note
Every value interpolated into the page — file paths, source lines, the exception message — is HTML-escaped, so exception content cannot inject markup.
Surfacing exceptions in tests¶
By default an unhandled exception becomes a 500 response and is logged with
its traceback (see Error handling),
which is right for production but still hides the error from an assertion. Set
PROPAGATE_EXCEPTIONS to re-raise it out of dispatch so the test sees the
original traceback directly.
from veloce import TestClient, Veloce
app = Veloce()
app.config["PROPAGATE_EXCEPTIONS"] = True
@app.get("/boom")
async def boom():
raise ValueError("something went wrong")
client = TestClient(app)
try:
client.get("/boom")
assert False, "expected the exception to propagate"
except ValueError as exc:
assert str(exc) == "something went wrong"
Without the flag, the same call returns a response instead of raising:
from veloce import TestClient, Veloce
app = Veloce()
@app.get("/boom")
async def boom():
raise ValueError("something went wrong")
client = TestClient(app)
resp = client.get("/boom")
assert resp.status_code == 500
Propagation resolution¶
PROPAGATE_EXCEPTIONS is a tri-state. When unset (None), it falls back to
the combination of DEBUG and TESTING: exceptions propagate only when both
are enabled. An explicit value always wins.
PROPAGATE_EXCEPTIONS |
DEBUG |
TESTING |
Exceptions propagate |
|---|---|---|---|
True |
(any) | (any) | Yes. |
False |
(any) | (any) | No — becomes a 500. |
None (default) |
True |
True |
Yes. |
None (default) |
otherwise | otherwise | No — becomes a 500. |
So enabling both DEBUG and TESTING is the implicit way to get propagation
without naming the key:
from veloce import Veloce
app = Veloce(debug=True)
app.config["TESTING"] = True # DEBUG + TESTING -> exceptions propagate
Note
Registered exception handlers run before propagation is considered. A
handler that returns a response for the raised type wins; propagation only
applies to exceptions that reach the unhandled 500 path. See
Error handling.
Frames from generated code¶
Veloce compiles a small resolver for routes whose parameters it can bind directly, so a traceback can pass through code that has no file on disk. Those frames read like any other — they name the handler they belong to and show their source:
File "<veloce-resolver:search:9f2c1a0b4e77>", line 7, in _resolver
_qp = request.query_params
^^^^^^^^^^^^^^^^^^^^
The name between the colons is the handler, so a frame tells you which route generated it. The trailing digest distinguishes resolvers, and is stable for a given handler — it changes only when the generated code does.
What you see there is the parameter binding for that route, with each type
resolved into the code rather than looked up per request: a str parameter is
read straight out of the query string, and an int or float appears as the
conversion itself, wrapped so a bad value raises the same 422 it always did.
A validation failure on a converted parameter therefore surfaces from generated
source rather than from a shared helper — the response is unchanged.
There is nothing to enable, and nothing to turn off in production: the source is registered once when the route is registered, and the request path never consults it.
Changed in version 0.18.0
Generated frames used to render as a bare File "<veloce-resolver>", line N
with no source line, and every resolver in the process shared that one name.
Next steps¶
- Convert raised exceptions into responses — see Error handling.
- Write tests against the in-memory client — see Testing.
- Run the app for local development — see Deployment.
- Full signatures are in the API reference.