0
# Error Handling
1
2
Structured error handling with HTTP status codes, custom exception classes, and automatic error response formatting. Flask-RESTPlus provides comprehensive error handling capabilities that integrate with automatic documentation generation.
3
4
## Capabilities
5
6
### Abort Function
7
8
Function for terminating requests with HTTP errors and structured error responses.
9
10
```python { .api }
11
def abort(code=500, message=None, **kwargs):
12
"""
13
Abort the current request with an HTTP error.
14
15
Args:
16
code (int): HTTP status code (default: 500)
17
message (str, optional): Error message
18
**kwargs: Additional error data to include in response
19
20
Raises:
21
HTTPException: Flask HTTP exception with error details
22
"""
23
```
24
25
### Exception Classes
26
27
Custom exception classes for different types of API errors.
28
29
```python { .api }
30
class RestError(Exception):
31
def __init__(self, msg):
32
"""
33
Base class for all Flask-RESTPlus errors.
34
35
Args:
36
msg (str): Error message
37
"""
38
self.msg = msg
39
40
def __str__(self):
41
"""
42
String representation of the error.
43
44
Returns:
45
str: Error message
46
"""
47
48
class ValidationError(RestError):
49
def __init__(self, msg):
50
"""
51
Error for input validation failures.
52
53
Args:
54
msg (str): Validation error message
55
"""
56
57
class SpecsError(RestError):
58
def __init__(self, msg):
59
"""
60
Error for API specification issues.
61
62
Args:
63
msg (str): Specification error message
64
"""
65
```
66
67
### HTTP Status Constants
68
69
HTTP status code constants and utilities for consistent error responses.
70
71
```python { .api }
72
# Available through flask_restplus._http.HTTPStatus
73
class HTTPStatus:
74
# Informational responses
75
CONTINUE = 100
76
SWITCHING_PROTOCOLS = 101
77
PROCESSING = 102
78
79
# Successful responses
80
OK = 200
81
CREATED = 201
82
ACCEPTED = 202
83
NO_CONTENT = 204
84
85
# Redirection messages
86
MOVED_PERMANENTLY = 301
87
FOUND = 302
88
NOT_MODIFIED = 304
89
90
# Client error responses
91
BAD_REQUEST = 400
92
UNAUTHORIZED = 401
93
FORBIDDEN = 403
94
NOT_FOUND = 404
95
METHOD_NOT_ALLOWED = 405
96
NOT_ACCEPTABLE = 406
97
CONFLICT = 409
98
GONE = 410
99
UNPROCESSABLE_ENTITY = 422
100
101
# Server error responses
102
INTERNAL_SERVER_ERROR = 500
103
NOT_IMPLEMENTED = 501
104
BAD_GATEWAY = 502
105
SERVICE_UNAVAILABLE = 503
106
```
107
108
## Usage Examples
109
110
### Basic Error Handling
111
112
```python
113
from flask_restplus import Api, Resource, abort
114
115
api = Api()
116
117
@api.route('/users/<int:user_id>')
118
class User(Resource):
119
def get(self, user_id):
120
# Simulate user lookup
121
user = find_user_by_id(user_id)
122
123
if not user:
124
# Abort with 404 Not Found
125
abort(404, message=f"User {user_id} not found")
126
127
if not user.is_active:
128
# Abort with custom message and additional data
129
abort(403,
130
message="Access denied",
131
reason="User account is inactive",
132
user_id=user_id)
133
134
return {'id': user.id, 'name': user.name}
135
136
def delete(self, user_id):
137
user = find_user_by_id(user_id)
138
139
if not user:
140
abort(404, message="User not found")
141
142
if user.is_admin:
143
abort(400,
144
message="Cannot delete admin user",
145
user_type="admin")
146
147
# Delete user...
148
return {'message': f'User {user_id} deleted'}, 200
149
```
150
151
### Custom Error Responses
152
153
```python
154
from flask_restplus import Api, Resource, abort, fields
155
156
api = Api()
157
158
# Define error response model for documentation
159
error_model = api.model('Error', {
160
'message': fields.String(required=True, description='Error message'),
161
'code': fields.Integer(description='Error code'),
162
'details': fields.Raw(description='Additional error details')
163
})
164
165
@api.route('/orders/<int:order_id>')
166
class Order(Resource):
167
@api.response(404, 'Order not found', error_model)
168
@api.response(400, 'Invalid request', error_model)
169
def get(self, order_id):
170
order = find_order(order_id)
171
172
if not order:
173
abort(404,
174
message="Order not found",
175
code="ORDER_NOT_FOUND",
176
order_id=order_id)
177
178
if order.status == 'cancelled':
179
abort(400,
180
message="Cannot access cancelled order",
181
code="ORDER_CANCELLED",
182
details={
183
'order_id': order_id,
184
'cancelled_at': order.cancelled_at.isoformat(),
185
'reason': order.cancellation_reason
186
})
187
188
return order.to_dict()
189
```
190
191
### Global Error Handlers
192
193
```python
194
from flask_restplus import Api
195
from werkzeug.exceptions import HTTPException
196
import logging
197
198
api = Api()
199
200
@api.errorhandler(ValidationError)
201
def handle_validation_error(error):
202
"""Handle validation errors."""
203
return {
204
'message': 'Validation failed',
205
'error': str(error),
206
'type': 'validation_error'
207
}, 400
208
209
@api.errorhandler(ValueError)
210
def handle_value_error(error):
211
"""Handle value errors."""
212
return {
213
'message': 'Invalid value provided',
214
'error': str(error),
215
'type': 'value_error'
216
}, 400
217
218
@api.errorhandler(KeyError)
219
def handle_key_error(error):
220
"""Handle missing key errors."""
221
return {
222
'message': 'Required field missing',
223
'field': str(error).strip("'\""),
224
'type': 'missing_field'
225
}, 400
226
227
@api.errorhandler(Exception)
228
def handle_unexpected_error(error):
229
"""Handle unexpected errors."""
230
logging.exception("Unexpected error occurred")
231
return {
232
'message': 'An unexpected error occurred',
233
'type': 'internal_error'
234
}, 500
235
236
# Handle specific HTTP exceptions
237
@api.errorhandler(404)
238
def handle_not_found(error):
239
"""Handle 404 errors."""
240
return {
241
'message': 'Resource not found',
242
'type': 'not_found'
243
}, 404
244
```
245
246
### Namespace-Specific Error Handlers
247
248
```python
249
from flask_restplus import Api, Namespace, Resource
250
251
api = Api()
252
ns = api.namespace('users', description='User operations')
253
254
@ns.errorhandler(ValidationError)
255
def handle_user_validation_error(error):
256
"""Handle validation errors in user namespace."""
257
return {
258
'message': 'User validation failed',
259
'error': str(error),
260
'namespace': 'users'
261
}, 400
262
263
@ns.errorhandler(ValueError)
264
def handle_user_value_error(error):
265
"""Handle value errors in user namespace."""
266
return {
267
'message': 'Invalid user data',
268
'error': str(error),
269
'namespace': 'users'
270
}, 400
271
272
@ns.route('/')
273
class UserList(Resource):
274
def post(self):
275
# Validation errors in this namespace will be handled by
276
# the namespace-specific error handlers above
277
user_data = api.payload
278
279
if not user_data.get('email'):
280
raise ValidationError("Email is required")
281
282
if '@' not in user_data['email']:
283
raise ValueError("Invalid email format")
284
285
return {'message': 'User created'}, 201
286
```
287
288
### Validation Error Handling
289
290
```python
291
from flask_restplus import Api, Resource, reqparse, fields
292
from flask_restplus.errors import ValidationError
293
294
api = Api()
295
296
# Model with validation
297
user_model = api.model('User', {
298
'name': fields.String(required=True, min_length=2, max_length=50),
299
'email': fields.String(required=True),
300
'age': fields.Integer(min=0, max=150)
301
})
302
303
@api.route('/users')
304
class UserList(Resource):
305
@api.expect(user_model, validate=True)
306
def post(self):
307
# Validation is automatic when validate=True
308
# Invalid data will trigger ValidationError
309
data = api.payload
310
311
# Additional custom validation
312
if '@' not in data['email']:
313
abort(400,
314
message="Invalid email format",
315
field="email",
316
value=data['email'])
317
318
# Process valid data...
319
return {'message': 'User created'}, 201
320
321
# Custom validation handler
322
@api.errorhandler(ValidationError)
323
def handle_validation_error(error):
324
return {
325
'message': 'Input validation failed',
326
'errors': error.msg if hasattr(error, 'msg') else str(error)
327
}, 400
328
```
329
330
### Request Parser Error Handling
331
332
```python
333
from flask_restplus import Api, Resource, reqparse, inputs
334
335
api = Api()
336
337
parser = reqparse.RequestParser(bundle_errors=True)
338
parser.add_argument('name', type=str, required=True, help='Name is required')
339
parser.add_argument('email', type=inputs.email(), required=True, help='Valid email required')
340
parser.add_argument('age', type=inputs.int_range(0, 150), help='Age must be 0-150')
341
342
@api.route('/register')
343
class Register(Resource):
344
@api.expect(parser)
345
def post(self):
346
try:
347
args = parser.parse_args()
348
# Process registration...
349
return {'message': 'Registration successful'}, 201
350
351
except Exception as e:
352
# Parser errors are automatically formatted
353
# With bundle_errors=True, all errors are returned together:
354
# {
355
# "message": "Input payload validation failed",
356
# "errors": {
357
# "name": "Name is required",
358
# "email": "Valid email required",
359
# "age": "Age must be 0-150"
360
# }
361
# }
362
abort(400, message=str(e))
363
```
364
365
### Business Logic Error Handling
366
367
```python
368
from flask_restplus import Api, Resource, abort
369
370
api = Api()
371
372
class InsufficientFundsError(Exception):
373
def __init__(self, balance, amount):
374
self.balance = balance
375
self.amount = amount
376
super().__init__(f"Insufficient funds: balance {balance}, requested {amount}")
377
378
class AccountLockedError(Exception):
379
def __init__(self, account_id, locked_until):
380
self.account_id = account_id
381
self.locked_until = locked_until
382
super().__init__(f"Account {account_id} locked until {locked_until}")
383
384
@api.errorhandler(InsufficientFundsError)
385
def handle_insufficient_funds(error):
386
return {
387
'message': 'Insufficient funds',
388
'balance': error.balance,
389
'requested_amount': error.amount,
390
'shortfall': error.amount - error.balance,
391
'error_code': 'INSUFFICIENT_FUNDS'
392
}, 400
393
394
@api.errorhandler(AccountLockedError)
395
def handle_account_locked(error):
396
return {
397
'message': 'Account temporarily locked',
398
'account_id': error.account_id,
399
'locked_until': error.locked_until.isoformat(),
400
'error_code': 'ACCOUNT_LOCKED'
401
}, 423 # HTTP 423 Locked
402
403
@api.route('/accounts/<int:account_id>/withdraw')
404
class Withdraw(Resource):
405
def post(self, account_id):
406
account = find_account(account_id)
407
amount = api.payload.get('amount', 0)
408
409
if not account:
410
abort(404, message="Account not found")
411
412
if account.is_locked():
413
raise AccountLockedError(account_id, account.locked_until)
414
415
if account.balance < amount:
416
raise InsufficientFundsError(account.balance, amount)
417
418
# Process withdrawal...
419
return {'message': 'Withdrawal successful', 'new_balance': account.balance - amount}
420
```
421
422
### Error Response Documentation
423
424
```python
425
from flask_restplus import Api, Resource, fields
426
427
api = Api()
428
429
# Define standard error models for documentation
430
error_model = api.model('Error', {
431
'message': fields.String(required=True, description='Error message'),
432
'error_code': fields.String(description='Machine-readable error code'),
433
'details': fields.Raw(description='Additional error details')
434
})
435
436
validation_error_model = api.model('ValidationError', {
437
'message': fields.String(required=True, description='Validation error message'),
438
'errors': fields.Raw(required=True, description='Field-specific validation errors')
439
})
440
441
@api.route('/products/<int:product_id>')
442
class Product(Resource):
443
@api.response(200, 'Success')
444
@api.response(404, 'Product not found', error_model)
445
@api.response(400, 'Invalid request', error_model)
446
@api.response(500, 'Internal server error', error_model)
447
def get(self, product_id):
448
"""Get a product by ID"""
449
product = find_product(product_id)
450
451
if not product:
452
abort(404,
453
message="Product not found",
454
error_code="PRODUCT_NOT_FOUND",
455
product_id=product_id)
456
457
return product.to_dict()
458
459
@api.expect(product_model, validate=True)
460
@api.response(201, 'Product created')
461
@api.response(400, 'Validation error', validation_error_model)
462
def post(self, product_id):
463
"""Create or update a product"""
464
# Validation handled automatically
465
data = api.payload
466
# Process product...
467
return {'message': 'Product created'}, 201
468
```
469
470
### Custom Error Formatting
471
472
```python
473
from flask_restplus import Api
474
from flask import jsonify
475
import traceback
476
import uuid
477
478
api = Api()
479
480
def format_error_response(message, code=None, details=None, trace_id=None):
481
"""Format consistent error responses."""
482
response = {
483
'success': False,
484
'message': message,
485
'timestamp': datetime.utcnow().isoformat(),
486
'trace_id': trace_id or str(uuid.uuid4())
487
}
488
489
if code:
490
response['error_code'] = code
491
492
if details:
493
response['details'] = details
494
495
return response
496
497
@api.errorhandler(Exception)
498
def handle_error(error):
499
"""Global error handler with consistent formatting."""
500
trace_id = str(uuid.uuid4())
501
502
# Log error with trace ID for debugging
503
logging.error(f"Error {trace_id}: {str(error)}")
504
logging.error(f"Traceback {trace_id}: {traceback.format_exc()}")
505
506
if isinstance(error, HTTPException):
507
return format_error_response(
508
message=error.description or "HTTP error occurred",
509
code=f"HTTP_{error.code}",
510
trace_id=trace_id
511
), error.code
512
513
# Handle other exceptions
514
return format_error_response(
515
message="An unexpected error occurred",
516
code="INTERNAL_ERROR",
517
details={"type": type(error).__name__},
518
trace_id=trace_id
519
), 500
520
521
@api.route('/test-error')
522
class TestError(Resource):
523
def get(self):
524
# This will trigger the global error handler
525
raise ValueError("This is a test error")
526
```
527
528
### Error Context and Debugging
529
530
```python
531
from flask_restplus import Api, Resource, abort
532
from flask import g, request
533
import logging
534
535
api = Api()
536
537
@api.before_request
538
def before_request():
539
"""Set up request context for error handling."""
540
g.request_id = str(uuid.uuid4())
541
g.start_time = time.time()
542
543
def log_error_context(error, status_code):
544
"""Log error with request context."""
545
context = {
546
'request_id': getattr(g, 'request_id', 'unknown'),
547
'method': request.method,
548
'url': request.url,
549
'user_agent': request.headers.get('User-Agent'),
550
'remote_addr': request.remote_addr,
551
'status_code': status_code,
552
'error': str(error),
553
'duration_ms': (time.time() - getattr(g, 'start_time', 0)) * 1000
554
}
555
556
logging.error(f"Request failed: {context}")
557
558
@api.errorhandler(Exception)
559
def handle_error_with_context(error):
560
"""Error handler that logs full request context."""
561
status_code = 500
562
563
if isinstance(error, HTTPException):
564
status_code = error.code
565
566
log_error_context(error, status_code)
567
568
return {
569
'message': 'An error occurred',
570
'request_id': getattr(g, 'request_id', 'unknown'),
571
'status': status_code
572
}, status_code
573
```