# Copyright 2025 Softwell S.r.l.
# Licensed under the Apache License, Version 2.0
"""Dispatcher - demultiplexes ASGI requests to the mounted apps.
The Dispatcher is the innermost layer of the server's middleware chain, but it
does no routing of its own. It picks the app from the first path segment and
relays the whole ASGI call to it; each app owns its middleware chain and routing
(``handle_request``).
Request flow:
scope → first-segment mount → apps[mount] (or apps[""] fallback)
→ await app(scope, receive, send)
Request creation, route resolution (``router.node``), handler invocation and
``response.set_result`` all happen inside the app, not here.
"""
from __future__ import annotations
from typing import TYPE_CHECKING
from ..exceptions import HTTPNotFound
from ..utils import decode_headers
if TYPE_CHECKING:
from .server import AsgiServer
from ..types import Receive, Scope, Send
[docs]
class Dispatcher:
"""Demultiplex requests to the app that owns the first path segment.
The server no longer routes: each app does its own routing through
``handle_request``. The dispatcher picks the app from the first path
segment (falling back to the empty-mount app for anything no app claims)
and delegates, wrapping the call in the app's own middleware chain when one
is configured.
Attributes:
server: Parent AsgiServer instance.
"""
__slots__ = ("server",)
[docs]
def __init__(self, server: AsgiServer) -> None:
"""Initialize dispatcher.
Args:
server: Parent AsgiServer instance.
"""
self.server = server
def _resolve_mount(self, path: str) -> str:
"""Return the first path segment (the mount name), or '' for root."""
stripped = path.strip("/")
if "/" in stripped:
return stripped.split("/", 1)[0]
return stripped
async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
"""Demultiplex on the first path segment and delegate to the app.
The chosen app is invoked as an ASGI callable: it owns its middleware
chain and routing (``handle_request``). The empty mount ('') is the app
for any path no other app claims. Raw headers are prepared here (ASGI
plumbing); auth, middleware and routing belong to the app.
A websocket scope follows the same mount rule but a different delegate:
an app exposing ``serve_websocket`` owns the connection; any other
mount falls back to the server's WsxHandler, which accepts every path
(no HTTPNotFound on the websocket branch).
"""
if "_headers" not in scope:
scope["_headers"] = decode_headers(scope)
path = scope.get("path", "/")
mount = self._resolve_mount(path)
app = self.server.apps.get(mount) or self.server.apps.get("")
if scope["type"] == "websocket":
hook = getattr(app, "serve_websocket", None)
if hook is not None:
await hook(scope, receive, send)
else:
await self.server.wsx_handler(scope, receive, send)
return
if app is None:
raise HTTPNotFound(path)
# The app is an ASGI callable: it enters its own middleware chain (if any)
# and ends at handle_request. The dispatcher only passes the baton.
await app(scope, receive, send)
if __name__ == "__main__":
pass