0
# Exception Handling
1
2
Complete HTTP exception hierarchy for proper error handling and response codes with WSGI integration. These exceptions provide a clean way to handle HTTP errors and generate appropriate responses in web applications.
3
4
## Capabilities
5
6
### Base HTTP Exception
7
8
The foundation class for all HTTP exceptions with WSGI application integration.
9
10
```python { .api }
11
class HTTPException(Exception):
12
def __init__(self, description=None, response=None):
13
"""
14
Base class for all HTTP exceptions.
15
16
Parameters:
17
- description: Custom error description
18
- response: Pre-built Response object to use
19
"""
20
21
# Class attributes
22
code: int # HTTP status code
23
description: str # Default error description
24
25
# Properties
26
name: str # Status code name (e.g., "Not Found")
27
28
def get_description(self, environ=None, scope=None):
29
"""
30
Get the error description for display.
31
32
Parameters:
33
- environ: WSGI environment (optional)
34
- scope: ASGI scope (optional)
35
36
Returns:
37
Error description string
38
"""
39
40
def get_body(self, environ=None, scope=None):
41
"""
42
Get the HTML error page body.
43
44
Parameters:
45
- environ: WSGI environment (optional)
46
- scope: ASGI scope (optional)
47
48
Returns:
49
HTML error page content
50
"""
51
52
def get_headers(self, environ=None, scope=None):
53
"""
54
Get response headers for the exception.
55
56
Parameters:
57
- environ: WSGI environment (optional)
58
- scope: ASGI scope (optional)
59
60
Returns:
61
List of (name, value) header tuples
62
"""
63
64
def get_response(self, environ=None, scope=None):
65
"""
66
Get a Response object for this exception.
67
68
Parameters:
69
- environ: WSGI environment (optional)
70
- scope: ASGI scope (optional)
71
72
Returns:
73
Response object with appropriate status and content
74
"""
75
76
def __call__(self, environ, start_response):
77
"""
78
WSGI application interface - allows exceptions to be called directly.
79
80
Parameters:
81
- environ: WSGI environment
82
- start_response: WSGI start_response callable
83
84
Returns:
85
WSGI response iterable
86
"""
87
```
88
89
### 4xx Client Error Exceptions
90
91
Exceptions for client-side errors (400-499 status codes).
92
93
```python { .api }
94
class BadRequest(HTTPException):
95
"""400 Bad Request - The request cannot be fulfilled due to bad syntax."""
96
code = 400
97
description = "The browser (or proxy) sent a request that this server could not understand."
98
99
class BadRequestKeyError(BadRequest, KeyError):
100
"""400 Bad Request caused by missing key in MultiDict-like object."""
101
102
def __init__(self, arg=None, *args, **kwargs):
103
"""
104
Parameters:
105
- arg: The missing key that caused the error
106
"""
107
108
class SecurityError(BadRequest):
109
"""400 Bad Request - Security violation detected."""
110
111
class Unauthorized(HTTPException):
112
"""401 Unauthorized - Authentication is required."""
113
code = 401
114
description = "The server could not verify that you are authorized to access the URL requested."
115
116
def __init__(self, description=None, response=None, www_authenticate=None):
117
"""
118
Parameters:
119
- description: Custom error description
120
- response: Pre-built response
121
- www_authenticate: WWW-Authenticate header value
122
"""
123
124
class Forbidden(HTTPException):
125
"""403 Forbidden - You don't have permission to access the resource."""
126
code = 403
127
description = "You don't have the permission to access the requested resource."
128
129
class NotFound(HTTPException):
130
"""404 Not Found - The requested resource could not be found."""
131
code = 404
132
description = "The requested URL was not found on the server."
133
134
class MethodNotAllowed(HTTPException):
135
"""405 Method Not Allowed - The method is not allowed for this resource."""
136
code = 405
137
description = "The method is not allowed for the requested URL."
138
139
def __init__(self, valid_methods=None, description=None):
140
"""
141
Parameters:
142
- valid_methods: List of allowed HTTP methods
143
- description: Custom error description
144
"""
145
146
class NotAcceptable(HTTPException):
147
"""406 Not Acceptable - Cannot satisfy Accept headers."""
148
code = 406
149
description = "The resource identified by the request is only capable of generating response entities which have content characteristics not acceptable according to the accept headers sent in the request."
150
151
class RequestTimeout(HTTPException):
152
"""408 Request Timeout - The request took too long to process."""
153
code = 408
154
description = "The server closed the network connection because the browser didn't finish the request within the specified time."
155
156
class Conflict(HTTPException):
157
"""409 Conflict - Request conflicts with current state of resource."""
158
code = 409
159
description = "A conflict happened while processing the request."
160
161
class Gone(HTTPException):
162
"""410 Gone - The resource is no longer available."""
163
code = 410
164
description = "The requested URL is no longer available on this server and there is no forwarding address."
165
166
class LengthRequired(HTTPException):
167
"""411 Length Required - Content-Length header is required."""
168
code = 411
169
description = "A request with this method requires a valid Content-Length header."
170
171
class PreconditionFailed(HTTPException):
172
"""412 Precondition Failed - Precondition in headers failed."""
173
code = 412
174
description = "The precondition on the request for the URL failed positive evaluation."
175
176
class RequestEntityTooLarge(HTTPException):
177
"""413 Payload Too Large - Request entity is too large."""
178
code = 413
179
description = "The data value transmitted exceeds the capacity limit."
180
181
class RequestURITooLarge(HTTPException):
182
"""414 URI Too Long - The request URI is too long."""
183
code = 414
184
description = "The length of the requested URL exceeds the capacity limit for this server."
185
186
class UnsupportedMediaType(HTTPException):
187
"""415 Unsupported Media Type - Media type not supported."""
188
code = 415
189
description = "The server does not support the media type transmitted in the request."
190
191
class RequestedRangeNotSatisfiable(HTTPException):
192
"""416 Range Not Satisfiable - Cannot satisfy Range header."""
193
code = 416
194
description = "The server cannot provide the requested range."
195
196
def __init__(self, length=None, units="bytes", description=None):
197
"""
198
Parameters:
199
- length: Total content length
200
- units: Range units (default: "bytes")
201
- description: Custom error description
202
"""
203
204
class ExpectationFailed(HTTPException):
205
"""417 Expectation Failed - Cannot meet Expect header requirements."""
206
code = 417
207
description = "The server could not meet the requirements of the Expect header."
208
209
class ImATeapot(HTTPException):
210
"""418 I'm a teapot - RFC 2324 April Fools' joke."""
211
code = 418
212
description = "This server is a teapot, not a coffee machine."
213
214
class MisdirectedRequest(HTTPException):
215
"""421 Misdirected Request - Request was directed at wrong server."""
216
code = 421
217
description = "The request was directed at a server that is not able to produce a response."
218
219
class UnprocessableEntity(HTTPException):
220
"""422 Unprocessable Entity - Request is well-formed but semantically incorrect."""
221
code = 422
222
description = "The request was well-formed but was unable to be followed due to semantic errors."
223
224
class Locked(HTTPException):
225
"""423 Locked - Resource is locked."""
226
code = 423
227
description = "The resource that is being accessed is locked."
228
229
class FailedDependency(HTTPException):
230
"""424 Failed Dependency - Request failed due to failure of previous request."""
231
code = 424
232
description = "The request failed because it depended on another request and that request failed."
233
234
class PreconditionRequired(HTTPException):
235
"""428 Precondition Required - Precondition headers are required."""
236
code = 428
237
description = "This request is required to be conditional."
238
239
class TooManyRequests(HTTPException):
240
"""429 Too Many Requests - Rate limit exceeded."""
241
code = 429
242
description = "This user has exceeded an allotted request count."
243
244
def __init__(self, description=None, response=None, retry_after=None):
245
"""
246
Parameters:
247
- description: Custom error description
248
- response: Pre-built response
249
- retry_after: Seconds to wait before retrying
250
"""
251
252
class RequestHeaderFieldsTooLarge(HTTPException):
253
"""431 Request Header Fields Too Large - Headers are too large."""
254
code = 431
255
description = "One or more header fields exceeds the maximum size."
256
257
class UnavailableForLegalReasons(HTTPException):
258
"""451 Unavailable For Legal Reasons - Content blocked for legal reasons."""
259
code = 451
260
description = "Unavailable for legal reasons."
261
```
262
263
### 5xx Server Error Exceptions
264
265
Exceptions for server-side errors (500-599 status codes).
266
267
```python { .api }
268
class InternalServerError(HTTPException):
269
"""500 Internal Server Error - Generic server error."""
270
code = 500
271
description = "The server encountered an internal error and was unable to complete your request."
272
273
class NotImplemented(HTTPException):
274
"""501 Not Implemented - Functionality not implemented."""
275
code = 501
276
description = "The server does not support the action requested by the browser."
277
278
class BadGateway(HTTPException):
279
"""502 Bad Gateway - Invalid response from upstream server."""
280
code = 502
281
description = "The proxy server received an invalid response from an upstream server."
282
283
class ServiceUnavailable(HTTPException):
284
"""503 Service Unavailable - Service temporarily unavailable."""
285
code = 503
286
description = "The server is temporarily unable to service your request due to maintenance downtime or capacity problems."
287
288
def __init__(self, description=None, response=None, retry_after=None):
289
"""
290
Parameters:
291
- description: Custom error description
292
- response: Pre-built response
293
- retry_after: Seconds until service may be available again
294
"""
295
296
class GatewayTimeout(HTTPException):
297
"""504 Gateway Timeout - Timeout from upstream server."""
298
code = 504
299
description = "The connection to an upstream server timed out."
300
301
class HTTPVersionNotSupported(HTTPException):
302
"""505 HTTP Version Not Supported - HTTP version not supported."""
303
code = 505
304
description = "The server does not support the HTTP protocol version used in the request."
305
```
306
307
### Exception Utilities
308
309
Helper functions and classes for working with HTTP exceptions.
310
311
```python { .api }
312
def abort(status, *args, **kwargs):
313
"""
314
Raise an HTTPException for the given status code.
315
316
Parameters:
317
- status: HTTP status code (int) or Response object
318
- *args: Arguments passed to exception constructor
319
- **kwargs: Keyword arguments passed to exception constructor
320
321
Raises:
322
Appropriate HTTPException subclass for the status code
323
324
Examples:
325
- abort(404) → raises NotFound
326
- abort(500, description="Custom error") → raises InternalServerError with custom description
327
"""
328
329
class Aborter:
330
def __init__(self, mapping=None, extra=None):
331
"""
332
Exception raiser with customizable status code mapping.
333
334
Parameters:
335
- mapping: Dict mapping status codes to exception classes
336
- extra: Additional exception mappings
337
"""
338
339
def __call__(self, status, *args, **kwargs):
340
"""
341
Raise exception for status code.
342
343
Parameters:
344
- status: HTTP status code or Response object
345
- *args: Arguments for exception constructor
346
- **kwargs: Keyword arguments for exception constructor
347
"""
348
349
# Default aborter instance
350
default_exceptions = {
351
400: BadRequest,
352
401: Unauthorized,
353
403: Forbidden,
354
404: NotFound,
355
405: MethodNotAllowed,
356
406: NotAcceptable,
357
408: RequestTimeout,
358
409: Conflict,
359
410: Gone,
360
# ... complete mapping available
361
}
362
```
363
364
## Usage Examples
365
366
### Basic Exception Handling
367
368
```python
369
from werkzeug.exceptions import HTTPException, NotFound, BadRequest, InternalServerError
370
from werkzeug.wrappers import Request, Response
371
372
def view_function(request):
373
"""Example view that may raise exceptions."""
374
375
# Check for required parameter
376
if 'id' not in request.args:
377
raise BadRequest("Missing 'id' parameter")
378
379
user_id = request.args.get('id')
380
381
try:
382
user_id = int(user_id)
383
except ValueError:
384
raise BadRequest("Invalid user ID format")
385
386
# Simulate database lookup
387
if user_id == 0:
388
raise BadRequest("User ID cannot be zero")
389
elif user_id < 0:
390
raise NotFound("User not found")
391
elif user_id > 1000:
392
raise InternalServerError("Database connection failed")
393
394
return f"User {user_id} profile"
395
396
def application(environ, start_response):
397
"""WSGI application with exception handling."""
398
request = Request(environ)
399
400
try:
401
result = view_function(request)
402
response = Response(result)
403
404
except HTTPException as e:
405
# HTTP exceptions can be used directly as WSGI applications
406
return e(environ, start_response)
407
408
except Exception as e:
409
# Convert unexpected exceptions to 500 errors
410
error = InternalServerError("An unexpected error occurred")
411
return error(environ, start_response)
412
413
return response(environ, start_response)
414
```
415
416
### Custom Error Pages
417
418
```python
419
from werkzeug.exceptions import HTTPException, NotFound, InternalServerError
420
from werkzeug.wrappers import Response
421
422
def custom_error_handler(exception, environ=None):
423
"""Create custom error responses."""
424
425
if isinstance(exception, NotFound):
426
# Custom 404 page
427
html = f"""
428
<html>
429
<head><title>Page Not Found</title></head>
430
<body>
431
<h1>Oops! Page Not Found</h1>
432
<p>The page you're looking for doesn't exist.</p>
433
<p>Error code: {exception.code}</p>
434
<a href="/">Go back to homepage</a>
435
</body>
436
</html>
437
"""
438
return Response(html, status=404, mimetype='text/html')
439
440
elif isinstance(exception, InternalServerError):
441
# Custom 500 page (don't leak internal details in production)
442
html = """
443
<html>
444
<head><title>Server Error</title></head>
445
<body>
446
<h1>Something went wrong</h1>
447
<p>We're working to fix this issue. Please try again later.</p>
448
</body>
449
</html>
450
"""
451
return Response(html, status=500, mimetype='text/html')
452
453
else:
454
# Use default error page for other exceptions
455
return exception.get_response(environ)
456
457
def application_with_custom_errors(environ, start_response):
458
"""WSGI app with custom error handling."""
459
request = Request(environ)
460
461
try:
462
# Your application logic here
463
if request.path == '/':
464
response = Response('Hello World!')
465
elif request.path == '/error':
466
raise InternalServerError("Intentional error for testing")
467
else:
468
raise NotFound()
469
470
except HTTPException as e:
471
response = custom_error_handler(e, environ)
472
473
return response(environ, start_response)
474
```
475
476
### Using abort() Function
477
478
```python
479
from werkzeug.exceptions import abort
480
from werkzeug.wrappers import Request, Response
481
482
def protected_view(request):
483
"""Example of using abort() for concise error handling."""
484
485
# Check authentication
486
if 'Authorization' not in request.headers:
487
abort(401, description="Authentication required")
488
489
# Check permissions
490
if not user_has_permission(request):
491
abort(403, description="Insufficient permissions")
492
493
# Validate input
494
if not request.json:
495
abort(400, description="JSON data required")
496
497
data = request.json
498
499
if 'name' not in data:
500
abort(400, description="Missing 'name' field")
501
502
# Process valid request
503
return {"message": f"Hello {data['name']}!"}
504
505
def user_has_permission(request):
506
"""Mock permission check."""
507
# In real application, implement proper permission checking
508
return request.headers.get('Authorization') == 'Bearer valid-token'
509
510
def rest_api_app(environ, start_response):
511
"""REST API with abort-based error handling."""
512
request = Request(environ)
513
514
try:
515
if request.method == 'POST' and request.path == '/api/users':
516
result = protected_view(request)
517
response = Response(json.dumps(result), mimetype='application/json')
518
else:
519
abort(404, description="API endpoint not found")
520
521
except HTTPException as e:
522
# Return JSON error responses for API
523
error_data = {
524
'error': e.name,
525
'code': e.code,
526
'description': e.get_description()
527
}
528
response = Response(
529
json.dumps(error_data),
530
status=e.code,
531
mimetype='application/json'
532
)
533
534
return response(environ, start_response)
535
```
536
537
### Exception Middleware
538
539
```python
540
import traceback
541
import logging
542
from werkzeug.exceptions import HTTPException, InternalServerError
543
544
class ExceptionMiddleware:
545
"""Middleware for comprehensive exception handling."""
546
547
def __init__(self, app, debug=False, logger=None):
548
self.app = app
549
self.debug = debug
550
self.logger = logger or logging.getLogger(__name__)
551
552
def __call__(self, environ, start_response):
553
"""WSGI middleware interface."""
554
555
try:
556
return self.app(environ, start_response)
557
558
except HTTPException as e:
559
# Log HTTP exceptions for monitoring
560
self.logger.warning(f"HTTP {e.code}: {e.description}")
561
return e(environ, start_response)
562
563
except Exception as e:
564
# Log unexpected exceptions
565
self.logger.error(f"Unhandled exception: {e}")
566
self.logger.error(traceback.format_exc())
567
568
# Create appropriate error response
569
if self.debug:
570
# In debug mode, show full traceback
571
error_html = f"""
572
<html>
573
<head><title>Debug Error</title></head>
574
<body>
575
<h1>Unhandled Exception</h1>
576
<h2>{type(e).__name__}: {e}</h2>
577
<pre>{traceback.format_exc()}</pre>
578
</body>
579
</html>
580
"""
581
error = InternalServerError()
582
error.description = "Debug information available"
583
response = Response(error_html, status=500, mimetype='text/html')
584
else:
585
# In production, use generic error
586
error = InternalServerError("An unexpected error occurred")
587
response = error.get_response(environ)
588
589
return response(environ, start_response)
590
591
# Usage
592
def create_app(debug=False):
593
def app(environ, start_response):
594
request = Request(environ)
595
596
if request.path == '/crash':
597
raise ValueError("Intentional crash for testing")
598
599
response = Response(f'Hello from {request.path}')
600
return response(environ, start_response)
601
602
# Wrap with exception middleware
603
return ExceptionMiddleware(app, debug=debug)
604
```
605
606
### Custom Exception Classes
607
608
```python
609
from werkzeug.exceptions import HTTPException, BadRequest
610
611
class ValidationError(BadRequest):
612
"""Custom exception for validation errors."""
613
614
def __init__(self, field=None, message=None):
615
self.field = field
616
description = f"Validation failed"
617
if field:
618
description += f" for field '{field}'"
619
if message:
620
description += f": {message}"
621
super().__init__(description)
622
623
class QuotaExceeded(HTTPException):
624
"""Custom exception for quota/rate limiting."""
625
code = 429
626
description = "Request quota exceeded"
627
628
def __init__(self, quota_type=None, retry_after=None):
629
self.quota_type = quota_type
630
self.retry_after = retry_after
631
632
description = "Request quota exceeded"
633
if quota_type:
634
description += f" for {quota_type}"
635
636
super().__init__(description)
637
638
def get_headers(self, environ=None, scope=None):
639
"""Add Retry-After header."""
640
headers = super().get_headers(environ, scope)
641
642
if self.retry_after:
643
headers.append(('Retry-After', str(self.retry_after)))
644
645
return headers
646
647
class APIError(HTTPException):
648
"""Base class for API-specific errors."""
649
650
def get_response(self, environ=None, scope=None):
651
"""Return JSON error response."""
652
error_data = {
653
'error': {
654
'code': self.code,
655
'name': self.name,
656
'description': self.get_description(environ, scope)
657
}
658
}
659
660
return Response(
661
json.dumps(error_data, indent=2),
662
status=self.code,
663
mimetype='application/json',
664
headers=self.get_headers(environ, scope)
665
)
666
667
# Usage of custom exceptions
668
def validate_user_data(data):
669
"""Validate user data and raise custom exceptions."""
670
671
if 'email' not in data:
672
raise ValidationError('email', 'Email is required')
673
674
if '@' not in data['email']:
675
raise ValidationError('email', 'Invalid email format')
676
677
if len(data.get('password', '')) < 8:
678
raise ValidationError('password', 'Password must be at least 8 characters')
679
680
def api_endpoint(request):
681
"""API endpoint using custom exceptions."""
682
683
# Check rate limiting
684
if exceeded_rate_limit(request):
685
raise QuotaExceeded('requests', retry_after=60)
686
687
# Validate JSON data
688
if not request.is_json:
689
raise APIError(description="Content-Type must be application/json")
690
691
data = request.get_json()
692
validate_user_data(data)
693
694
# Process valid data
695
return {'status': 'success', 'user_id': 12345}
696
```
697
698
### Exception Testing
699
700
```python
701
from werkzeug.test import Client
702
from werkzeug.exceptions import NotFound, BadRequest
703
704
def test_exceptions():
705
"""Test exception handling in applications."""
706
707
def test_app(environ, start_response):
708
request = Request(environ)
709
710
if request.path == '/404':
711
raise NotFound("Test 404 error")
712
elif request.path == '/400':
713
raise BadRequest("Test 400 error")
714
else:
715
response = Response("OK")
716
return response(environ, start_response)
717
718
client = Client(test_app)
719
720
# Test normal response
721
response = client.get('/')
722
assert response.status_code == 200
723
assert response.text == "OK"
724
725
# Test 404 exception
726
response = client.get('/404')
727
assert response.status_code == 404
728
assert "Test 404 error" in response.text
729
730
# Test 400 exception
731
response = client.get('/400')
732
assert response.status_code == 400
733
assert "Test 400 error" in response.text
734
735
print("All exception tests passed!")
736
737
def test_custom_exceptions():
738
"""Test custom exception behavior."""
739
740
# Test custom exception properties
741
error = ValidationError('email', 'Invalid format')
742
assert error.code == 400
743
assert 'email' in error.description
744
assert 'Invalid format' in error.description
745
746
# Test quota exception with headers
747
quota_error = QuotaExceeded('API requests', retry_after=120)
748
headers = quota_error.get_headers()
749
header_dict = dict(headers)
750
assert header_dict.get('Retry-After') == '120'
751
752
print("Custom exception tests passed!")
753
754
if __name__ == '__main__':
755
test_exceptions()
756
test_custom_exceptions()
757
```
758
759
### Production Error Handling
760
761
```python
762
import os
763
import sys
764
import logging
765
from werkzeug.exceptions import HTTPException, InternalServerError
766
767
# Configure logging for production
768
logging.basicConfig(
769
level=logging.INFO,
770
format='%(asctime)s %(levelname)s %(name)s %(message)s',
771
handlers=[
772
logging.FileHandler('app.log'),
773
logging.StreamHandler(sys.stdout)
774
]
775
)
776
777
def production_error_handler(app):
778
"""Production-ready error handling wrapper."""
779
780
logger = logging.getLogger('error_handler')
781
is_debug = os.getenv('DEBUG', '').lower() in ('1', 'true', 'yes')
782
783
def error_app(environ, start_response):
784
try:
785
return app(environ, start_response)
786
787
except HTTPException as e:
788
# Log client errors (4xx) at info level
789
if 400 <= e.code < 500:
790
logger.info(f"Client error {e.code}: {e.description}")
791
# Log server errors (5xx) at error level
792
else:
793
logger.error(f"Server error {e.code}: {e.description}")
794
795
return e(environ, start_response)
796
797
except Exception as e:
798
# Log all unexpected exceptions as errors
799
logger.error(f"Unhandled exception: {e}", exc_info=True)
800
801
# Create safe error response
802
if is_debug:
803
# Show details in debug mode
804
error = InternalServerError(f"Debug: {type(e).__name__}: {e}")
805
else:
806
# Generic message in production
807
error = InternalServerError("Internal server error")
808
809
return error(environ, start_response)
810
811
return error_app
812
813
# Example production application
814
def create_production_app():
815
"""Create production-ready application with proper error handling."""
816
817
def base_app(environ, start_response):
818
request = Request(environ)
819
820
# Your application routes here
821
if request.path == '/':
822
response = Response('Production app running')
823
elif request.path == '/health':
824
response = Response('OK', mimetype='text/plain')
825
else:
826
raise NotFound("Endpoint not found")
827
828
return response(environ, start_response)
829
830
# Wrap with production error handler
831
return production_error_handler(base_app)
832
833
if __name__ == '__main__':
834
from werkzeug.serving import run_simple
835
836
app = create_production_app()
837
run_simple('localhost', 8000, app)
838
```