Middleware¶
Lilya includes several middleware classes unique to the application but also allowing some other ways of designing
them by using protocols
.
Lilya middleware¶
The Lilya middleware is the classic already available way of declaring the middleware within an Lilya application.
from lilya.apps import Lilya
from lilya.middleware import DefineMiddleware
from lilya.middleware.httpsredirect import HTTPSRedirectMiddleware
from lilya.middleware.trustedhost import TrustedHostMiddleware
app = Lilya(
routes=[...],
middleware=[
DefineMiddleware(
TrustedHostMiddleware,
allowed_hosts=["example.com", "*.example.com"],
),
DefineMiddleware(HTTPSRedirectMiddleware),
],
)
Lilya protocols¶
Lilya protocols are not too different from the Lilya middleware. In fact, the name itself happens only because of the use of the python protocols which forces a certain structure to happen.
When designing a middleware, you can inherit and subclass the MiddlewareProtocol provided by Lilya.
from contextlib import AsyncExitStack
from typing import Optional
from lilya.protocols.middleware import MiddlewareProtocol
from lilya.types import ASGIApp, Receive, Scope, Send
class AsyncExitStackMiddleware(MiddlewareProtocol):
def __init__(self, app: "ASGIApp"):
"""AsyncExitStack Middleware class.
Args:
app: The 'next' ASGI app to call.
config: The AsyncExitConfig instance.
"""
super().__init__(app)
self.app = app
async def __call__(self, scope: "Scope", receive: "Receive", send: "Send") -> None:
if not AsyncExitStack:
await self.app(scope, receive, send) # pragma: no cover
exception: Optional[Exception] = None
async with AsyncExitStack() as stack:
scope["lilya_astack"] = stack
try:
await self.app(scope, receive, send)
except Exception as e:
exception = e
raise e
if exception:
raise exception
MiddlewareProtocol¶
For those coming from a more enforced typed language like Java or C#, a protocol is the python equivalent to an interface.
The MiddlewareProtocol
is simply an interface to build middlewares for Lilya by enforcing the implementation of
the __init__
and the async def __call__
.
Enforcing this protocol also aligns with writing a Pure ASGI Middleware.
Quick sample¶
from typing import Any, Dict
from lilya.protocols.middleware import MiddlewareProtocol
from lilya.types import ASGIApp, Receive, Scope, Send
class SampleMiddleware(MiddlewareProtocol):
def __init__(self, app: "ASGIApp", **kwargs):
"""SampleMiddleware Middleware class.
The `app` is always enforced.
Args:
app: The 'next' ASGI app to call.
kwargs: Any arbitrarty data.
"""
super().__init__(app)
self.app = app
self.kwargs = kwargs
async def __call__(self, scope: "Scope", receive: "Receive", send: "Send") -> None:
"""
Implement the middleware logic here
"""
...
class AnotherSample(MiddlewareProtocol):
def __init__(self, app: "ASGIApp", **kwargs: Dict[str, Any]):
super().__init__(app, **kwargs)
self.app = app
async def __call__(self, scope: "Scope", receive: "Receive", send: "Send") -> None:
await self.app(scope, receive, send)
Middleware and the application¶
Creating this type of middlewares will make sure the protocols are followed and therefore reducing development errors by removing common mistakes.
To add middlewares to the application is very simple.
from typing import Any, Dict
from lilya.apps import Lilya
from lilya.middleware import DefineMiddleware
from lilya.protocols.middleware import MiddlewareProtocol
from lilya.types import ASGIApp, Receive, Scope, Send
class SampleMiddleware(MiddlewareProtocol):
def __init__(self, app: "ASGIApp", **kwargs):
"""SampleMiddleware Middleware class.
The `app` is always enforced.
Args:
app: The 'next' ASGI app to call.
kwargs: Any arbitrarty data.
"""
super().__init__(app)
self.app = app
self.kwargs = kwargs
async def __call__(self, scope: "Scope", receive: "Receive", send: "Send") -> None:
"""
Implement the middleware logic here
"""
...
class AnotherSample(MiddlewareProtocol):
def __init__(self, app: "ASGIApp", **kwargs: Dict[str, Any]):
super().__init__(app, **kwargs)
self.app = app
async def __call__(self, scope: "Scope", receive: "Receive", send: "Send") -> None:
await self.app(scope, receive, send)
app = Lilya(
routes=[...],
middleware=[
DefineMiddleware(SampleMiddleware),
DefineMiddleware(AnotherSample),
],
)
from typing import Any, Dict
from lilya.apps import Lilya
from lilya.middleware import DefineMiddleware
from lilya.protocols.middleware import MiddlewareProtocol
from lilya.routing import Include, Path
from lilya.types import ASGIApp, Receive, Scope, Send
class SampleMiddleware(MiddlewareProtocol):
def __init__(self, app: "ASGIApp", **kwargs):
"""SampleMiddleware Middleware class.
The `app` is always enforced.
Args:
app: The 'next' ASGI app to call.
kwargs: Any arbitrarty data.
"""
super().__init__(app)
self.app = app
self.kwargs = kwargs
async def __call__(self, scope: "Scope", receive: "Receive", send: "Send") -> None:
"""
Implement the middleware logic here
"""
...
class AnotherSample(MiddlewareProtocol):
def __init__(self, app: "ASGIApp", **kwargs: Dict[str, Any]):
super().__init__(app, **kwargs)
self.app = app
async def __call__(self, scope: "Scope", receive: "Receive", send: "Send") -> None: ...
class CustomMiddleware(MiddlewareProtocol):
def __init__(self, app: "ASGIApp", **kwargs: Dict[str, Any]):
super().__init__(app, **kwargs)
self.app = app
async def __call__(self, scope: "Scope", receive: "Receive", send: "Send") -> None: ...
async def home():
return "Hello world"
# Via Path
app = Lilya(
routes=[Path("/", handler=home, middleware=[DefineMiddleware(AnotherSample)])],
middleware=[DefineMiddleware(SampleMiddleware)],
)
# Via Include
app = Lilya(
routes=[
Include(
"/",
routes=[Path("/", handler=home, middleware=[DefineMiddleware(SampleMiddleware)])],
middleware=[DefineMiddleware(CustomMiddleware)],
)
],
middleware=[DefineMiddleware(AnotherSample)],
)
Quick note¶
Info
The middleware is not limited to Lilya
, ChildLilya
, Include
and Path
.
We simply choose Path
as it looks simpler to read and understand.
Pure ASGI Middleware¶
Lilya follows the ASGI spec. This capability allows for the implementation of ASGI middleware using the ASGI interface directly. This involves creating a chain of ASGI applications that call into the next one. Notably, this approach mirrors the implementation of middleware classes shipped with Lilya.
Example of the most common approach
from lilya.types import ASGIApp, Scope, Receive, Send
class MyMiddleware:
def __init__(self, app: ASGIApp):
self.app = app
async def __call__(self, scope: Scope, receive: Receive, send: Send):
await self.app(scope, receive, send)
When implementing a Pure ASGI middleware, it is like implementing an ASGI application, the first
parameter should always be an app and the __call__
should always return the app.
BaseAuthMiddleware¶
This is a very special middleware and helps with any authentication middleware that can be used within an Lilya application but like everything else, you can design your own.
BaseAuthMiddleware
is also a protocol that simply enforces the implementation of the authenticate
method and
assigning the result object into a AuthResult
and make it available on every request.
Example of a JWT middleware class¶
from myapp.models import User
from myapp.security.jwt.token import Token
from saffier.exceptions import ObjectNotFound
from lilya._internal._connection import Connection
from lilya.exceptions import NotAuthorized
from lilya.middleware.authentication import AuthResult, BaseAuthMiddleware
from lilya.types import ASGIApp
class JWTAuthMiddleware(BaseAuthMiddleware):
"""
An example how to integrate and design a JWT authentication
middleware assuming a `myapp` in Lilya.
"""
def __init__(
self,
app: ASGIApp,
signing_key: str,
algorithm: str,
api_key_header: str,
):
super().__init__(app)
self.app = app
self.signing_key = signing_key
self.algorithm = algorithm
self.api_key_header = api_key_header
async def retrieve_user(self, user_id) -> User:
try:
return await User.get(pk=user_id)
except ObjectNotFound:
raise NotAuthorized()
async def authenticate(self, request: Connection) -> AuthResult:
token = request.headers.get(self.api_key_header)
if not token:
raise NotAuthorized("JWT token not found.")
token = Token.decode(token=token, key=self.signing_key, algorithm=self.algorithm)
user = await self.retrieve_user(token.sub)
return AuthResult(user=user)
- Import the
BaseAuthMiddleware
andAuthResult
fromlilya.middleware.authentication
. - Implement the
authenticate
and assign theuser
result to theAuthResult
.
Import the middleware into a Lilya application¶
from lilya import Lilya
from lilya.middleware import DefineMiddleware
from .middleware.jwt import JWTAuthMiddleware
app = Lilya(routes=[...], middleware=[DefineMiddleware(JWTAuthMiddleware)])
from dataclasses import dataclass
from typing import List
from lilya.conf.global_settings import Settings
from lilya.middleware import DefineMiddleware
from .middleware.jwt import JWTAuthMiddleware
@dataclass
class AppSettings(Settings):
@property
def middleware(self) -> List[DefineMiddleware]:
return [
DefineMiddleware(JWTAuthMiddleware)
]
# load the settings via LILYA_SETTINGS_MODULE=src.configs.live.AppSettings
app = Lilya(routes=[...])
Tip
To know more about loading the settings and the available properties, have a look at the settings docs.
Middleware and the settings¶
One of the advantages of Lilya is leveraging the settings to make the codebase tidy, clean and easy to maintain. As mentioned in the settings document, the middleware is one of the properties available to use to start a Lilya application.
from __future__ import annotations
from dataclasses import dataclass
from lilya.conf.global_settings import Settings
from lilya.middleware import DefineMiddleware
from lilya.middleware.compression import GZipMiddleware
from lilya.middleware.httpsredirect import HTTPSRedirectMiddleware
@dataclass
class AppSettings(Settings):
@property
def middleware(self) -> list[DefineMiddleware]:
"""
All the middlewares to be added when the application starts.
"""
return [
DefineMiddleware(HTTPSRedirectMiddleware),
DefineMiddleware(GZipMiddleware, minimum_size=500, compresslevel=9),
]
Start the application with the new settings
LILYA_SETTINGS_MODULE=configs.live.AppSettings uvicorn src:app
INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit)
INFO: Started reloader process [28720]
INFO: Started server process [28722]
INFO: Waiting for application startup.
INFO: Application startup complete.
Attention
If LILYA_SETTINGS_MODULE
is not specified as the module to be loaded, Lilya will load the default settings
but your middleware will not be initialized.
Important¶
If you need to specify parameters in your middleware then you will need to wrap it in a
lilya.middleware.DefineMiddleware
object to do it so. See GZipMiddleware
example.
Available middlewares¶
CSRFMiddleware
- Handles with the CSRF.CORSMiddleware
- Handles with the CORS.TrustedHostMiddleware
- Handles with the CORS if a givenallowed_hosts
is populated.GZipMiddleware
- Compression middlewaregzip
.HTTPSRedirectMiddleware
- Middleware that handles HTTPS redirects for your application. Very useful to be used for production or production like environments.SessionMiddleware
- Middleware that handles the sessions.WSGIMiddleware
- Allows to connect WSGI applications and run them inside Lilya. A great example how to use it is available.XFrameOptionsMiddleware
- Middleware that handles specifically against clickjacking.SecurityMiddleware
- Provides several security enhancements to the request/response cycle and adds security headers to the response.
CSRFMiddleware¶
The default parameters used by the CSRFMiddleware implementation are restrictive by default and Lilya allows some ways of using this middleware depending of the taste.
from __future__ import annotations
from dataclasses import dataclass
from lilya.apps import Lilya
from lilya.conf.global_settings import Settings
from lilya.middleware import DefineMiddleware
from lilya.middleware.csrf import CSRFMiddleware
routes = [...]
# Option one
middleware = [DefineMiddleware(CSRFMiddleware, secret="your-long-unique-secret")]
app = Lilya(routes=routes, middleware=middleware)
# Option two - Using the settings module
# Running the application with your custom settings -> LILYA_SETTINGS_MODULE
@dataclass
class AppSettings(Settings):
@property
def middleware(self) -> list[DefineMiddleware]:
return [
DefineMiddleware(CSRFMiddleware, secret="your-long-unique-secret"),
]
CORSMiddleware¶
The default parameters used by the CORSMiddleware implementation are restrictive by default and Lilya allows some ways of using this middleware depending of the taste.
from __future__ import annotations
from dataclasses import dataclass
from lilya.apps import Lilya
from lilya.conf.global_settings import Settings
from lilya.middleware import DefineMiddleware
from lilya.middleware.cors import CORSMiddleware
routes = [...]
# Option one
middleware = [DefineMiddleware(CORSMiddleware, allow_origins=["*"])]
app = Lilya(routes=routes, middleware=middleware)
# Option two - Using the settings module
# Running the application with your custom settings -> LILYA_SETTINGS_MODULE
@dataclass
class AppSettings(Settings):
@property
def middleware(self) -> list[DefineMiddleware]:
return [
DefineMiddleware(CORSMiddleware, allow_origins=["*"]),
]
SessionMiddleware¶
Adds signed cookie-based HTTP sessions. Session information is readable but not modifiable.
from __future__ import annotations
from dataclasses import dataclass
from lilya.apps import Lilya
from lilya.conf.global_settings import Settings
from lilya.middleware import DefineMiddleware
from lilya.middleware.sessions import SessionMiddleware
routes = [...]
# Option one
middleware = [DefineMiddleware(SessionMiddleware, secret_key=...)]
app = Lilya(routes=routes, middleware=middleware)
# Option two - Using the settings module
# Running the application with your custom settings -> LILYA_SETTINGS_MODULE
@dataclass
class AppSettings(Settings):
@property
def middleware(self) -> list[DefineMiddleware]:
return [
DefineMiddleware(SessionMiddleware, secret_key=...),
]
HTTPSRedirectMiddleware¶
Enforces that all incoming requests must either be https or wss. Any http os ws will be redirected to the secure schemes instead.
from __future__ import annotations
from dataclasses import dataclass
from lilya.apps import Lilya
from lilya.conf.global_settings import Settings
from lilya.middleware import DefineMiddleware
from lilya.middleware.httpsredirect import HTTPSRedirectMiddleware
routes = [...]
# Option one
middleware = [DefineMiddleware(HTTPSRedirectMiddleware)]
app = Lilya(routes=routes, middleware=middleware)
# Option two - Using the settings module
# Running the application with your custom settings -> LILYA_SETTINGS_MODULE
@dataclass
class AppSettings(Settings):
@property
def middleware(self) -> list[DefineMiddleware]:
return [
DefineMiddleware(HTTPSRedirectMiddleware),
]
TrustedHostMiddleware¶
Enforces all requests to have a correct set Host
header in order to protect against host header attacks.
from __future__ import annotations
from dataclasses import dataclass
from lilya.apps import Lilya
from lilya.conf.global_settings import Settings
from lilya.middleware import DefineMiddleware
from lilya.middleware.trustedhost import TrustedHostMiddleware
routes = [...]
# Option one
middleware = [
DefineMiddleware(TrustedHostMiddleware, allowed_hosts=["www.example.com", "*.example.com"])
]
app = Lilya(routes=routes, middleware=middleware)
# Option two - Using the settings module
# Running the application with your custom settings -> LILYA_SETTINGS_MODULE
@dataclass
class AppSettings(Settings):
@property
def middleware(self) -> list[DefineMiddleware]:
return [
DefineMiddleware(
TrustedHostMiddleware, allowed_hosts=["www.example.com", "*.example.com"]
),
]
GZipMiddleware¶
It handles GZip responses for any request that includes "gzip" in the Accept-Encoding header.
from __future__ import annotations
from dataclasses import dataclass
from lilya.apps import Lilya
from lilya.conf.global_settings import Settings
from lilya.middleware import DefineMiddleware
from lilya.middleware.compression import GZipMiddleware
routes = [...]
middleware = [DefineMiddleware(GZipMiddleware, minimum_size=1000)]
app = Lilya(routes=routes, middleware=middleware)
# Option two - Using the settings module
# Running the application with your custom settings -> LILYA_SETTINGS_MODULE
@dataclass
class AppSettings(Settings):
@property
def middleware(self) -> list[DefineMiddleware]:
return [
DefineMiddleware(GZipMiddleware, minimum_size=1000),
]
WSGIMiddleware¶
A middleware class in charge of converting a WSGI application into an ASGI one. There are some more examples in the WSGI Frameworks section.
from flask import Flask, make_response
from lilya.apps import Lilya
from lilya.middleware.wsgi import WSGIMiddleware
from lilya.routing import Include
flask = Flask(__name__)
@flask.route("/home")
def home():
return make_response({"message": "Serving via flask"})
# Add the flask app into Lilya to be served by Lilya.
routes = [Include("/external", app=WSGIMiddleware(flask))]
app = Lilya(routes=routes)
The WSGIMiddleware
also allows to pass the app
as a string <dotted>.<path>
and this can make it
easier for code organisation.
Let us assume the previous example of the flask
app was inside myapp/asgi_or_wsgi/apps
. Like this:
from flask import Flask, make_response
flask = Flask(__name__)
@flask.route("/home")
def home():
return make_response({"message": "Serving via flask"})
To call it inside the middleware is as simple as:
from lilya.apps import Lilya
from lilya.middleware.wsgi import WSGIMiddleware
from lilya.routing import Include
# Add the flask app into Lilya to be served by Lilya.
routes = [
Include(
"/external",
app=WSGIMiddleware("myapp.asgi_or_wsgi.apps.flask"),
),
]
app = Lilya(routes=routes)
XFrameOptionsMiddleware¶
The clickjacking middleware that provides easy-to-use protection against clickjacking. This type of attack occurs when a malicious site tricks a user into clicking on a concealed element of another site which they have loaded in a hidden frame or iframe.
This middleware reads the value x_frame_options
from the settings and defaults to DENY
.
This also adds the X-Frame-Options
to the response headers.
from __future__ import annotations
from dataclasses import dataclass
from lilya.apps import Lilya
from lilya.conf.global_settings import Settings
from lilya.middleware import DefineMiddleware
from lilya.middleware.clickjacking import XFrameOptionsMiddleware
routes = [...]
# Option one
middleware = [DefineMiddleware(XFrameOptionsMiddleware)]
app = Lilya(routes=routes, middleware=middleware)
# Option two - Using the settings module
# Running the application with your custom settings -> LILYA_SETTINGS_MODULE
@dataclass
class AppSettings(Settings):
x_frame_options: str = "SAMEORIGIN"
def middleware(self) -> list[DefineMiddleware]:
return [
DefineMiddleware(XFrameOptionsMiddleware),
]
SecurityMiddleware¶
Provides several security enhancements to the request/response cycle and adds security headers to the response.
from __future__ import annotations
from dataclasses import dataclass
from lilya.apps import Lilya
from lilya.conf.global_settings import Settings
from lilya.middleware import DefineMiddleware
from lilya.middleware.security import SecurityMiddleware
routes = [...]
content_policy_dict = {
"default-src": "'self'",
"img-src": [
"*",
"data:",
],
"connect-src": "'self'",
"script-src": "'self'",
"style-src": ["'self'", "'unsafe-inline'"],
"script-src-elem": [
"https://unpkg.com/@stoplight/elements/web-components.min.jss",
],
"style-src-elem": [
"https://unpkg.com/@stoplight/elements/styles.min.css",
],
}
# Option one
middleware = [DefineMiddleware(SecurityMiddleware, content_policy=content_policy_dict)]
app = Lilya(routes=routes, middleware=middleware)
# Option two - Using the settings module
# Running the application with your custom settings -> LILYA_SETTINGS_MODULE
@dataclass
class AppSettings(Settings):
def middleware(self) -> list[DefineMiddleware]:
return [
DefineMiddleware(SecurityMiddleware, content_policy=content_policy_dict),
]
ClientIPMiddleware¶
Parses the client ip and add it in the request scope at two places: headers ("x-real-ip") and the request scope directly ("real-clientip").
from __future__ import annotations
from dataclasses import dataclass
from lilya.apps import Lilya
from lilya.conf.global_settings import Settings
from lilya.middleware import DefineMiddleware
from lilya.middleware.clientip import ClientIPMiddleware
routes = [...]
# Only trust unix (no client ip is set) (default)
trusted_proxies = ["unix"]
# No forwarded ip headers will be evaluated. Only the direct ip is accepted
trusted_proxies = []
# trust all client ips to provide forwarded headers
trusted_proxies = ["*"]
# Option one
middleware = [DefineMiddleware(ClientIPMiddleware, trusted_proxies=trusted_proxies)]
app = Lilya(routes=routes, middleware=middleware)
# Option two - Using the settings module
# Running the application with your custom settings -> LILYA_SETTINGS_MODULE
@dataclass
class AppSettings(Settings):
def middleware(self) -> list[DefineMiddleware]:
return [
DefineMiddleware(ClientIPMiddleware, trusted_proxies=trusted_proxies),
]
There are two special "ip"s: "*" and "unix"
The first one is a match all and implies all proxies are trustworthy, the second one applies when a unix socket is used or no client ip address was found.
Note
If you don't want to use the middleware you can use: get_ip
from lilya.clientip
directly.
Note
It is currently not possible to simulate a client ip address in lilyas TestClient. So you may want to use the Forwarded header and trust "unix" for tests.
Other middlewares¶
You can build your own middlewares as explained above but also reuse middlewares directly for any other ASGI application if you wish. If the middlewares follow the pure asgi then the middlewares are 100% compatible.
RateLimitMiddleware¶
A ASGI Middleware to rate limit and highly customizable.
CorrelationIdMiddleware¶
A middleware class for reading/generating request IDs and attaching them to application logs.
Tip
For Lilya apps, just substitute FastAPI with Lilya in the examples given or implement in the way Lilya shows in this document.
TimingMiddleware¶
ASGI middleware to record and emit timing metrics (to something like statsd).
Important points¶
- Lilya supports Lilya middleware (MiddlewareProtocol).
- A
MiddlewareProtocol
is simply an interface that enforces__init__
andasync __call__
to be implemented. app
is required parameter from any class inheriting from theMiddlewareProtocol
.- Pure ASGI Middleware is encouraged and the
MiddlewareProtocol
enforces that. - Middleware classes can be added to any layer of the application
- All authentication middlewares must inherit from the BaseAuthMiddleware.
- You can load the application middleware in different ways.