Repository: sanic-org/sanic

Issue:
Middleware Runs Twice if Route Not Found + Middleware Exception
### Is there an existing issue for this?

- [X] I have searched the existing issues

### Describe the bug

Middleware will be run twice if the router raises an exception and then the middleware raises an Exception.

Output:

```
[2024-06-02 14:29:48 -0400] [304967] [INFO] Starting worker [304967]
Middleware 1 ran
Middleware 2 ran
Middleware 1 ran
Middleware 2 ran
[2024-06-02 14:29:54 -0400] [304967] [ERROR] Exception occurred while handling uri: 'http://localhost:8000/not-found'
Traceback (most recent call last):
  File "handle_request", line 39, in handle_request
    Iterable,
              
  File "/home/james/.local/share/pyenv/versions/3.11.9/envs/.venv/lib/python3.11/site-packages/sanic/router.py", line 75, in get
    return self._get(path, method, host)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/james/.local/share/pyenv/versions/3.11.9/envs/.venv/lib/python3.11/site-packages/sanic/router.py", line 40, in _get
    raise NotFound(f"Requested URL {e.path} not found") from None
sanic.exceptions.NotFound: Requested URL /foo not found

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "/home/james/.local/share/pyenv/versions/3.11.9/envs/.venv/lib/python3.11/site-packages/sanic/http/http1.py", line 119, in http1
    await self.protocol.request_handler(self.request)
  File "handle_request", line 159, in handle_request
    use for the application. Defaults to `Request`.
  File "handle_exception", line 73, in handle_exception
    from sanic.http import Stage
                       ^^^^^^^^^^
  File "_run_request_middleware", line 17, in _run_request_middleware
    from asyncio.futures import Future
                       ^^^^^^^^^^^^^^^^
  File "/home/james/.local/share/pyenv/versions/3.11.9/envs/.venv/lib/python3.11/site-packages/sanic/middleware.py", line 45, in __call__
    return self.func(*args, **kwargs)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/james/Projects/vidrovr/vrlib/example.py", line 14, in my_middleware2
    raise SanicException()
sanic.exceptions.SanicException: Internal Server Error

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "/home/james/.local/share/pyenv/versions/3.11.9/envs/.venv/lib/python3.11/site-packages/sanic/http/http1.py", line 426, in error_response
    await app.handle_exception(
  File "handle_exception", line 73, in handle_exception
    from sanic.http import Stage
                       ^^^^^^^^^^
  File "_run_request_middleware", line 17, in _run_request_middleware
    from asyncio.futures import Future
                       ^^^^^^^^^^^^^^^^
  File "/home/james/.local/share/pyenv/versions/3.11.9/envs/.venv/lib/python3.11/site-packages/sanic/middleware.py", line 45, in __call__
    return self.func(*args, **kwargs)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/james/Projects/vidrovr/vrlib/example.py", line 14, in my_middleware2
    raise SanicException()
sanic.exceptions.SanicException: Internal Server Error
```

### Code snippet

```python
from sanic import Sanic
from sanic.exceptions import SanicException
from sanic.response import html

app = Sanic(__name__)

@app.middleware("request")
def my_middleware1(request):
    print("Middleware 1 ran", flush=True)

@app.middleware("request")
def my_middleware2(request):
    print("Middleware 2 ran", flush=True)
    raise SanicException()

@app.get("/")
def index(request):
    return html("<p>hello world</p>")

if __name__ == "__main__":
    app.run(debug=True, auto_reload=False, access_log=False)
```

```shell
$ curl localhost:8000/not-found
```

or

```shell
$ curl -X POST localhost:8000
```

### Expected Behavior

Request Middleware only runs once

### How do you run Sanic?

Sanic CLI

### Operating System

Linux

### Sanic Version

23.12.1

### Additional context

_No response_

Discussion:

Patch:
diff --git a/sanic/app.py b/sanic/app.py
index 22435ef6..6b547eea 100644
--- a/sanic/app.py
+++ b/sanic/app.py
@@ -1209,10 +1209,15 @@ class Sanic(
         # Request Middleware
         # -------------------------------------------- #
         if run_middleware:
-            middleware = (
-                request.route and request.route.extra.request_middleware
-            ) or self.request_middleware
-            response = await self._run_request_middleware(request, middleware)
+            try:
+                middleware = (
+                    request.route and request.route.extra.request_middleware
+                ) or self.request_middleware
+                response = await self._run_request_middleware(
+                    request, middleware
+                )
+            except Exception as e:
+                return await self.handle_exception(request, e, False)
         # No middleware results
         if not response:
             try:


Test patch:
diff --git a/tests/test_exceptions.py b/tests/test_exceptions.py
index 7932eb6d..b945eade 100644
--- a/tests/test_exceptions.py
+++ b/tests/test_exceptions.py
@@ -1,5 +1,7 @@
 import logging
 
+from itertools import count
+
 import pytest
 
 from bs4 import BeautifulSoup
@@ -413,3 +415,21 @@ def test_exception_message_attribute():
     assert CustomError().message == CustomError.message == str(CustomError())
     assert SanicException().message != ""
     assert SanicException("").message == ""
+
+
+def test_request_middleware_exception_on_404(app: Sanic):
+    """See https://github.com/sanic-org/sanic/issues/2950"""
+    counter = count()
+
+    @app.on_request
+    def request_middleware(request):
+        value = next(counter)
+        raise Exception
+
+    @app.route("/")
+    async def handler(request): ...
+
+    _, response = app.test_client.get("/not-found")
+
+    assert response.status == 500
+    assert next(counter) == 1