Repository: encode/starlette

Issue:
Background tasks exception handler
Currently, if a background task faces an exception, it isn't handled and there's no way to handle it (rather than wrapping the task)
My suggestion is to be able to add exception handler for background tasks, as there is for requests.


Discussion:
Yup, this is interesting.

So, the exception handler currently only runs for HTTP requests (not WebSocket) and only runs if a response hasn't already been returned. This is because any 500 page returned by the exception handler can't be displayed if we've already returned a response.

However, I think we've got the wrong policy there. Instead we ought to *always* run the exception handler, but only render its response if we haven't already started doing so.

The more tricky bit of that is how websockets should fit in there. I guess we probably want to change the signature from `handler(request, exc)` to `handler (conn, exc)`, and document that `conn` is an `HTTPConnection`, and might be *either one of* of the `Request` or `WebSocket` subclasses.
Sounds good, should I submit a pr? 
Welcome to take a look at it, yup!

Resolving patch:
diff --git a/starlette/middleware/errors.py b/starlette/middleware/errors.py
--- a/starlette/middleware/errors.py
+++ b/starlette/middleware/errors.py
@@ -158,21 +158,21 @@ async def _send(message: Message) -> None:
         try:
             await self.app(scope, 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)
+            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:
-                    # 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)
+                    response = await run_in_threadpool(self.handler, request, exc)
 
+            if not response_started:
                 await response(scope, receive, send)
 
             # We always continue to raise the exception.


Test patch:
diff --git a/tests/middleware/test_errors.py b/tests/middleware/test_errors.py
--- a/tests/middleware/test_errors.py
+++ b/tests/middleware/test_errors.py
@@ -1,7 +1,10 @@
 import pytest
 
+from starlette.applications import Starlette
+from starlette.background import BackgroundTask
 from starlette.middleware.errors import ServerErrorMiddleware
 from starlette.responses import JSONResponse, Response
+from starlette.routing import Route
 
 
 def test_handler(test_client_factory):
@@ -68,3 +71,28 @@ async def app(scope, receive, send):
         client = test_client_factory(app)
         with client.websocket_connect("/"):
             pass  # pragma: nocover
+
+
+def test_background_task(test_client_factory):
+    accessed_error_handler = False
+
+    def error_handler(request, exc):
+        nonlocal accessed_error_handler
+        accessed_error_handler = True
+
+    def raise_exception():
+        raise Exception("Something went wrong")
+
+    async def endpoint(request):
+        task = BackgroundTask(raise_exception)
+        return Response(status_code=204, background=task)
+
+    app = Starlette(
+        routes=[Route("/", endpoint=endpoint)],
+        exception_handlers={Exception: error_handler},
+    )
+
+    client = test_client_factory(app, raise_server_exceptions=False)
+    response = client.get("/")
+    assert response.status_code == 204
+    assert accessed_error_handler