0
# Middleware and Signals
1
2
Sanic provides a flexible middleware system for request/response processing and an event-driven signal system for application lifecycle management. These systems enable cross-cutting concerns, custom processing pipelines, and reactive programming patterns.
3
4
## Capabilities
5
6
### Middleware System
7
8
Middleware functions that process requests and responses in a configurable pipeline.
9
10
```python { .api }
11
def middleware(middleware_or_request: str):
12
"""
13
Decorator for registering middleware functions.
14
15
Parameters:
16
- middleware_or_request: Middleware type ('request' or 'response')
17
18
Usage:
19
@app.middleware('request')
20
async def add_session(request):
21
# Process request before route handler
22
request.ctx.session = await get_session(request)
23
24
@app.middleware('response')
25
async def add_cors_headers(request, response):
26
# Process response after route handler
27
response.headers['Access-Control-Allow-Origin'] = '*'
28
"""
29
30
# Middleware registration methods
31
def request_middleware(self, middleware_fn):
32
"""
33
Register request middleware function.
34
35
Parameters:
36
- middleware_fn: Middleware function to register
37
"""
38
39
def response_middleware(self, middleware_fn):
40
"""
41
Register response middleware function.
42
43
Parameters:
44
- middleware_fn: Middleware function to register
45
"""
46
47
def add_middleware(
48
self,
49
middleware_fn,
50
attach_to: str = "request"
51
):
52
"""
53
Add middleware programmatically.
54
55
Parameters:
56
- middleware_fn: Middleware function
57
- attach_to: Middleware type ('request' or 'response')
58
"""
59
```
60
61
### Request Middleware
62
63
Process incoming requests before they reach route handlers.
64
65
```python { .api }
66
async def request_middleware_function(request):
67
"""
68
Request middleware function signature.
69
70
Parameters:
71
- request: Request object
72
73
The middleware can:
74
- Modify the request object
75
- Add data to request.ctx
76
- Return early response to short-circuit processing
77
- Raise exceptions for error handling
78
79
Returns:
80
- None: Continue processing
81
- HTTPResponse: Short-circuit with response
82
"""
83
```
84
85
### Response Middleware
86
87
Process responses after route handlers complete.
88
89
```python { .api }
90
async def response_middleware_function(request, response):
91
"""
92
Response middleware function signature.
93
94
Parameters:
95
- request: Original request object
96
- response: Response object from handler
97
98
The middleware can:
99
- Modify response headers
100
- Transform response body
101
- Add tracking/logging information
102
- Handle cleanup operations
103
104
Returns:
105
- None: Use provided response
106
- HTTPResponse: Replace with new response
107
"""
108
```
109
110
### Signal System
111
112
Event-driven programming system for application lifecycle and custom events.
113
114
```python { .api }
115
class Signal:
116
"""Signal class for event management."""
117
118
def __init__(
119
self,
120
event: str = None,
121
conditions: dict = None,
122
exclusive: bool = True
123
):
124
"""
125
Initialize signal.
126
127
Parameters:
128
- event: Signal event name
129
- conditions: Signal conditions
130
- exclusive: Whether signal is exclusive
131
"""
132
133
async def send(
134
self,
135
*args,
136
**kwargs
137
):
138
"""
139
Send signal asynchronously.
140
141
Parameters:
142
- *args: Signal arguments
143
- **kwargs: Signal keyword arguments
144
"""
145
146
def send_sync(
147
self,
148
*args,
149
**kwargs
150
):
151
"""
152
Send signal synchronously.
153
154
Parameters:
155
- *args: Signal arguments
156
- **kwargs: Signal keyword arguments
157
"""
158
159
def signal(event: str, **kwargs):
160
"""
161
Decorator for signal handlers.
162
163
Parameters:
164
- event: Signal event name
165
- **kwargs: Signal conditions
166
167
Usage:
168
@app.signal("http.lifecycle.request")
169
async def handle_request_signal(request):
170
# Handle request lifecycle signal
171
pass
172
"""
173
```
174
175
### Built-in Signals
176
177
Pre-defined signals for common application lifecycle events.
178
179
```python { .api }
180
# Server lifecycle signals
181
"server.init.before" # Before server initialization
182
"server.init.after" # After server initialization
183
"server.shutdown.before" # Before server shutdown
184
"server.shutdown.after" # After server shutdown
185
186
# HTTP lifecycle signals
187
"http.lifecycle.begin" # HTTP request begins
188
"http.lifecycle.complete" # HTTP request complete
189
"http.lifecycle.exception" # HTTP request exception
190
"http.lifecycle.handle" # HTTP request handling
191
"http.lifecycle.read_body" # HTTP request body read
192
"http.lifecycle.read_head" # HTTP request head read
193
"http.lifecycle.request" # HTTP request object created
194
"http.lifecycle.response" # HTTP response object created
195
"http.lifecycle.send" # HTTP response send
196
197
# Middleware signals
198
"http.middleware.before" # Before middleware execution
199
"http.middleware.after" # After middleware execution
200
201
# Routing signals
202
"http.routing.before" # Before routing
203
"http.routing.after" # After routing
204
205
# WebSocket signals
206
"websocket.before" # Before WebSocket connection
207
"websocket.after" # After WebSocket connection
208
```
209
210
### Signal Registration
211
212
Register signal handlers for built-in and custom signals.
213
214
```python { .api }
215
def add_signal(
216
self,
217
handler,
218
event: str,
219
**conditions
220
):
221
"""
222
Add signal handler programmatically.
223
224
Parameters:
225
- handler: Signal handler function
226
- event: Signal event name
227
- **conditions: Signal conditions
228
"""
229
230
def signal_handler(event: str, **conditions):
231
"""
232
Signal handler decorator.
233
234
Parameters:
235
- event: Signal event name
236
- **conditions: Signal conditions
237
"""
238
```
239
240
### Custom Signals
241
242
Create and dispatch custom application signals.
243
244
```python { .api }
245
def dispatch(
246
event: str,
247
*,
248
context: dict = None,
249
condition: dict = None,
250
fail_not_found: bool = True,
251
inline: bool = False,
252
reverse: bool = False
253
):
254
"""
255
Dispatch custom signal.
256
257
Parameters:
258
- event: Signal event name
259
- context: Signal context data
260
- condition: Signal conditions
261
- fail_not_found: Fail if no handlers found
262
- inline: Execute handlers inline
263
- reverse: Execute handlers in reverse order
264
"""
265
```
266
267
## Usage Examples
268
269
### Basic Middleware
270
271
```python
272
from sanic import Sanic
273
from sanic.response import json
274
import time
275
276
app = Sanic("MyApp")
277
278
@app.middleware('request')
279
async def add_start_time(request):
280
\"\"\"Add request start time for performance tracking.\"\"\"
281
request.ctx.start_time = time.time()
282
283
@app.middleware('response')
284
async def add_process_time(request, response):
285
\"\"\"Add processing time header to response.\"\"\"
286
if hasattr(request.ctx, 'start_time'):
287
process_time = time.time() - request.ctx.start_time
288
response.headers['X-Process-Time'] = str(process_time)
289
290
@app.route("/api/data")
291
async def get_data(request):
292
# Simulate processing
293
await asyncio.sleep(0.1)
294
return json({"data": "example"})
295
```
296
297
### Authentication Middleware
298
299
```python
300
from sanic.response import json
301
from sanic.exceptions import Unauthorized
302
303
@app.middleware('request')
304
async def authenticate_request(request):
305
\"\"\"Authenticate requests to protected endpoints.\"\"\"
306
307
# Skip authentication for public endpoints
308
public_paths = ['/login', '/register', '/health']
309
if request.path in public_paths:
310
return
311
312
# Check for authorization header
313
auth_header = request.headers.get('Authorization')
314
if not auth_header:
315
raise Unauthorized("Authorization header required")
316
317
# Validate token
318
try:
319
token = auth_header.replace('Bearer ', '')
320
user = await validate_token(token)
321
request.ctx.user = user
322
except Exception:
323
raise Unauthorized("Invalid token")
324
325
@app.route("/api/profile")
326
async def get_profile(request):
327
\"\"\"Protected endpoint requiring authentication.\"\"\"
328
return json({"user": request.ctx.user})
329
```
330
331
### CORS Middleware
332
333
```python
334
@app.middleware('response')
335
async def add_cors_headers(request, response):
336
\"\"\"Add CORS headers to all responses.\"\"\"
337
338
response.headers.update({
339
'Access-Control-Allow-Origin': '*',
340
'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS',
341
'Access-Control-Allow-Headers': 'Origin, Accept, Content-Type, X-Requested-With, Authorization',
342
'Access-Control-Max-Age': '86400'
343
})
344
345
@app.middleware('request')
346
async def handle_options_request(request):
347
\"\"\"Handle preflight OPTIONS requests.\"\"\"
348
if request.method == 'OPTIONS':
349
return json({}, status=200)
350
```
351
352
### Logging Middleware
353
354
```python
355
import logging
356
import uuid
357
358
@app.middleware('request')
359
async def add_request_id(request):
360
\"\"\"Add unique request ID for tracing.\"\"\"
361
request.ctx.request_id = str(uuid.uuid4())
362
request.ctx.logger = logging.getLogger('app').bind(request_id=request.ctx.request_id)
363
364
@app.middleware('request')
365
async def log_request(request):
366
\"\"\"Log incoming requests.\"\"\"
367
request.ctx.logger.info(
368
"Request started",
369
method=request.method,
370
path=request.path,
371
remote_addr=request.ip
372
)
373
374
@app.middleware('response')
375
async def log_response(request, response):
376
\"\"\"Log outgoing responses.\"\"\"
377
request.ctx.logger.info(
378
"Request completed",
379
status_code=response.status,
380
content_length=len(response.body) if response.body else 0
381
)
382
```
383
384
### Signal Handlers
385
386
```python
387
@app.signal("server.init.before")
388
async def setup_database(app, **context):
389
\"\"\"Initialize database connection before server starts.\"\"\"
390
app.ctx.db = await create_database_connection()
391
print("Database connection established")
392
393
@app.signal("server.shutdown.before")
394
async def cleanup_database(app, **context):
395
\"\"\"Close database connection before server shuts down.\"\"\"
396
if hasattr(app.ctx, 'db'):
397
await app.ctx.db.close()
398
print("Database connection closed")
399
400
@app.signal("http.lifecycle.request")
401
async def track_request(request, **context):
402
\"\"\"Track request metrics.\"\"\"
403
await increment_request_counter(
404
method=request.method,
405
path=request.path
406
)
407
408
@app.signal("http.lifecycle.exception")
409
async def handle_request_exception(request, exception, **context):
410
\"\"\"Handle request exceptions.\"\"\"
411
await log_exception(
412
request_id=getattr(request.ctx, 'request_id', 'unknown'),
413
exception=exception,
414
path=request.path
415
)
416
```
417
418
### Custom Signals
419
420
```python
421
# Define custom signal events
422
CUSTOM_EVENTS = {
423
"user.created": "user.created",
424
"user.updated": "user.updated",
425
"user.deleted": "user.deleted",
426
"order.placed": "order.placed",
427
"order.fulfilled": "order.fulfilled"
428
}
429
430
@app.signal(CUSTOM_EVENTS["user.created"])
431
async def send_welcome_email(user, **context):
432
\"\"\"Send welcome email when user is created.\"\"\"
433
await send_email(
434
to=user['email'],
435
template='welcome',
436
context={'user': user}
437
)
438
439
@app.signal(CUSTOM_EVENTS["order.placed"])
440
async def process_order(order, **context):
441
\"\"\"Process order when placed.\"\"\"
442
await update_inventory(order['items'])
443
await notify_fulfillment_center(order)
444
445
# Dispatch custom signals from route handlers
446
@app.route("/api/users", methods=["POST"])
447
async def create_user(request):
448
user_data = request.json
449
user = await create_user_in_db(user_data)
450
451
# Dispatch custom signal
452
await app.dispatch(
453
CUSTOM_EVENTS["user.created"],
454
context={"user": user}
455
)
456
457
return json({"user": user}, status=201)
458
```
459
460
### Conditional Middleware
461
462
```python
463
@app.middleware('request')
464
async def rate_limit_middleware(request):
465
\"\"\"Apply rate limiting to API endpoints.\"\"\"
466
467
# Only apply to API routes
468
if not request.path.startswith('/api/'):
469
return
470
471
# Get client identifier
472
client_id = request.ip
473
if 'X-API-Key' in request.headers:
474
client_id = request.headers['X-API-Key']
475
476
# Check rate limit
477
if await is_rate_limited(client_id):
478
return json(
479
{"error": "Rate limit exceeded"},
480
status=429,
481
headers={"Retry-After": "60"}
482
)
483
484
# Track request
485
await track_request(client_id)
486
487
@app.middleware('response')
488
async def cache_control_middleware(request, response):
489
\"\"\"Add cache control headers based on route.\"\"\"
490
491
# Different caching strategies for different endpoints
492
if request.path.startswith('/api/static/'):
493
# Long cache for static content
494
response.headers['Cache-Control'] = 'public, max-age=86400'
495
elif request.path.startswith('/api/'):
496
# No cache for API responses
497
response.headers['Cache-Control'] = 'no-cache, no-store, must-revalidate'
498
else:
499
# Default cache policy
500
response.headers['Cache-Control'] = 'public, max-age=3600'
501
```
502
503
### Advanced Signal Patterns
504
505
```python
506
@app.signal("http.lifecycle.request", priority=1)
507
async def high_priority_request_handler(request, **context):
508
\"\"\"High priority request handler.\"\"\"
509
# This runs first due to higher priority
510
request.ctx.processed_by_high_priority = True
511
512
@app.signal("http.lifecycle.request", priority=0)
513
async def normal_priority_request_handler(request, **context):
514
\"\"\"Normal priority request handler.\"\"\"
515
# This runs after high priority handlers
516
if hasattr(request.ctx, 'processed_by_high_priority'):
517
request.ctx.processing_complete = True
518
519
# Conditional signal handlers
520
@app.signal("http.lifecycle.response", condition={"status_code": 404})
521
async def handle_404_responses(request, response, **context):
522
\"\"\"Handle 404 responses specifically.\"\"\"
523
await log_404(request.path, request.ip)
524
525
@app.signal("http.lifecycle.response", condition={"method": "POST"})
526
async def handle_post_responses(request, response, **context):
527
\"\"\"Handle POST method responses.\"\"\"
528
await audit_post_request(request, response)
529
```