or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

Files

docs

blueprints.mdconfiguration.mdcore-application.mdexceptions.mdindex.mdmiddleware-signals.mdrequest-response.mdserver-deployment.mdwebsockets.md

middleware-signals.mddocs/

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

```