0
# Error Handling
1
2
Comprehensive error handling for context lifecycle, configuration, and plugin validation. The starlette-context library provides custom exceptions with support for custom error responses and detailed error messages.
3
4
## Capabilities
5
6
### Base Exception Classes
7
8
Foundation exception classes that establish the error hierarchy for the library.
9
10
```python { .api }
11
class StarletteContextError(Exception):
12
"""Base exception class for all starlette-context errors."""
13
pass
14
15
class ContextDoesNotExistError(RuntimeError, StarletteContextError):
16
"""
17
Raised when context is accessed outside of a request-response cycle.
18
19
This occurs when:
20
- Context is accessed before middleware has created it
21
- Context is accessed after request processing is complete
22
- Context is accessed in code not running within a request context
23
"""
24
25
def __init__(self) -> None:
26
self.message = (
27
"You didn't use the required middleware or "
28
"you're trying to access `context` object "
29
"outside of the request-response cycle."
30
)
31
super().__init__(self.message)
32
33
class ConfigurationError(StarletteContextError):
34
"""
35
Raised for configuration errors in middleware or plugins.
36
37
Common causes:
38
- Invalid plugin instances passed to middleware
39
- Unsupported UUID versions in UUID plugins
40
- Invalid middleware configuration
41
"""
42
pass
43
```
44
45
### Middleware Validation Errors
46
47
Errors that occur during request processing and can return custom HTTP responses.
48
49
```python { .api }
50
class MiddleWareValidationError(StarletteContextError):
51
"""
52
Base class for middleware validation errors.
53
54
These errors can include custom HTTP responses that will be returned
55
to the client instead of the default error response.
56
"""
57
58
def __init__(
59
self, *args: Any, error_response: Optional[Response] = None
60
) -> None:
61
"""
62
Initialize validation error.
63
64
Parameters:
65
- *args: Exception arguments
66
- error_response: Optional custom HTTP response for this error
67
"""
68
super().__init__(*args)
69
self.error_response = error_response
70
71
class WrongUUIDError(MiddleWareValidationError):
72
"""
73
Raised when UUID validation fails in UUID-based plugins.
74
75
Occurs when:
76
- Invalid UUID format in request headers
77
- UUID version mismatch
78
- Malformed UUID strings
79
"""
80
pass
81
82
class DateFormatError(MiddleWareValidationError):
83
"""
84
Raised when date header parsing fails in DateHeaderPlugin.
85
86
Occurs when:
87
- Invalid RFC1123 date format
88
- Non-GMT timezone specified
89
- Malformed date strings
90
"""
91
pass
92
```
93
94
## Usage Examples
95
96
### Context Existence Checking
97
98
```python
99
from starlette_context import context
100
from starlette_context.errors import ContextDoesNotExistError
101
102
def safe_context_access():
103
"""Safely access context with error handling."""
104
try:
105
user_id = context["user_id"]
106
return f"User: {user_id}"
107
except ContextDoesNotExistError:
108
return "Context not available"
109
110
# Better approach: check existence first
111
def preferred_safe_access():
112
"""Preferred way to safely access context."""
113
if context.exists():
114
return context.get("user_id", "No user ID")
115
return "Context not available"
116
```
117
118
### Plugin Configuration Errors
119
120
```python
121
from starlette_context.middleware import ContextMiddleware
122
from starlette_context.errors import ConfigurationError
123
from starlette_context.plugins import RequestIdPlugin
124
125
try:
126
# This will raise ConfigurationError - invalid plugin
127
app.add_middleware(
128
ContextMiddleware,
129
plugins=["not_a_plugin", RequestIdPlugin()] # String is not a Plugin
130
)
131
except ConfigurationError as e:
132
print(f"Configuration error: {e}")
133
# Fix: Use only valid Plugin instances
134
app.add_middleware(
135
ContextMiddleware,
136
plugins=[RequestIdPlugin()]
137
)
138
```
139
140
### UUID Validation Errors
141
142
```python
143
from starlette_context.plugins import RequestIdPlugin, CorrelationIdPlugin
144
from starlette_context.errors import WrongUUIDError
145
from starlette.responses import JSONResponse
146
147
# Custom error response for UUID validation
148
uuid_error_response = JSONResponse(
149
{
150
"error": "Invalid UUID format",
151
"message": "Request ID must be a valid UUID v4",
152
"example": "550e8400-e29b-41d4-a716-446655440000"
153
},
154
status_code=422
155
)
156
157
# Plugin with custom error response
158
request_id_plugin = RequestIdPlugin(
159
validate=True,
160
error_response=uuid_error_response
161
)
162
163
app.add_middleware(
164
ContextMiddleware,
165
plugins=[request_id_plugin]
166
)
167
168
# Test invalid UUID
169
# Request with header: X-Request-ID: invalid-uuid
170
# Will return the custom JSON error response
171
```
172
173
### Date Format Errors
174
175
```python
176
from starlette_context.plugins import DateHeaderPlugin
177
from starlette_context.errors import DateFormatError
178
from starlette.responses import JSONResponse
179
180
# Custom error response for date parsing
181
date_error_response = JSONResponse(
182
{
183
"error": "Invalid date format",
184
"message": "Date header must be in RFC1123 format",
185
"expected_format": "Wed, 01 Jan 2020 04:27:12 GMT",
186
"received": None # Will be filled by middleware
187
},
188
status_code=422
189
)
190
191
date_plugin = DateHeaderPlugin(error_response=date_error_response)
192
193
app.add_middleware(ContextMiddleware, plugins=[date_plugin])
194
195
# Test invalid date
196
# Request with header: Date: invalid-date-format
197
# Will return the custom JSON error response
198
```
199
200
### Middleware Error Handling
201
202
```python
203
from starlette_context.middleware import ContextMiddleware
204
from starlette_context.errors import MiddleWareValidationError
205
from starlette.responses import JSONResponse
206
207
# Default error response for all validation errors
208
default_error = JSONResponse(
209
{"error": "Request validation failed"},
210
status_code=400
211
)
212
213
app.add_middleware(
214
ContextMiddleware,
215
plugins=[
216
RequestIdPlugin(validate=True), # No custom error response
217
CorrelationIdPlugin(validate=True) # No custom error response
218
],
219
default_error_response=default_error
220
)
221
222
# Validation errors without custom responses will use default_error_response
223
```
224
225
### Custom Plugin Error Handling
226
227
```python
228
from starlette_context.plugins import Plugin
229
from starlette_context.errors import MiddleWareValidationError
230
from starlette.responses import JSONResponse
231
232
class ApiKeyValidationPlugin(Plugin):
233
key = "X-API-Key"
234
235
def __init__(self, valid_keys, error_response=None):
236
self.valid_keys = set(valid_keys)
237
self.error_response = error_response or JSONResponse(
238
{"error": "Invalid API key"},
239
status_code=401
240
)
241
242
async def process_request(self, request):
243
api_key = await self.extract_value_from_header_by_key(request)
244
245
if not api_key:
246
raise MiddleWareValidationError(
247
"Missing API key",
248
error_response=JSONResponse(
249
{"error": "API key required"},
250
status_code=401
251
)
252
)
253
254
if api_key not in self.valid_keys:
255
raise MiddleWareValidationError(
256
"Invalid API key",
257
error_response=self.error_response
258
)
259
260
return api_key
261
262
# Usage
263
api_plugin = ApiKeyValidationPlugin(
264
valid_keys=["key1", "key2", "key3"]
265
)
266
267
app.add_middleware(ContextMiddleware, plugins=[api_plugin])
268
```
269
270
### Error Response Precedence
271
272
The error response precedence order is:
273
274
1. Plugin-specific error response (from exception)
275
2. Plugin's configured error response
276
3. Middleware's default error response
277
278
```python
279
from starlette_context.plugins import PluginUUIDBase
280
from starlette_context.errors import WrongUUIDError
281
from starlette.responses import JSONResponse
282
283
class CustomUUIDPlugin(PluginUUIDBase):
284
key = "X-Custom-ID"
285
286
def __init__(self):
287
# Plugin-level error response
288
super().__init__(
289
validate=True,
290
error_response=JSONResponse(
291
{"error": "Plugin-level error"},
292
status_code=422
293
)
294
)
295
296
async def process_request(self, request):
297
try:
298
return await super().process_request(request)
299
except WrongUUIDError:
300
# Exception-specific error response (highest precedence)
301
raise WrongUUIDError(
302
"Custom UUID validation failed",
303
error_response=JSONResponse(
304
{"error": "Exception-level error"},
305
status_code=400
306
)
307
)
308
309
# Middleware-level default (lowest precedence)
310
app.add_middleware(
311
ContextMiddleware,
312
plugins=[CustomUUIDPlugin()],
313
default_error_response=JSONResponse(
314
{"error": "Middleware-level error"},
315
status_code=500
316
)
317
)
318
```
319
320
### Logging Errors
321
322
```python
323
import logging
324
from starlette_context.middleware import ContextMiddleware
325
from starlette_context.errors import MiddleWareValidationError
326
327
logger = logging.getLogger(__name__)
328
329
class LoggingContextMiddleware(ContextMiddleware):
330
async def dispatch(self, request, call_next):
331
try:
332
return await super().dispatch(request, call_next)
333
except MiddleWareValidationError as e:
334
# Log validation errors
335
logger.warning(
336
f"Context validation error: {e}",
337
extra={
338
"path": request.url.path,
339
"method": request.method,
340
"headers": dict(request.headers)
341
}
342
)
343
# Re-raise to return error response
344
raise
345
346
app.add_middleware(LoggingContextMiddleware, plugins=[...])
347
```
348
349
### Testing Error Scenarios
350
351
```python
352
import pytest
353
from starlette.testclient import TestClient
354
from starlette_context.errors import ContextDoesNotExistError
355
from starlette_context import context
356
357
def test_context_without_middleware():
358
"""Test accessing context without middleware raises error."""
359
with pytest.raises(ContextDoesNotExistError):
360
value = context["key"]
361
362
def test_invalid_uuid_plugin():
363
"""Test UUID plugin with invalid UUID."""
364
client = TestClient(app) # App with RequestIdPlugin(validate=True)
365
366
response = client.get("/", headers={"X-Request-ID": "invalid-uuid"})
367
assert response.status_code == 422
368
assert "Invalid UUID" in response.json()["error"]
369
370
def test_custom_error_response():
371
"""Test plugin with custom error response."""
372
# Setup app with custom error response
373
client = TestClient(app)
374
375
response = client.get("/", headers={"Date": "invalid-date"})
376
assert response.status_code == 422
377
assert response.json()["error"] == "Invalid date format"
378
```
379
380
## Best Practices
381
382
### Error Response Design
383
384
```python
385
# Good: Structured error responses
386
error_response = JSONResponse(
387
{
388
"error": "validation_failed",
389
"message": "Request validation failed",
390
"details": {
391
"field": "X-Request-ID",
392
"expected": "Valid UUID v4",
393
"received": "invalid-format"
394
}
395
},
396
status_code=422
397
)
398
399
# Avoid: Generic or unclear errors
400
error_response = JSONResponse({"error": "Bad request"}, status_code=400)
401
```
402
403
### Error Handling Strategy
404
405
```python
406
# Context access pattern
407
def get_user_context():
408
"""Get user data from context with proper error handling."""
409
if not context.exists():
410
return None
411
412
return {
413
"user_id": context.get("user_id"),
414
"request_id": context.get("X-Request-ID"),
415
"session": context.get("session_id")
416
}
417
418
# Plugin validation pattern
419
class ValidationPlugin(Plugin):
420
def __init__(self, error_response=None):
421
self.error_response = error_response or self.default_error_response
422
423
@property
424
def default_error_response(self):
425
return JSONResponse(
426
{"error": f"Invalid {self.key} header"},
427
status_code=422
428
)
429
430
async def process_request(self, request):
431
value = await self.extract_value_from_header_by_key(request)
432
if not self.validate_value(value):
433
raise MiddleWareValidationError(
434
f"Invalid {self.key}: {value}",
435
error_response=self.error_response
436
)
437
return value
438
```