Skip to content

Commit ad18478

Browse files
Update middlewares.md (#30)
1 parent bff2f8b commit ad18478

File tree

1 file changed

+217
-6
lines changed

1 file changed

+217
-6
lines changed

blacksheep/docs/middlewares.md

Lines changed: 217 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,11 @@ request.
55

66
This page covers:
77

8-
- [X] Introduction to middlewares.
8+
- [X] Introduction to BlackSheep middlewares.
99
- [X] How to use function decorators to avoid code repetition.
1010
- [X] Middleware management with MiddlewareList and MiddlewareCategory.
1111
- [X] Organizing middlewares by categories and priorities.
12+
- [X] How to integrate ASGI middlewares.
1213

1314
## Introduction to middlewares
1415

@@ -106,7 +107,7 @@ When middlewares are defined for an application, resolution chains are built at
106107
its start. Every handler configured in the application router is replaced by a
107108
chain, executing middlewares in order, down to the registered handler.
108109

109-
## Middleware management with MiddlewareList and MiddlewareCategory
110+
## Middleware management
110111

111112
/// admonition | New in BlackSheep 2.4.4
112113
type: info
@@ -350,7 +351,217 @@ def headers(additional_headers: Tuple[Tuple[str, str], ...]):
350351
return decorator
351352
```
352353

353-
!!! warning
354-
The `ensure_response` function is necessary to support scenarios
355-
when the request handlers defined by the user doesn't return an instance of
356-
Response class (see _[request handlers normalization](request-handlers.md)_).
354+
/// admonition | Additional dependencies.
355+
type: warning
356+
357+
The `ensure_response` function is necessary to support scenarios
358+
when the request handlers defined by the user doesn't return an instance of
359+
Response class (see _[request handlers normalization](request-handlers.md)_).
360+
361+
///
362+
363+
## How to integrate ASGI middlewares
364+
365+
BlackSheep middlewares cannot be mixed with ASGI middlewares because they use different
366+
code APIs. However, the `Application` class itself in BlackSheep supports the signature
367+
of ASGI middlewares, and can be mixed with them at the application level instead of the
368+
middleware chain level.
369+
370+
Consider the following example, where the `Starlette` `TrustedHostMiddleware` is used
371+
with a BlackSheep application, following the pattern described in the Starlette
372+
documentation at [_Using Middleware In Other Frameworks_](https://starlette.dev/middleware/#using-middleware-in-other-frameworks).
373+
374+
```python {hl_lines='12'}
375+
from blacksheep import Application, get
376+
from starlette.middleware.trustedhost import TrustedHostMiddleware
377+
378+
379+
app = Application()
380+
381+
382+
@get("/")
383+
async def home():
384+
return "Hello!"
385+
386+
app = TrustedHostMiddleware(app, allowed_hosts=["localhost"])
387+
```
388+
389+
Below is an example where `FastAPI-Events` is used with a BlackSheep application:
390+
391+
```python {hl_lines='27-30'}
392+
from blacksheep import Application, get
393+
from fastapi_events.dispatcher import dispatch
394+
from fastapi_events.middleware import EventHandlerASGIMiddleware
395+
from fastapi_events.handlers.local import LocalHandler
396+
from fastapi_events.typing import Event
397+
398+
399+
app = Application()
400+
401+
402+
async def handle_all_events(event: Event):
403+
"""Handler for all events"""
404+
print(f"Event received: {event}")
405+
406+
407+
# Create a local handler for events
408+
local_handler = LocalHandler()
409+
local_handler.register(handle_all_events)
410+
411+
412+
@get("/")
413+
async def home():
414+
dispatch("my-fancy-event", payload={"id": 1}) # Emit events anywhere in your code
415+
return "Hello!"
416+
417+
418+
app = EventHandlerASGIMiddleware(
419+
app,
420+
handlers=[local_handler]
421+
)
422+
```
423+
424+
### Creating a custom application class for ASGI middleware management
425+
426+
While the direct wrapping approach shown above works well for simple cases, you may
427+
want to create a custom application class if you need to manage multiple ASGI middlewares
428+
or prefer a more explicit API that's consistent with BlackSheep's middleware system.
429+
430+
The following example shows how to define such a custom class that supports adding ASGI
431+
middlewares through a dedicated method:
432+
433+
```python
434+
# yourapp.py
435+
from typing import Callable
436+
437+
from blacksheep import Application, Router
438+
from blacksheep.server.routing import MountRegistry
439+
from rodi import ContainerProtocol
440+
441+
442+
class CustomApplication(Application):
443+
"""
444+
Application subclass that supports ASGI middleware at the application level.
445+
446+
ASGI middleware are applied before BlackSheep processes the request, providing
447+
a clean separation between ASGI-level and BlackSheep-level middleware.
448+
449+
Usage:
450+
app = CustomApplication()
451+
452+
# Add ASGI middleware (order matters - first added wraps outermost)
453+
app.add_asgi_middleware(some_asgi_middleware)
454+
app.add_asgi_middleware(another_asgi_middleware)
455+
"""
456+
457+
def __init__(
458+
self,
459+
*,
460+
router: Router | None = None,
461+
services: ContainerProtocol | None = None,
462+
show_error_details: bool = False,
463+
mount: MountRegistry | None = None,
464+
):
465+
super().__init__(
466+
router=router,
467+
services=services,
468+
show_error_details=show_error_details,
469+
mount=mount,
470+
)
471+
self._asgi_chain = super().__call__
472+
self._asgi_middlewares: list[Callable] = []
473+
474+
def add_asgi_middleware(self, middleware: Callable) -> None:
475+
"""
476+
Adds an ASGI middleware to the application.
477+
478+
The middleware should be a callable with signature:
479+
async def middleware(app, scope, receive, send) -> None
480+
481+
Or a factory that returns such a callable:
482+
def middleware_factory(app) -> Callable
483+
484+
Middleware are applied in the order they are added, with the first
485+
added being the outermost layer.
486+
487+
Args:
488+
middleware: An ASGI middleware callable or factory
489+
"""
490+
self._asgi_middlewares.append(middleware)
491+
492+
async def start(self):
493+
self._asgi_chain = self._build_asgi_chain()
494+
return await super().start()
495+
496+
async def __call__(self, scope, receive, send):
497+
return await self._asgi_chain(scope, receive, send)
498+
499+
def _build_asgi_chain(self) -> Callable:
500+
"""
501+
Builds the ASGI middleware chain, with the base Application.__call__
502+
as the innermost application.
503+
"""
504+
# Start with the base application handler
505+
app = super().__call__
506+
507+
# Wrap with each middleware in reverse order (last added wraps innermost)
508+
for middleware in reversed(self._asgi_middlewares):
509+
# Check if it's a factory (single parameter) or direct middleware
510+
import inspect
511+
sig = inspect.signature(middleware)
512+
params = list(sig.parameters.keys())
513+
514+
# Factory pattern: middleware(app) -> callable (single parameter)
515+
if len(params) == 1:
516+
app = middleware(app)
517+
# Direct ASGI callable: needs to be wrapped
518+
elif len(params) == 3 and params == ['scope', 'receive', 'send']:
519+
# Wrap to provide app parameter
520+
wrapped_app = app
521+
async def asgi_wrapper(scope, receive, send, mw=middleware, inner=wrapped_app):
522+
await mw(inner, scope, receive, send)
523+
app = asgi_wrapper # type: ignore
524+
else:
525+
raise TypeError(
526+
f"ASGI middleware must have signature (app, scope, receive, send) "
527+
f"or be a factory with signature (app). Got: {sig}"
528+
)
529+
530+
return app
531+
```
532+
533+
The following example demonstrates how to use the custom `CustomApplication` class.
534+
Notice the use of a lambda function to wrap the middleware initialization—this factory
535+
pattern ensures the middleware receives the application instance correctly:
536+
537+
```python
538+
from blacksheep import get
539+
from fastapi_events.dispatcher import dispatch
540+
from fastapi_events.middleware import EventHandlerASGIMiddleware
541+
from fastapi_events.handlers.local import LocalHandler
542+
from fastapi_events.typing import Event
543+
from yourapp import CustomApplication
544+
545+
546+
app = CustomApplication()
547+
548+
549+
async def handle_all_events(event: Event):
550+
"""Handler for all events"""
551+
print(f"Event received: {event}")
552+
553+
554+
# Create a local handler for events
555+
local_handler = LocalHandler()
556+
local_handler.register(handle_all_events)
557+
558+
559+
@get("/")
560+
async def home():
561+
dispatch("my-fancy-event", payload={"id": 1}) # Emit events anywhere in your code
562+
return "Hello!"
563+
564+
565+
# Note how the factory pattern is used below:
566+
app.add_asgi_middleware(lambda app: EventHandlerASGIMiddleware(app, handlers=[local_handler]))
567+
```

0 commit comments

Comments
 (0)