Add middleware ability to mount subapps via `_subapp`

Commit 57973dc · patx · 2025-07-19T14:05:28-04:00

Changeset
57973dc4fbc6ab597d7e043f6e9f8aa2e5eeeeed
Parents
10af9d6a9a9cc91dc3db62994b0f7276be29e3e7

View source at this commit

Comments

No comments yet.

Log in to comment

Diff

diff --git a/docs/release_notes.md b/docs/release_notes.md
index a43eeb7..fa9c5d4 100644
--- a/docs/release_notes.md
+++ b/docs/release_notes.md
@@ -1,6 +1,7 @@
 [![Logo](https://patx.github.io/micropie/logo.png)](https://patx.github.io/micropie)
 
 ## Releases Notes
+- **[0.19](https://github.com/patx/micropie/releases/tag/v0.19)** - Easier debugging with `traceback`. Add `_sub_app` attribute to allow middleware to mount other ASGI applications
 - **[0.18](https://github.com/patx/micropie/releases/tag/v0.18)** - Ensure handlers that return async generators are killed upon client disconnect to prevent memory leaks
 - **[0.17](https://github.com/patx/micropie/releases/tag/v0.17)** - Change API of lifespan events to match API of middlewaress, eg. `app.startup_handlers.append(handler)`
 - **[0.16](https://github.com/patx/micropie/releases/tag/v0.16)** - Add support for lifespan events using `on_startup` and `on_shutdown`
diff --git a/examples/middleware/subapp.py b/examples/middleware/subapp.py
new file mode 100644
index 0000000..84af163
--- /dev/null
+++ b/examples/middleware/subapp.py
@@ -0,0 +1,44 @@
+from micropie import App, HttpMiddleware, Request
+
+# Define the Sub-App
+class ApiApp(App):
+    async def index(self):
+        return {"message": "Welcome to the API!"}
+
+    async def users(self, user_id: str):
+        return {"user_id": user_id, "message": f"User {user_id} from API"}
+
+# Define a Middleware to Mount the Sub-App
+class SubAppMiddleware(HttpMiddleware):
+    def __init__(self, mount_path: str, subapp: App):
+        self.mount_path = mount_path.lstrip("/")
+        self.subapp = subapp
+
+    async def before_request(self, request):
+        path = request.scope["path"].lstrip("/")
+        if path.startswith(self.mount_path):
+            # Set the subapp and the remaining path
+            request._subapp = self.subapp
+            request._subapp_path = path[len(self.mount_path):].lstrip("/") or "/"
+            return None  # Continue processing
+        return None  # Not a subapp path, continue with main app
+
+    async def after_request(
+        self, request, status_code, response_body, extra_headers):
+        return None  # No changes to response
+
+# Define the Main App
+class MainApp(App):
+    async def index(self):
+        return {"message": "Welcome to the Main App!"}
+
+    async def hello(self, name: str):
+        return {"message": f"Hello, {name} from Main App!"}
+
+# Create and Configure the Apps
+app = MainApp()
+api_app = ApiApp()
+
+# Mount the sub-app at /api
+subapp_middleware = SubAppMiddleware(mount_path="/api", subapp=api_app)
+app.middlewares.append(subapp_middleware)
diff --git a/micropie.py b/micropie.py
index 4024822..0f8ffc2 100644
--- a/micropie.py
+++ b/micropie.py
@@ -481,7 +481,12 @@ class App:
                     )
                     await self._send_response(send, status_code, response_body, extra_headers)
                     return
-
+            if hasattr(request, "_subapp"):
+                new_scope = dict(scope)
+                new_scope["path"] = request._subapp_path
+                new_scope["root_path"] = scope.get("root_path", "") + "/" + self.middlewares[0].mount_path
+                await request._subapp(new_scope, receive, send)
+                return
             # Parse path and find handler
             path: str = scope["path"].lstrip("/")
             parts: List[str] = path.split("/") if path else []