Errors#

Responses as exceptions#

ResponseRedirect and ResponseError are responses, and raising one is the normal way to stop a handler:

from muffin import ResponseError, ResponseRedirect

@app.route("/private")
async def private(request):
    if not request.cookies.get("auth"):
        raise ResponseError.UNAUTHORIZED()
    raise ResponseRedirect("/login")

Every HTTP status has a shortcut on ResponseError: BAD_REQUEST(), NOT_FOUND(), METHOD_NOT_ALLOWED(), INTERNAL_SERVER_ERROR(), BAD_GATEWAY() and so on. They accept an optional message and headers:

raise ResponseError.NOT_FOUND("No such page", headers={"Cache-Control": "no-store"})

The router raises these too: an unknown path is ResponseError.NOT_FOUND and a matching path with the wrong method is ResponseError.METHOD_NOT_ALLOWED, so custom handlers for them cover routing as well.

Exception handlers#

app.on_error(SomeException) registers a handler. It receives the request and the exception, and whatever it returns is converted to a response like any other return value:

@app.on_error(ResponseError)
async def on_response_error(request, error):
    if error.status_code == 404:
        return ResponseError.NOT_FOUND("Custom 404 Page")
    return error
@app.on_error(ValueError)
async def on_value_error(request, error):
    app.logger.warning("Bad input: %s", error)
    return 400, {"error": "bad input"}

Handlers are looked up along the exception’s MRO, so the first matching class wins: register ValidationError for the specific case and Exception for everything else. Order of registration does not matter, the exception type does.

Unhandled exceptions#

Without a matching handler, Muffin logs the traceback and returns a plain 500 response; the connection stays usable:

@app.route("/boom")
async def boom(request):
    raise RuntimeError("unexpected")     # logged, 500, no traceback to the client

With DEBUG=True the exception is not caught at all: it propagates to the ASGI server, which prints the traceback and closes the response. That is what you want in development and never in production.

Muffin’s own exceptions#

class muffin.errors.MuffinError#

Base class for Muffin Errors.

class muffin.errors.AsyncRequiredError(func)#

Raised when an async function is required.

Parameters:

func (Callable)

__init__(func)#

Initialize the exception.

Parameters:

func (Callable)

class muffin.errors.InvalidAppError(name)#

Raised when an application is not found.

Parameters:

name (str)

__init__(name)#

Initialize the exception.

Parameters:

name (str)

Plugin errors (PluginError, PluginNotInstalledError) are documented in Plugins.

AsyncRequiredError and InvalidAppError appear at application definition time, not at request time: the first when a handler method is not a coroutine, or when a command registered with lifespan=True is not async; the second when import_app cannot resolve the given path.

PluginNotInstalledError is raised by the plugin.app property when a plugin was created but never bound to an application, including plugins that were disabled by configuration.

Errors from ASGI middleware#

Internal middleware runs inside request processing, so exceptions raised there are handled by the same exception handlers. ASGI middleware bound to the lifespan sits outside it; it sees the raw exceptions that escape, and a failing one can break the connection rather than return a response.