Skip to content

Commit

Permalink
Rejig ExceptionMiddleware and ServerErrorMiddleware (#193)
Browse files Browse the repository at this point in the history
* Rejig ExceptionMiddleware and ServerErrorMiddleware

* Tweak DebugMiddleware implementation

* Support custom 500 handlers

* Exception handling updates
  • Loading branch information
tomchristie authored Nov 8, 2018
1 parent 0c34538 commit 9f3dcb7
Show file tree
Hide file tree
Showing 11 changed files with 297 additions and 280 deletions.
4 changes: 2 additions & 2 deletions docs/applications.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,6 @@ Submounting applications is a powerful way to include reusable ASGI applications
You can use either of the following to catch and handle particular types of
exceptions that occur within the application:

* `app.add_exception_handler(exc_class, handler)` - Add an error handler. The handler function may be either a coroutine or a regular function, with a signature like `func(request, exc) -> response`.
* `@app.exception_handler(exc_class)` - Add an error handler, decorator style.
* `app.add_exception_handler(exc_class_or_status_code, handler)` - Add an error handler. The handler function may be either a coroutine or a regular function, with a signature like `func(request, exc) -> response`.
* `@app.exception_handler(exc_class_or_status_code)` - Add an error handler, decorator style.
* `app.debug` - Enable or disable error tracebacks in the browser.
21 changes: 0 additions & 21 deletions docs/debug.md

This file was deleted.

111 changes: 47 additions & 64 deletions docs/exceptions.md
Original file line number Diff line number Diff line change
@@ -1,93 +1,76 @@

Starlette includes an exception handling middleware that you can use in order
to dispatch different classes of exceptions to different handlers.

To see how this works, we'll start by with this small ASGI application:
Starlette allows you to install custom exception handlers to deal with
how you return responses when errors or handled exceptions occur.

```python
from starlette.exceptions import ExceptionMiddleware, HTTPException


class App:
def __init__(self, scope):
raise HTTPException(status_code=403)


app = ExceptionMiddleware(App)
```

If you run the app and make an HTTP request to it, you'll get a plain text
response with a "403 Permission Denied" response. This is the behaviour that the
default handler responds with when an `HTTPException` class or subclass is raised.

Let's change the exception handling, so that we get JSON error responses
instead:
from starlette.applications import Starlette
from starlette.responses import HTMLResponse


```python
from starlette.exceptions import ExceptionMiddleware, HTTPException
from starlette.responses import JSONResponse

HTML_404_PAGE = ...
HTML_500_PAGE = ...

class App:
def __init__(self, scope):
raise HTTPException(status_code=403)

app = Starlette()

def handler(request, exc):
return JSONResponse({"detail": exc.detail}, status_code=exc.status_code)

@app.exception_handler(404)
async def not_found(request, exc):
return HTMLResponse(content=HTML_404_PAGE)

app = ExceptionMiddleware(App)
app.add_exception_handler(HTTPException, handler)
@app.exception_handler(500)
async def server_error(request, exc):
return HTMLResponse(content=HTML_500_PAGE)
```

Now if we make a request to the application, we'll get back a JSON encoded
HTTP response.

By default two types of exceptions are caught and dealt with:

* `HTTPException` - Used to raise standard HTTP error codes.
* `Exception` - Used as a catch-all handler to deal with any `500 Internal
Server Error` responses. The `Exception` case also wraps any other exception
handling.
If `debug` is enabled and an error occurs, then instead of using the installed
500 handler, Starlette will respond with a traceback response.

The catch-all `Exception` case is used to return simple `500 Internal Server Error`
responses. During development you might want to switch the behaviour so that
it displays an error traceback in the browser:

```
app = ExceptionMiddleware(App, debug=True)
```python
app = Starlette(debug=True)
```

This uses the same error tracebacks as the more minimal [`DebugMiddleware`](../debugging).
As well as registering handlers for specific status codes, you can also
register handlers for classes of exceptions.

The exception handler currently only catches and deals with exceptions within
HTTP requests. Any websocket exceptions will simply be raised to the server
and result in an error log.
In particular you might want to override how the built-in `HTTPException` class
is handled. For example, to use JSON style responses:

## ExceptionMiddleware
```python
@app.exception_handler(HTTPException)
async def http_exception(request, exc):
return JSONResponse({"detail": exc.detail}, status_code=exc.status_code)
```

The exception middleware catches and handles the exceptions, returning
appropriate HTTP responses.
## Errors and handled exceptions

* `ExceptionMiddleware(app, debug=False)` - Instantiate the exception handler,
wrapping up it around an inner ASGI application.
It is important to differentiate between handled exceptions and errors.

Adding handlers:
Handled exceptions do not represent error cases. They are coerced into appropriate
HTTP responses, which are then sent through the standard middleware stack. By default
the `HTTPException` class is used to manage any handled exceptions.

* `.add_exception_handler(exc_class, handler)` - Set a handler function to run
for the given exception class.
Errors are any other exception that occurs within the application. These cases
should bubble through the entire middleware stack as exceptions. Any error
logging middleware should ensure that it re-raises the exception all the
way up to the server.

Enabling debug mode:
In order to deal with this behaviour correctly, the middleware stack of a
`Starlette` application is configured like this:

* `.debug` - If set to `True`, then the catch-all handler for `Exception` will
not be used, and error tracebacks will be sent as responses instead.
* `ServerErrorMiddleware` - Returns 500 responses when server errors occur.
* Installed middleware
* `ExceptionMiddleware` - Deals with handled exceptions, and returns responses.
* Router
* Endpoints

## HTTPException

The `HTTPException` class provides a base class that you can use for any
standard HTTP error conditions. The `ExceptionMiddleware` implementation
defaults to returning plain-text HTTP responses for any `HTTPException`.
handled exceptions. The `ExceptionMiddleware` implementation defaults to
returning plain-text HTTP responses for any `HTTPException`.

* `HTTPException(status_code, detail=None)`

You should only raise `HTTPException` inside routing or endpoints. Middleware
classes should instead just return appropriate responses directly.
1 change: 0 additions & 1 deletion mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@ nav:
- Events: 'events.md'
- Background Tasks: 'background.md'
- Exceptions: 'exceptions.md'
- Debug: 'debug.md'
- Test Client: 'testclient.md'
- Release Notes: 'release-notes.md'

Expand Down
35 changes: 27 additions & 8 deletions starlette/applications.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,21 @@
from starlette.exceptions import ExceptionMiddleware
from starlette.lifespan import LifespanHandler
from starlette.middleware.base import BaseHTTPMiddleware
from starlette.middleware.errors import ServerErrorMiddleware
from starlette.routing import BaseRoute, Router
from starlette.schemas import BaseSchemaGenerator
from starlette.types import ASGIApp, ASGIInstance, Scope


class Starlette:
def __init__(self, debug: bool = False) -> None:
self._debug = debug
self.router = Router()
self.lifespan_handler = LifespanHandler()
self.app = self.router
self.exception_middleware = ExceptionMiddleware(self.router, debug=debug)
self.error_middleware = ServerErrorMiddleware(
self.exception_middleware, debug=debug
)
self.schema_generator = None # type: typing.Optional[BaseSchemaGenerator]

@property
Expand All @@ -23,11 +27,13 @@ def routes(self) -> typing.List[BaseRoute]:

@property
def debug(self) -> bool:
return self.exception_middleware.debug
return self._debug

@debug.setter
def debug(self, value: bool) -> None:
self._debug = value
self.exception_middleware.debug = value
self.error_middleware.debug = value

@property
def schema(self) -> dict:
Expand All @@ -41,10 +47,21 @@ def mount(self, path: str, app: ASGIApp, name: str = None) -> None:
self.router.mount(path, app=app, name=name)

def add_middleware(self, middleware_class: type, **kwargs: typing.Any) -> None:
self.exception_middleware.app = middleware_class(self.app, **kwargs)
self.error_middleware.app = middleware_class(
self.error_middleware.app, **kwargs
)

def add_exception_handler(self, exc_class: type, handler: typing.Callable) -> None:
self.exception_middleware.add_exception_handler(exc_class, handler)
def add_exception_handler(
self,
exc_class_or_status_code: typing.Union[int, typing.Type[Exception]],
handler: typing.Callable,
) -> None:
if exc_class_or_status_code in (500, Exception):
self.error_middleware.handler = handler
else:
self.exception_middleware.add_exception_handler(
exc_class_or_status_code, handler
)

def add_event_handler(self, event_type: str, func: typing.Callable) -> None:
self.lifespan_handler.add_event_handler(event_type, func)
Expand All @@ -61,9 +78,11 @@ def add_route(
def add_websocket_route(self, path: str, route: typing.Callable) -> None:
self.router.add_websocket_route(path, route)

def exception_handler(self, exc_class: type) -> typing.Callable:
def exception_handler(
self, exc_class_or_status_code: typing.Union[int, typing.Type[Exception]]
) -> typing.Callable:
def decorator(func: typing.Callable) -> typing.Callable:
self.add_exception_handler(exc_class, func)
self.add_exception_handler(exc_class_or_status_code, func)
return func

return decorator
Expand Down Expand Up @@ -107,4 +126,4 @@ def __call__(self, scope: Scope) -> ASGIInstance:
scope["app"] = self
if scope["type"] == "lifespan":
return self.lifespan_handler(scope)
return self.exception_middleware(scope)
return self.error_middleware(scope)
108 changes: 0 additions & 108 deletions starlette/debug.py

This file was deleted.

Loading

0 comments on commit 9f3dcb7

Please sign in to comment.