Repository: encode/starlette

Issue:
Exceptions and middleware
Currently our middleware stack looks like this:

* Server
* ExceptionMiddleware # Generates 500 pages, and deals with handled exceptions.
* User Middleware
* Router
* Endpoint

Having implemented https://github.com/encode/sentry-asgi I've realised that this isn't quite right. We don't want handled exceptions to be captured by sentry or other exception logging middleware. Instead what we *ought* to have is this...

* Server
* ServerErrorMiddleware  # Generates 500 pages.
* User Middleware
* ExceptionMiddleware  # Deals with handled exceptions.
* Router
* Endpoint

Handled exceptions may be raised inside endpoints (or the router), but should not be available to regular middleware, which should just return responses directly. We want this behavior anyway because middleware classes that raise exceptions to be handled will not be portable across to other ASGI frameworks (without also including an exception handling middleware.)

Discussion:

Patch:
diff --git a/docs/applications.md b/docs/applications.md
index a9f3aed..aac8592 100644
--- a/docs/applications.md
+++ b/docs/applications.md
@@ -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.
diff --git a/docs/debug.md b/docs/debug.md
deleted file mode 100644
index 52e194c..0000000
--- a/docs/debug.md
+++ /dev/null
@@ -1,21 +0,0 @@
-
-You can use Starlette's `DebugMiddleware` to display simple error tracebacks in the browser.
-
-```python
-from starlette.debug import DebugMiddleware
-
-
-class App:
-    def __init__(self, scope):
-        self.scope = scope
-
-    async def __call__(self, receive, send):
-        raise RuntimeError('Something went wrong')
-
-
-app = DebugMiddleware(App)
-```
-
-For a more complete handling of exception cases you may wish to use Starlette's
-[`ExceptionMiddleware`](../exceptions/) class instead, which also includes
-optional debug handling.
diff --git a/docs/exceptions.md b/docs/exceptions.md
index 09de6e8..03908f8 100644
--- a/docs/exceptions.md
+++ b/docs/exceptions.md
@@ -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.
diff --git a/docs/staticfiles.md b/docs/staticfiles.md
index 448cc3b..cc8db5e 100644
--- a/docs/staticfiles.md
+++ b/docs/staticfiles.md
@@ -1,7 +1,12 @@
 
-Starlette also includes a `StaticFiles` class for serving a specific directory:
+Starlette also includes a `StaticFiles` class for serving files in a given directory:
 
-* `StaticFiles(directory)` - Serve any files in the given `directory`.
+### StaticFiles
+
+Signature: `StaticFiles(directory, check_dir=True)`
+
+* `directory` - A string denoting the directory path
+* `check_dir` - Ensure that the directory exists upon instantiation. Defaults to `True`
 
 You can combine this ASGI application with Starlette's routing to provide
 comprehensive static file serving.
diff --git a/mkdocs.yml b/mkdocs.yml
index 91bd5a0..c7cb8fe 100644
--- a/mkdocs.yml
+++ b/mkdocs.yml
@@ -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'
 
diff --git a/starlette/applications.py b/starlette/applications.py
index 464e9c5..db47ef6 100644
--- a/starlette/applications.py
+++ b/starlette/applications.py
@@ -4,6 +4,7 @@ from starlette.datastructures import URL, URLPath
 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
@@ -11,10 +12,13 @@ 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
@@ -23,11 +27,13 @@ class Starlette:
 
     @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:
@@ -41,10 +47,21 @@ class Starlette:
         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)
@@ -61,9 +78,11 @@ class Starlette:
     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
@@ -107,4 +126,4 @@ class Starlette:
         scope["app"] = self
         if scope["type"] == "lifespan":
             return self.lifespan_handler(scope)
-        return self.exception_middleware(scope)
+        return self.error_middleware(scope)
diff --git a/starlette/debug.py b/starlette/debug.py
deleted file mode 100644
index 32cac00..0000000
--- a/starlette/debug.py
+++ /dev/null
@@ -1,108 +0,0 @@
-import traceback
-
-from starlette.requests import Request
-from starlette.responses import HTMLResponse, PlainTextResponse, Response
-from starlette.types import ASGIApp, ASGIInstance, Message, Receive, Scope, Send
-
-STYLES = """\
-    .traceback-container {border: 1px solid #038BB8;}
-    .traceback-title {background-color: #038BB8;color: lemonchiffon;padding: 12px;font-size: 20px;margin-top: 0px;}
-    .traceback-content {padding: 5px 0px 20px 20px;}
-    .frame-line {font-weight: unset;padding: 10px 10px 10px 20px;background-color: #E4F4FD;
-    margin-left: 10px;margin-right: 10px;font: #394D54;color: #191f21;font-size: 17px;border: 1px solid #c7dce8;}
-"""
-
-TEMPLATE = """
-    <style type='text/css'>{style}</style>
-    <title>Starlette Debugger</title>
-    <h1>500 Server Error</h1>
-    <h2>{error}</h2>
-    <div class='traceback-container'>
-    <p class='traceback-title'>Traceback</p>
-    <div class='traceback-content'>{ext_html}</div>
-    </div>
-"""
-
-FRAME_TEMPLATE = """
-    <p>
-    File <span class='debug-filename'>`{frame_filename}`</span>,
-    line <i>{frame_lineno}</i>,
-    in <b>{frame_name}</b>
-    <p class='frame-line'>{frame_line}</p>
-    </p>
-"""
-
-
-class DebugGenerator:
-    def __init__(self, exc: Exception) -> None:
-        self.exc = exc
-        self.traceback_obj = traceback.TracebackException.from_exception(
-            exc, capture_locals=True
-        )
-        self.error = f"{self.traceback_obj.exc_type.__name__}: {self.traceback_obj}"
-
-    @staticmethod
-    def gen_frame_html(frame: traceback.FrameSummary) -> str:
-        values = {
-            "frame_filename": frame.filename,
-            "frame_lineno": frame.lineno,
-            "frame_name": frame.name,
-            "frame_line": frame.line,
-        }
-        return FRAME_TEMPLATE.format(**values)
-
-    def generate_html(self) -> str:
-        ext_html = "".join(
-            [self.gen_frame_html(frame) for frame in self.traceback_obj.stack]
-        )
-        values = {"style": STYLES, "error": self.error, "ext_html": ext_html}
-
-        return TEMPLATE.format(**values)
-
-    def generate_plain_text(self) -> str:
-        return "".join(traceback.format_tb(self.exc.__traceback__))
-
-
-def get_debug_response(request: Request, exc: Exception) -> Response:
-    accept = request.headers.get("accept", "")
-    debug_gen = DebugGenerator(exc)
-
-    if "text/html" in accept:
-        content = debug_gen.generate_html()
-        return HTMLResponse(content, status_code=500)
-    content = debug_gen.generate_plain_text()
-    return PlainTextResponse(content, status_code=500)
-
-
-class DebugMiddleware:
-    def __init__(self, app: ASGIApp) -> None:
-        self.app = app
-
-    def __call__(self, scope: Scope) -> ASGIInstance:
-        if scope["type"] != "http":
-            return self.app(scope)
-        return _DebugResponder(self.app, scope)
-
-
-class _DebugResponder:
-    def __init__(self, app: ASGIApp, scope: Scope) -> None:
-        self.app = app
-        self.scope = scope
-        self.response_started = False
-
-    async def __call__(self, receive: Receive, send: Send) -> None:
-        self.raw_send = send
-        try:
-            asgi = self.app(self.scope)
-            await asgi(receive, self.send)
-        except Exception as exc:
-            if not self.response_started:
-                request = Request(self.scope)
-                response = get_debug_response(request, exc)
-                await response(receive, send)
-            raise exc from None
-
-    async def send(self, message: Message) -> None:
-        if message["type"] == "http.response.start":
-            self.response_started = True
-        await self.raw_send(message)
diff --git a/starlette/exceptions.py b/starlette/exceptions.py
index a8d7142..64691e8 100644
--- a/starlette/exceptions.py
+++ b/starlette/exceptions.py
@@ -2,7 +2,7 @@ import asyncio
 import http
 import typing
 
-from starlette.debug import get_debug_response
+from starlette.concurrency import run_in_threadpool
 from starlette.requests import Request
 from starlette.responses import PlainTextResponse, Response
 from starlette.types import ASGIApp, ASGIInstance, Message, Receive, Scope, Send
@@ -19,24 +19,27 @@ class HTTPException(Exception):
 class ExceptionMiddleware:
     def __init__(self, app: ASGIApp, debug: bool = False) -> None:
         self.app = app
-        self.debug = debug
+        self.debug = debug  # TODO: We ought to handle 404 cases if debug is set.
+        self._status_handlers = {}  # type: typing.Dict[int, typing.Callable]
         self._exception_handlers = {
-            Exception: self.server_error,
-            HTTPException: self.http_exception,
-        }
+            HTTPException: self.http_exception
+        }  # type: typing.Dict[typing.Type[Exception], typing.Callable]
 
     def add_exception_handler(
-        self, exc_class: typing.Type[Exception], handler: typing.Callable
+        self,
+        exc_class_or_status_code: typing.Union[int, typing.Type[Exception]],
+        handler: typing.Callable,
     ) -> None:
-        assert issubclass(exc_class, BaseException)
-        self._exception_handlers[exc_class] = handler
+        if isinstance(exc_class_or_status_code, int):
+            self._status_handlers[exc_class_or_status_code] = handler
+        else:
+            assert issubclass(exc_class_or_status_code, Exception)
+            self._exception_handlers[exc_class_or_status_code] = handler
 
     def _lookup_exception_handler(
         self, exc: Exception
     ) -> typing.Optional[typing.Callable]:
         for cls in type(exc).__mro__:
-            if cls is Exception:
-                break
             if cls in self._exception_handlers:
                 return self._exception_handlers[cls]
         return None
@@ -56,48 +59,30 @@ class ExceptionMiddleware:
                 await send(message)
 
             try:
-                try:
-                    instance = self.app(scope)
-                    await instance(receive, sender)
-                except Exception as exc:
-                    # Exception handling is applied to any registed exception
-                    # class or subclass that occurs within the application.
-                    handler = self._lookup_exception_handler(exc)
+                instance = self.app(scope)
+                await instance(receive, sender)
+            except Exception as exc:
+                handler = None
 
-                    # Note that we always handle `Exception` in the outermost block.
-                    if handler is None:
-                        raise exc from None
+                if isinstance(exc, HTTPException):
+                    handler = self._status_handlers.get(exc.status_code)
 
-                    if response_started:
-                        msg = "Caught handled exception, but response already started."
-                        raise RuntimeError(msg) from exc
+                if handler is None:
+                    handler = self._lookup_exception_handler(exc)
 
-                    request = Request(scope, receive=receive)
-                    if asyncio.iscoroutinefunction(handler):
-                        response = await handler(request, exc)
-                    else:
-                        response = handler(request, exc)
-                    await response(receive, sender)
+                if handler is None:
+                    raise exc from None
+
+                if response_started:
+                    msg = "Caught handled exception, but response already started."
+                    raise RuntimeError(msg) from exc
 
-            except Exception as exc:
-                # The 'Exception' case always wraps everything else, and
-                # provides a last-ditch handler for dealing with server errors.
                 request = Request(scope, receive=receive)
-                if self.debug:
-                    handler = get_debug_response
-                else:
-                    handler = self._exception_handlers[Exception]
                 if asyncio.iscoroutinefunction(handler):
-                    response = await handler(request, exc)  # type: ignore
+                    response = await handler(request, exc)
                 else:
-                    response = handler(request, exc)  # type: ignore
-                if not response_started:
-                    await response(receive, send)
-
-                # We always raise the exception up to the server so that it
-                # is notified too. Typically this will mean that it'll log
-                # the exception.
-                raise
+                    response = await run_in_threadpool(handler, request, exc)
+                await response(receive, sender)
 
         return app
 
@@ -105,6 +90,3 @@ class ExceptionMiddleware:
         if exc.status_code in {204, 304}:
             return Response(b"", status_code=exc.status_code)
         return PlainTextResponse(exc.detail, status_code=exc.status_code)
-
-    def server_error(self, request: Request, exc: HTTPException) -> Response:
-        return PlainTextResponse("Internal Server Error", status_code=500)
diff --git a/starlette/middleware/errors.py b/starlette/middleware/errors.py
new file mode 100644
index 0000000..56cb20f
--- /dev/null
+++ b/starlette/middleware/errors.py
@@ -0,0 +1,141 @@
+import asyncio
+import functools
+import traceback
+import typing
+
+from starlette.concurrency import run_in_threadpool
+from starlette.requests import Request
+from starlette.responses import HTMLResponse, PlainTextResponse, Response
+from starlette.types import ASGIApp, ASGIInstance, Message, Receive, Scope, Send
+
+STYLES = """\
+    .traceback-container {border: 1px solid #038BB8;}
+    .traceback-title {background-color: #038BB8;color: lemonchiffon;padding: 12px;font-size: 20px;margin-top: 0px;}
+    .traceback-content {padding: 5px 0px 20px 20px;}
+    .frame-line {font-weight: unset;padding: 10px 10px 10px 20px;background-color: #E4F4FD;
+    margin-left: 10px;margin-right: 10px;font: #394D54;color: #191f21;font-size: 17px;border: 1px solid #c7dce8;}
+"""
+
+TEMPLATE = """
+    <style type='text/css'>{style}</style>
+    <title>Starlette Debugger</title>
+    <h1>500 Server Error</h1>
+    <h2>{error}</h2>
+    <div class='traceback-container'>
+    <p class='traceback-title'>Traceback</p>
+    <div class='traceback-content'>{ext_html}</div>
+    </div>
+"""
+
+FRAME_TEMPLATE = """
+    <p>
+    File <span class='debug-filename'>`{frame_filename}`</span>,
+    line <i>{frame_lineno}</i>,
+    in <b>{frame_name}</b>
+    <p class='frame-line'>{frame_line}</p>
+    </p>
+"""
+
+
+class DebugGenerator:
+    def __init__(self, exc: Exception) -> None:
+        self.exc = exc
+        self.traceback_obj = traceback.TracebackException.from_exception(
+            exc, capture_locals=True
+        )
+        self.error = f"{self.traceback_obj.exc_type.__name__}: {self.traceback_obj}"
+
+    @staticmethod
+    def gen_frame_html(frame: traceback.FrameSummary) -> str:
+        values = {
+            "frame_filename": frame.filename,
+            "frame_lineno": frame.lineno,
+            "frame_name": frame.name,
+            "frame_line": frame.line,
+        }
+        return FRAME_TEMPLATE.format(**values)
+
+    def generate_html(self) -> str:
+        ext_html = "".join(
+            [self.gen_frame_html(frame) for frame in self.traceback_obj.stack]
+        )
+        values = {"style": STYLES, "error": self.error, "ext_html": ext_html}
+
+        return TEMPLATE.format(**values)
+
+    def generate_plain_text(self) -> str:
+        return "".join(traceback.format_tb(self.exc.__traceback__))
+
+
+class ServerErrorMiddleware:
+    """
+    Handles returning 500 responses when a server error occurs.
+
+    If 'debug' is set, then traceback responses will be returned,
+    otherwise the designated 'handler' will be called.
+
+    This middleware class should generally be used to wrap *everything*
+    else up, so that unhandled exceptions anywhere in the stack
+    always result in an appropriate 500 response.
+    """
+
+    def __init__(
+        self, app: ASGIApp, handler: typing.Callable = None, debug: bool = False
+    ) -> None:
+        self.app = app
+        self.handler = handler
+        self.debug = debug
+
+    def __call__(self, scope: Scope) -> ASGIInstance:
+        if scope["type"] != "http":
+            return self.app(scope)
+        return functools.partial(self.asgi, scope=scope)
+
+    async def asgi(self, receive: Receive, send: Send, scope: Scope) -> None:
+        response_started = False
+
+        async def _send(message: Message) -> None:
+            nonlocal response_started, send
+
+            if message["type"] == "http.response.start":
+                response_started = True
+            await send(message)
+
+        try:
+            asgi = self.app(scope)
+            await asgi(receive, _send)
+        except Exception as exc:
+            if not response_started:
+                request = Request(scope)
+                if self.debug:
+                    # In debug mode, return traceback responses.
+                    response = self.debug_response(request, exc)
+                elif self.handler is None:
+                    # Use our default 500 error handler.
+                    response = self.error_response(request, exc)
+                else:
+                    # Use an installed 500 error handler.
+                    if asyncio.iscoroutinefunction(self.handler):
+                        response = await self.handler(request, exc)
+                    else:
+                        response = await run_in_threadpool(self.handler, request, exc)
+
+                await response(receive, send)
+
+            # We always continue to raise the exception.
+            # This allows servers to log the error, or allows test clients
+            # to optionally raise the error within the test case.
+            raise exc from None
+
+    def debug_response(self, request: Request, exc: Exception) -> Response:
+        accept = request.headers.get("accept", "")
+        debug_gen = DebugGenerator(exc)
+
+        if "text/html" in accept:
+            content = debug_gen.generate_html()
+            return HTMLResponse(content, status_code=500)
+        content = debug_gen.generate_plain_text()
+        return PlainTextResponse(content, status_code=500)
+
+    def error_response(self, request: Request, exc: Exception) -> Response:
+        return PlainTextResponse("Internal Server Error", status_code=500)
diff --git a/starlette/responses.py b/starlette/responses.py
index 5433194..3f5edd8 100644
--- a/starlette/responses.py
+++ b/starlette/responses.py
@@ -107,7 +107,7 @@ class Response:
             cookie[key]["secure"] = True  # type: ignore
         if httponly:
             cookie[key]["httponly"] = True  # type: ignore
-        cookie_val = cookie.output(header="")
+        cookie_val = cookie.output(header="").strip()
         self.raw_headers.append((b"set-cookie", cookie_val.encode("latin-1")))
 
     def delete_cookie(self, key: str, path: str = "/", domain: str = None) -> None:
diff --git a/starlette/staticfiles.py b/starlette/staticfiles.py
index 8a7527a..ec2418e 100644
--- a/starlette/staticfiles.py
+++ b/starlette/staticfiles.py
@@ -8,7 +8,9 @@ from starlette.types import ASGIInstance, Receive, Scope, Send
 
 
 class StaticFiles:
-    def __init__(self, *, directory: str) -> None:
+    def __init__(self, *, directory: str, check_dir: bool = True) -> None:
+        if check_dir and not os.path.isdir(directory):
+            raise RuntimeError("Directory '%s' does not exist" % directory)
         self.directory = directory
         self.config_checked = False
 


Test patch:
diff --git a/tests/test_debug.py b/tests/middleware/test_errors.py
similarity index 69%
rename from tests/test_debug.py
rename to tests/middleware/test_errors.py
index 48f7116..ccaaafb 100644
--- a/tests/test_debug.py
+++ b/tests/middleware/test_errors.py
@@ -1,10 +1,27 @@
 import pytest
 
-from starlette.debug import DebugMiddleware
-from starlette.responses import Response
+from starlette.middleware.errors import ServerErrorMiddleware
+from starlette.responses import JSONResponse, Response
 from starlette.testclient import TestClient
 
 
+def test_handler():
+    def app(scope):
+        async def asgi(receive, send):
+            raise RuntimeError("Something went wrong")
+
+        return asgi
+
+    def error_500(request, exc):
+        return JSONResponse({"detail": "Server Error"}, status_code=500)
+
+    app = ServerErrorMiddleware(app, handler=error_500)
+    client = TestClient(app, raise_server_exceptions=False)
+    response = client.get("/")
+    assert response.status_code == 500
+    assert response.json() == {"detail": "Server Error"}
+
+
 def test_debug_text():
     def app(scope):
         async def asgi(receive, send):
@@ -12,7 +29,7 @@ def test_debug_text():
 
         return asgi
 
-    app = DebugMiddleware(app)
+    app = ServerErrorMiddleware(app, debug=True)
     client = TestClient(app, raise_server_exceptions=False)
     response = client.get("/")
     assert response.status_code == 500
@@ -27,7 +44,7 @@ def test_debug_html():
 
         return asgi
 
-    app = DebugMiddleware(app)
+    app = ServerErrorMiddleware(app, debug=True)
     client = TestClient(app, raise_server_exceptions=False)
     response = client.get("/", headers={"Accept": "text/html, */*"})
     assert response.status_code == 500
@@ -44,7 +61,7 @@ def test_debug_after_response_sent():
 
         return asgi
 
-    app = DebugMiddleware(app)
+    app = ServerErrorMiddleware(app, debug=True)
     client = TestClient(app)
     with pytest.raises(RuntimeError):
         client.get("/")
@@ -54,7 +71,7 @@ def test_debug_error_during_scope():
     def app(scope):
         raise RuntimeError("Something went wrong")
 
-    app = DebugMiddleware(app)
+    app = ServerErrorMiddleware(app, debug=True)
     client = TestClient(app, raise_server_exceptions=False)
     response = client.get("/", headers={"Accept": "text/html, */*"})
     assert response.status_code == 500
@@ -70,7 +87,7 @@ def test_debug_not_http():
     def app(scope):
         raise RuntimeError("Something went wrong")
 
-    app = DebugMiddleware(app)
+    app = ServerErrorMiddleware(app)
 
     with pytest.raises(RuntimeError):
         app({"type": "websocket"})
diff --git a/tests/test_applications.py b/tests/test_applications.py
index 323914a..ef8496c 100644
--- a/tests/test_applications.py
+++ b/tests/test_applications.py
@@ -29,13 +29,18 @@ app = Starlette()
 app.add_middleware(TrustedHostMiddleware, hostname="testserver")
 
 
-@app.exception_handler(Exception)
+@app.exception_handler(500)
 async def error_500(request, exc):
     return JSONResponse({"detail": "Server Error"}, status_code=500)
 
 
+@app.exception_handler(405)
+async def method_not_allowed(request, exc):
+    return JSONResponse({"detail": "Custom message"}, status_code=405)
+
+
 @app.exception_handler(HTTPException)
-async def handler(request, exc):
+async def http_exception(request, exc):
     return JSONResponse({"detail": exc.detail}, status_code=exc.status_code)
 
 
@@ -136,11 +141,11 @@ def test_400():
 def test_405():
     response = client.post("/func")
     assert response.status_code == 405
-    assert response.json() == {"detail": "Method Not Allowed"}
+    assert response.json() == {"detail": "Custom message"}
 
     response = client.post("/class")
     assert response.status_code == 405
-    assert response.json() == {"detail": "Method Not Allowed"}
+    assert response.json() == {"detail": "Custom message"}
 
 
 def test_500():
diff --git a/tests/test_exceptions.py b/tests/test_exceptions.py
index 51de7ff..a18ef32 100644
--- a/tests/test_exceptions.py
+++ b/tests/test_exceptions.py
@@ -43,23 +43,23 @@ app = ExceptionMiddleware(router)
 client = TestClient(app)
 
 
-def test_server_error():
-    with pytest.raises(RuntimeError):
-        response = client.get("/runtime_error")
-
-    allow_500_client = TestClient(app, raise_server_exceptions=False)
-    response = allow_500_client.get("/runtime_error")
-    assert response.status_code == 500
-    assert response.text == "Internal Server Error"
-
-
-def test_debug_enabled():
-    app = ExceptionMiddleware(router)
-    app.debug = True
-    allow_500_client = TestClient(app, raise_server_exceptions=False)
-    response = allow_500_client.get("/runtime_error")
-    assert response.status_code == 500
-    assert "RuntimeError" in response.text
+# def test_server_error():
+#     with pytest.raises(RuntimeError):
+#         response = client.get("/runtime_error")
+#
+#     allow_500_client = TestClient(app, raise_server_exceptions=False)
+#     response = allow_500_client.get("/runtime_error")
+#     assert response.status_code == 500
+#     assert response.text == "Internal Server Error"
+
+
+# def test_debug_enabled():
+#     app = ExceptionMiddleware(router)
+#     app.debug = True
+#     allow_500_client = TestClient(app, raise_server_exceptions=False)
+#     response = allow_500_client.get("/runtime_error")
+#     assert response.status_code == 500
+#     assert "RuntimeError" in response.text
 
 
 def test_not_acceptable():
diff --git a/tests/test_staticfiles.py b/tests/test_staticfiles.py
index 9d12de8..84f3368 100644
--- a/tests/test_staticfiles.py
+++ b/tests/test_staticfiles.py
@@ -54,9 +54,16 @@ def test_staticfiles_with_missing_file_returns_404(tmpdir):
     assert response.text == "Not Found"
 
 
+def test_staticfiles_instantiated_with_missing_directory(tmpdir):
+    with pytest.raises(RuntimeError) as exc:
+        path = os.path.join(tmpdir, "no_such_directory")
+        app = StaticFiles(directory=path)
+    assert "does not exist" in str(exc)
+
+
 def test_staticfiles_configured_with_missing_directory(tmpdir):
     path = os.path.join(tmpdir, "no_such_directory")
-    app = StaticFiles(directory=path)
+    app = StaticFiles(directory=path, check_dir=False)
     client = TestClient(app)
     with pytest.raises(RuntimeError) as exc:
         client.get("/example.txt")
@@ -68,7 +75,7 @@ def test_staticfiles_configured_with_file_instead_of_directory(tmpdir):
     with open(path, "w") as file:
         file.write("<file content>")
 
-    app = StaticFiles(directory=path)
+    app = StaticFiles(directory=path, check_dir=False)
     client = TestClient(app)
     with pytest.raises(RuntimeError) as exc:
         client.get("/example.txt")