Repository: sanic-org/sanic

Issue:
request middleware not running when registered in a method since 22.9.0
### Is there an existing issue for this?

- [X] I have searched the existing issues

### Describe the bug

Prior to 22.9.0, you could register request middleware in functions and listeners. Since 22.9.0 this has stopped working. I'm unclear on whether this is by design or not, but I wasn't able to find anything in the docs that explains the discrepancy.

The last working version I tested was 22.6.2.

### Code snippet

```
from sanic import Sanic, response                                                                                                                                                                                                              
                                                                                                                                                                                                                                               
app = Sanic("my-hello-world-app")                                                                                                                                                                                                              
                                                                                                                                                                                                                                               
@app.middleware("request")                                                                                                                                                                                                                     
async def this_always_works(request):                                                                                                                                                                                                          
    print("this is request middleware")                                                                                                                                                                                                        
                                                                                                                                                                                                                                               
@app.before_server_start                                                                                                                                                                                                                       
async def before_server_start_listener(app, loop):                                                                                                                                                                                             
    def this_fails_in_22_9_0_and_after(request):                                                                                                                                                                                               
        print("this will not fire after in 22.9.0 and after")                                                                                                                                                                                  
    app.register_middleware(this_fails_in_22_9_0_and_after, "request")                                                                                                                                                                         
                                                                                                                                                                                                                                               
@app.route('/')                                                                                                                                                                                                                                
async def test(request):                                                                                                                                                                                                                       
    return response.json({'hello': 'world'})                                                                                                                                                                                                   
                                                                                                                                                                                                                                               
if __name__ == '__main__':                                                                                                                                                                                                                     
    app.register_middleware(lambda app: print("this will also not fire in 22.9.0 and after"), "request")                                                                                                                                       
    app.run(host="0.0.0.0", debug=True, auto_reload=True)                                                                                                                                                                                      
```

### Expected Behavior

I expected all middleware to fire, but only the first one I set up fires since 22.9.0. I know middlware priority was introduced in 22.9, but I wouldn't expect that to have broken existing apps. I tried explicitly setting the priority and I still was not able to get the middleware to fire.

### How do you run Sanic?

As a script (`app.run` or `Sanic.serve`)

### Operating System

Linux

### Sanic Version

22.9.0

### Additional context

Thanks for Sanic!

Discussion:
codecov[bot]: # [Codecov](https://codecov.io/gh/sanic-org/sanic/pull/2704?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=sanic-org) Report
Patch coverage: **`30.000`**% and project coverage change: **`-0.073`** :warning:
> Comparison is base [(`cb49c2b`)](https://codecov.io/gh/sanic-org/sanic/commit/cb49c2b26d146f154b18bb6ffd550c686b128035?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=sanic-org) 88.705% compared to head [(`a70ff3d`)](https://codecov.io/gh/sanic-org/sanic/pull/2704?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=sanic-org) 88.633%.

<details><summary>Additional details and impacted files</summary>


```diff
@@              Coverage Diff              @@
##              main     #2704       +/-   ##
=============================================
- Coverage   88.705%   88.633%   -0.073%     
=============================================
  Files           87        87               
  Lines         6853      6862        +9     
  Branches      1177      1178        +1     
=============================================
+ Hits          6079      6082        +3     
- Misses         533       539        +6     
  Partials       241       241               
```


| [Impacted Files](https://codecov.io/gh/sanic-org/sanic/pull/2704?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=sanic-org) | Coverage Δ | |
|---|---|---|
| [sanic/app.py](https://codecov.io/gh/sanic-org/sanic/pull/2704?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=sanic-org#diff-c2FuaWMvYXBwLnB5) | `88.986% <30.000%> (-1.068%)` | :arrow_down: |
| [sanic/http/http1.py](https://codecov.io/gh/sanic-org/sanic/pull/2704?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=sanic-org#diff-c2FuaWMvaHR0cC9odHRwMS5weQ==) | `84.246% <0.000%> (+0.684%)` | :arrow_up: |

Help us with your feedback. Take ten seconds to tell us [how you rate us](https://about.codecov.io/nps?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=sanic-org). Have a feature suggestion? [Share it here.](https://app.codecov.io/gh/feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=sanic-org)

</details>

[:umbrella: View full report at Codecov](https://codecov.io/gh/sanic-org/sanic/pull/2704?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=sanic-org).   
:loudspeaker: Do you have feedback about the report comment? [Let us know in this issue](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=sanic-org).


Patch:
diff --git a/sanic/app.py b/sanic/app.py
index 1adc2732..9fb7e027 100644
--- a/sanic/app.py
+++ b/sanic/app.py
@@ -16,7 +16,7 @@ from asyncio import (
 )
 from asyncio.futures import Future
 from collections import defaultdict, deque
-from contextlib import suppress
+from contextlib import contextmanager, suppress
 from functools import partial
 from inspect import isawaitable
 from os import environ
@@ -33,6 +33,7 @@ from typing import (
     Deque,
     Dict,
     Iterable,
+    Iterator,
     List,
     Optional,
     Set,
@@ -433,14 +434,15 @@ class Sanic(StaticHandleMixin, BaseSanic, StartupMixin, metaclass=TouchUpMeta):
 
         ctx = params.pop("route_context")
 
-        routes = self.router.add(**params)
-        if isinstance(routes, Route):
-            routes = [routes]
+        with self.amend():
+            routes = self.router.add(**params)
+            if isinstance(routes, Route):
+                routes = [routes]
 
-        for r in routes:
-            r.extra.websocket = websocket
-            r.extra.static = params.get("static", False)
-            r.ctx.__dict__.update(ctx)
+            for r in routes:
+                r.extra.websocket = websocket
+                r.extra.static = params.get("static", False)
+                r.ctx.__dict__.update(ctx)
 
         return routes
 
@@ -449,17 +451,19 @@ class Sanic(StaticHandleMixin, BaseSanic, StartupMixin, metaclass=TouchUpMeta):
         middleware: FutureMiddleware,
         route_names: Optional[List[str]] = None,
     ):
-        if route_names:
-            return self.register_named_middleware(
-                middleware.middleware, route_names, middleware.attach_to
-            )
-        else:
-            return self.register_middleware(
-                middleware.middleware, middleware.attach_to
-            )
+        with self.amend():
+            if route_names:
+                return self.register_named_middleware(
+                    middleware.middleware, route_names, middleware.attach_to
+                )
+            else:
+                return self.register_middleware(
+                    middleware.middleware, middleware.attach_to
+                )
 
     def _apply_signal(self, signal: FutureSignal) -> Signal:
-        return self.signal_router.add(*signal)
+        with self.amend():
+            return self.signal_router.add(*signal)
 
     def dispatch(
         self,
@@ -1520,6 +1524,27 @@ class Sanic(StaticHandleMixin, BaseSanic, StartupMixin, metaclass=TouchUpMeta):
     # Lifecycle
     # -------------------------------------------------------------------- #
 
+    @contextmanager
+    def amend(self) -> Iterator[None]:
+        """
+        If the application has started, this function allows changes
+        to be made to add routes, middleware, and signals.
+        """
+        if not self.state.is_started:
+            yield
+        else:
+            do_router = self.router.finalized
+            do_signal_router = self.signal_router.finalized
+            if do_router:
+                self.router.reset()
+            if do_signal_router:
+                self.signal_router.reset()
+            yield
+            if do_signal_router:
+                self.signalize(self.config.TOUCHUP)
+            if do_router:
+                self.finalize()
+
     def finalize(self):
         try:
             self.router.finalize()


Test patch:
diff --git a/tests/test_late_adds.py b/tests/test_late_adds.py
new file mode 100644
index 00000000..f7281d38
--- /dev/null
+++ b/tests/test_late_adds.py
@@ -0,0 +1,54 @@
+import pytest
+
+from sanic import Sanic, text
+
+
+@pytest.fixture
+def late_app(app: Sanic):
+    app.config.TOUCHUP = False
+    app.get("/")(lambda _: text(""))
+    return app
+
+
+def test_late_route(late_app: Sanic):
+    @late_app.before_server_start
+    async def late(app: Sanic):
+        @app.get("/late")
+        def handler(_):
+            return text("late")
+
+    _, response = late_app.test_client.get("/late")
+    assert response.status_code == 200
+    assert response.text == "late"
+
+
+def test_late_middleware(late_app: Sanic):
+    @late_app.get("/late")
+    def handler(request):
+        return text(request.ctx.late)
+
+    @late_app.before_server_start
+    async def late(app: Sanic):
+        @app.on_request
+        def handler(request):
+            request.ctx.late = "late"
+
+    _, response = late_app.test_client.get("/late")
+    assert response.status_code == 200
+    assert response.text == "late"
+
+
+def test_late_signal(late_app: Sanic):
+    @late_app.get("/late")
+    def handler(request):
+        return text(request.ctx.late)
+
+    @late_app.before_server_start
+    async def late(app: Sanic):
+        @app.signal("http.lifecycle.request")
+        def handler(request):
+            request.ctx.late = "late"
+
+    _, response = late_app.test_client.get("/late")
+    assert response.status_code == 200
+    assert response.text == "late"