Skip to content

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

app.py
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.

curl http://127.0.0.1:8000/boom        # plain-text traceback

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.