0
# Exceptions & Status
1
2
Django REST Framework provides a comprehensive exception handling system with typed error responses and HTTP status codes. The type stubs ensure type-safe exception handling, error detail formatting, and status code management.
3
4
## Exception Classes
5
6
### APIException
7
8
```python { .api }
9
class APIException(Exception):
10
"""Base class for all REST framework exceptions."""
11
12
status_code: int
13
default_detail: str | dict[str, Any] | list[Any]
14
default_code: str
15
detail: Any
16
17
def __init__(
18
self,
19
detail: str | dict[str, Any] | list[Any] | None = None,
20
code: str | None = None
21
) -> None: ...
22
23
def get_codes(self) -> str | dict[str, Any] | list[Any]:
24
"""
25
Return error codes for the exception.
26
27
Returns:
28
str | dict[str, Any] | list[Any]: Error codes structure
29
"""
30
...
31
32
def get_full_details(self) -> dict[str, Any] | list[dict[str, Any]]:
33
"""
34
Return full error details including codes and messages.
35
36
Returns:
37
dict[str, Any] | list[dict[str, Any]]: Complete error information
38
"""
39
...
40
41
def __str__(self) -> str: ...
42
```
43
44
### Validation Exceptions
45
46
```python { .api }
47
class ValidationError(APIException):
48
"""Exception raised when serializer validation fails."""
49
50
status_code: int # 400
51
default_code: str # 'invalid'
52
53
def __init__(
54
self,
55
detail: str | dict[str, Any] | list[Any] | None = None,
56
code: str | None = None
57
) -> None: ...
58
59
class ParseError(APIException):
60
"""Exception raised when request parsing fails."""
61
62
status_code: int # 400
63
default_detail: str # 'Malformed request.'
64
default_code: str # 'parse_error'
65
```
66
67
### Authentication Exceptions
68
69
```python { .api }
70
class AuthenticationFailed(APIException):
71
"""Exception raised when authentication fails."""
72
73
status_code: int # 401
74
default_detail: str # 'Incorrect authentication credentials.'
75
default_code: str # 'authentication_failed'
76
77
class NotAuthenticated(APIException):
78
"""Exception raised when authentication is required but not provided."""
79
80
status_code: int # 401
81
default_detail: str # 'Authentication credentials were not provided.'
82
default_code: str # 'not_authenticated'
83
```
84
85
### Permission Exceptions
86
87
```python { .api }
88
class PermissionDenied(APIException):
89
"""Exception raised when permission check fails."""
90
91
status_code: int # 403
92
default_detail: str # 'You do not have permission to perform this action.'
93
default_code: str # 'permission_denied'
94
```
95
96
### Resource Exceptions
97
98
```python { .api }
99
class NotFound(APIException):
100
"""Exception raised when a requested resource is not found."""
101
102
status_code: int # 404
103
default_detail: str # 'Not found.'
104
default_code: str # 'not_found'
105
106
class MethodNotAllowed(APIException):
107
"""Exception raised when HTTP method is not allowed."""
108
109
status_code: int # 405
110
default_detail: str # 'Method "{method}" not allowed.'
111
default_code: str # 'method_not_allowed'
112
113
def __init__(self, method: str, detail: str | None = None, code: str | None = None) -> None: ...
114
115
class NotAcceptable(APIException):
116
"""Exception raised when requested content type cannot be satisfied."""
117
118
status_code: int # 406
119
default_detail: str # 'Could not satisfy the request Accept header.'
120
default_code: str # 'not_acceptable'
121
122
class UnsupportedMediaType(APIException):
123
"""Exception raised when request media type is not supported."""
124
125
status_code: int # 415
126
default_detail: str # 'Unsupported media type "{media_type}" in request.'
127
default_code: str # 'unsupported_media_type'
128
129
def __init__(self, media_type: str, detail: str | None = None, code: str | None = None) -> None: ...
130
```
131
132
### Rate Limiting Exception
133
134
```python { .api }
135
class Throttled(APIException):
136
"""Exception raised when request is throttled."""
137
138
status_code: int # 429
139
default_detail: str # 'Request was throttled.'
140
default_code: str # 'throttled'
141
extra_detail_singular: str
142
extra_detail_plural: str
143
144
def __init__(self, wait: float | None = None, detail: str | None = None, code: str | None = None) -> None: ...
145
```
146
147
**Parameters:**
148
- `wait: float | None` - Number of seconds until throttling expires
149
150
## Error Detail Types
151
152
### ErrorDetail
153
154
```python { .api }
155
class ErrorDetail(str):
156
"""Enhanced string that includes an error code."""
157
158
code: str | None
159
160
def __new__(cls, string: str, code: str | None = None) -> ErrorDetail: ...
161
def __eq__(self, other: Any) -> bool: ...
162
def __ne__(self, other: Any) -> bool: ...
163
def __repr__(self) -> str: ...
164
def __hash__(self) -> int: ...
165
```
166
167
### Type Aliases
168
169
```python { .api }
170
# Complex type definitions for error structures
171
_Detail = Union[
172
str,
173
ErrorDetail,
174
dict[str, Any],
175
list[Any]
176
]
177
178
_APIExceptionInput = Union[
179
str,
180
dict[str, Any],
181
list[Any],
182
ErrorDetail
183
]
184
185
_ErrorCodes = Union[
186
str,
187
dict[str, Any],
188
list[Any]
189
]
190
191
_ErrorFullDetails = Union[
192
dict[str, Any],
193
list[dict[str, Any]]
194
]
195
```
196
197
## Exception Handling Functions
198
199
### Error Detail Processing
200
201
```python { .api }
202
def _get_error_details(
203
data: _APIExceptionInput,
204
default_code: str | None = None
205
) -> _Detail:
206
"""
207
Convert exception input to structured error details.
208
209
Args:
210
data: Raw error data
211
default_code: Default error code if none provided
212
213
Returns:
214
_Detail: Structured error details
215
"""
216
...
217
218
def _get_codes(detail: _Detail) -> _ErrorCodes:
219
"""
220
Extract error codes from error details.
221
222
Args:
223
detail: Error details structure
224
225
Returns:
226
_ErrorCodes: Error codes structure
227
"""
228
...
229
230
def _get_full_details(detail: _Detail) -> _ErrorFullDetails:
231
"""
232
Get full error details including codes and messages.
233
234
Args:
235
detail: Error details structure
236
237
Returns:
238
_ErrorFullDetails: Complete error information
239
"""
240
...
241
```
242
243
### Exception Handlers
244
245
```python { .api }
246
def server_error(
247
request: HttpRequest | Request,
248
*args: Any,
249
**kwargs: Any
250
) -> JsonResponse:
251
"""
252
Handle 500 server errors.
253
254
Args:
255
request: Current request object
256
*args: Additional arguments
257
**kwargs: Additional keyword arguments
258
259
Returns:
260
JsonResponse: JSON error response
261
"""
262
...
263
264
def bad_request(
265
request: HttpRequest | Request,
266
exception: Exception,
267
*args: Any,
268
**kwargs: Any
269
) -> JsonResponse:
270
"""
271
Handle 400 bad request errors.
272
273
Args:
274
request: Current request object
275
exception: Exception that caused the error
276
*args: Additional arguments
277
**kwargs: Additional keyword arguments
278
279
Returns:
280
JsonResponse: JSON error response
281
"""
282
...
283
```
284
285
## HTTP Status Codes
286
287
### Status Code Constants
288
289
```python { .api }
290
# Informational responses
291
HTTP_100_CONTINUE: Literal[100]
292
HTTP_101_SWITCHING_PROTOCOLS: Literal[101]
293
HTTP_102_PROCESSING: Literal[102]
294
295
# Success responses
296
HTTP_200_OK: Literal[200]
297
HTTP_201_CREATED: Literal[201]
298
HTTP_202_ACCEPTED: Literal[202]
299
HTTP_203_NON_AUTHORITATIVE_INFORMATION: Literal[203]
300
HTTP_204_NO_CONTENT: Literal[204]
301
HTTP_205_RESET_CONTENT: Literal[205]
302
HTTP_206_PARTIAL_CONTENT: Literal[206]
303
HTTP_207_MULTI_STATUS: Literal[207]
304
HTTP_208_ALREADY_REPORTED: Literal[208]
305
HTTP_226_IM_USED: Literal[226]
306
307
# Redirection responses
308
HTTP_300_MULTIPLE_CHOICES: Literal[300]
309
HTTP_301_MOVED_PERMANENTLY: Literal[301]
310
HTTP_302_FOUND: Literal[302]
311
HTTP_303_SEE_OTHER: Literal[303]
312
HTTP_304_NOT_MODIFIED: Literal[304]
313
HTTP_305_USE_PROXY: Literal[305]
314
HTTP_307_TEMPORARY_REDIRECT: Literal[307]
315
HTTP_308_PERMANENT_REDIRECT: Literal[308]
316
317
# Client error responses
318
HTTP_400_BAD_REQUEST: Literal[400]
319
HTTP_401_UNAUTHORIZED: Literal[401]
320
HTTP_402_PAYMENT_REQUIRED: Literal[402]
321
HTTP_403_FORBIDDEN: Literal[403]
322
HTTP_404_NOT_FOUND: Literal[404]
323
HTTP_405_METHOD_NOT_ALLOWED: Literal[405]
324
HTTP_406_NOT_ACCEPTABLE: Literal[406]
325
HTTP_407_PROXY_AUTHENTICATION_REQUIRED: Literal[407]
326
HTTP_408_REQUEST_TIMEOUT: Literal[408]
327
HTTP_409_CONFLICT: Literal[409]
328
HTTP_410_GONE: Literal[410]
329
HTTP_411_LENGTH_REQUIRED: Literal[411]
330
HTTP_412_PRECONDITION_FAILED: Literal[412]
331
HTTP_413_REQUEST_ENTITY_TOO_LARGE: Literal[413]
332
HTTP_414_REQUEST_URI_TOO_LONG: Literal[414]
333
HTTP_415_UNSUPPORTED_MEDIA_TYPE: Literal[415]
334
HTTP_416_REQUESTED_RANGE_NOT_SATISFIABLE: Literal[416]
335
HTTP_417_EXPECTATION_FAILED: Literal[417]
336
HTTP_418_IM_A_TEAPOT: Literal[418]
337
HTTP_422_UNPROCESSABLE_ENTITY: Literal[422]
338
HTTP_423_LOCKED: Literal[423]
339
HTTP_424_FAILED_DEPENDENCY: Literal[424]
340
HTTP_426_UPGRADE_REQUIRED: Literal[426]
341
HTTP_428_PRECONDITION_REQUIRED: Literal[428]
342
HTTP_429_TOO_MANY_REQUESTS: Literal[429]
343
HTTP_431_REQUEST_HEADER_FIELDS_TOO_LARGE: Literal[431]
344
HTTP_451_UNAVAILABLE_FOR_LEGAL_REASONS: Literal[451]
345
346
# Server error responses
347
HTTP_500_INTERNAL_SERVER_ERROR: Literal[500]
348
HTTP_501_NOT_IMPLEMENTED: Literal[501]
349
HTTP_502_BAD_GATEWAY: Literal[502]
350
HTTP_503_SERVICE_UNAVAILABLE: Literal[503]
351
HTTP_504_GATEWAY_TIMEOUT: Literal[504]
352
HTTP_505_HTTP_VERSION_NOT_SUPPORTED: Literal[505]
353
HTTP_507_INSUFFICIENT_STORAGE: Literal[507]
354
HTTP_508_LOOP_DETECTED: Literal[508]
355
HTTP_510_NOT_EXTENDED: Literal[510]
356
HTTP_511_NETWORK_AUTHENTICATION_REQUIRED: Literal[511]
357
```
358
359
### Status Code Utility Functions
360
361
```python { .api }
362
def is_informational(code: int) -> bool:
363
"""Check if status code is informational (1xx)."""
364
...
365
366
def is_success(code: int) -> bool:
367
"""Check if status code indicates success (2xx)."""
368
...
369
370
def is_redirect(code: int) -> bool:
371
"""Check if status code indicates redirection (3xx)."""
372
...
373
374
def is_client_error(code: int) -> bool:
375
"""Check if status code indicates client error (4xx)."""
376
...
377
378
def is_server_error(code: int) -> bool:
379
"""Check if status code indicates server error (5xx)."""
380
...
381
```
382
383
## Exception Usage Examples
384
385
### Basic Exception Handling
386
387
```python { .api }
388
from rest_framework import exceptions, status
389
from rest_framework.views import APIView
390
from rest_framework.response import Response
391
392
class BookDetailView(APIView):
393
"""Demonstrate basic exception handling."""
394
395
def get(self, request: Request, pk: int) -> Response:
396
"""Retrieve book with proper exception handling."""
397
398
try:
399
book = Book.objects.get(pk=pk)
400
except Book.DoesNotExist:
401
raise exceptions.NotFound("Book not found")
402
403
# Check permissions
404
if not self.has_permission(request, book):
405
raise exceptions.PermissionDenied("Access denied to this book")
406
407
serializer = BookSerializer(book)
408
return Response(serializer.data)
409
410
def put(self, request: Request, pk: int) -> Response:
411
"""Update book with validation error handling."""
412
413
try:
414
book = Book.objects.get(pk=pk)
415
except Book.DoesNotExist:
416
raise exceptions.NotFound("Book not found")
417
418
serializer = BookSerializer(book, data=request.data)
419
if not serializer.is_valid():
420
raise exceptions.ValidationError(serializer.errors)
421
422
serializer.save()
423
return Response(serializer.data)
424
425
def has_permission(self, request: Request, book: Book) -> bool:
426
"""Check if user has permission to access book."""
427
return book.is_public or request.user == book.owner
428
```
429
430
### Custom Exception Classes
431
432
```python { .api }
433
class BookNotAvailableError(exceptions.APIException):
434
"""Custom exception for unavailable books."""
435
436
status_code = status.HTTP_409_CONFLICT
437
default_detail = 'Book is currently not available'
438
default_code = 'book_not_available'
439
440
class QuotaExceededError(exceptions.APIException):
441
"""Custom exception for quota limits."""
442
443
status_code = status.HTTP_429_TOO_MANY_REQUESTS
444
default_detail = 'Download quota exceeded'
445
default_code = 'quota_exceeded'
446
447
def __init__(self, limit: int, reset_time: datetime, detail: str | None = None):
448
if detail is None:
449
detail = f'Quota limit of {limit} exceeded. Resets at {reset_time.isoformat()}'
450
super().__init__(detail)
451
self.limit = limit
452
self.reset_time = reset_time
453
454
class InvalidFileFormatError(exceptions.APIException):
455
"""Custom exception for file format validation."""
456
457
status_code = status.HTTP_415_UNSUPPORTED_MEDIA_TYPE
458
default_detail = 'Invalid file format'
459
default_code = 'invalid_file_format'
460
461
def __init__(self, allowed_formats: list[str], received_format: str):
462
detail = f'Invalid format "{received_format}". Allowed formats: {", ".join(allowed_formats)}'
463
super().__init__(detail)
464
self.allowed_formats = allowed_formats
465
self.received_format = received_format
466
```
467
468
### Exception Handling in Views
469
470
```python { .api }
471
class BookDownloadView(APIView):
472
"""View demonstrating custom exception usage."""
473
474
def post(self, request: Request, pk: int) -> Response:
475
"""Download book with comprehensive error handling."""
476
477
# Get book
478
try:
479
book = Book.objects.get(pk=pk)
480
except Book.DoesNotExist:
481
raise exceptions.NotFound({
482
'error': 'Book not found',
483
'code': 'BOOK_NOT_FOUND',
484
'book_id': pk
485
})
486
487
# Check availability
488
if not book.is_available:
489
raise BookNotAvailableError()
490
491
# Check user quota
492
user_downloads = request.user.downloads.filter(
493
created_at__date=timezone.now().date()
494
).count()
495
496
daily_limit = getattr(request.user, 'daily_download_limit', 10)
497
if user_downloads >= daily_limit:
498
tomorrow = timezone.now().replace(hour=0, minute=0, second=0, microsecond=0) + timedelta(days=1)
499
raise QuotaExceededError(daily_limit, tomorrow)
500
501
# Check file format
502
requested_format = request.data.get('format', 'pdf')
503
allowed_formats = ['pdf', 'epub', 'mobi']
504
505
if requested_format not in allowed_formats:
506
raise InvalidFileFormatError(allowed_formats, requested_format)
507
508
# Process download
509
download = Download.objects.create(
510
user=request.user,
511
book=book,
512
format=requested_format
513
)
514
515
return Response({
516
'download_id': download.id,
517
'download_url': download.get_download_url(),
518
'expires_at': (timezone.now() + timedelta(hours=24)).isoformat()
519
}, status=status.HTTP_201_CREATED)
520
```
521
522
### Validation Error Details
523
524
```python { .api }
525
from rest_framework import serializers
526
527
class BookSerializer(serializers.ModelSerializer):
528
"""Serializer with detailed validation errors."""
529
530
class Meta:
531
model = Book
532
fields = ['title', 'author', 'isbn', 'published_date', 'pages']
533
534
def validate_isbn(self, value: str) -> str:
535
"""Validate ISBN with detailed error."""
536
if len(value) not in [10, 13]:
537
raise serializers.ValidationError(
538
detail='ISBN must be 10 or 13 characters long',
539
code='invalid_length'
540
)
541
542
# Check if ISBN already exists
543
if Book.objects.filter(isbn=value).exists():
544
raise serializers.ValidationError(
545
detail='Book with this ISBN already exists',
546
code='duplicate_isbn'
547
)
548
549
return value
550
551
def validate_pages(self, value: int) -> int:
552
"""Validate page count."""
553
if value <= 0:
554
raise serializers.ValidationError(
555
detail='Page count must be positive',
556
code='invalid_page_count'
557
)
558
559
if value > 10000:
560
raise serializers.ValidationError(
561
detail='Page count exceeds maximum limit of 10,000',
562
code='page_count_too_high'
563
)
564
565
return value
566
567
def validate(self, data: dict[str, Any]) -> dict[str, Any]:
568
"""Cross-field validation."""
569
published_date = data.get('published_date')
570
571
if published_date and published_date > timezone.now().date():
572
raise serializers.ValidationError({
573
'published_date': ErrorDetail(
574
'Published date cannot be in the future',
575
code='future_date'
576
)
577
})
578
579
return data
580
```
581
582
## Custom Exception Handler
583
584
### Global Exception Handler
585
586
```python { .api }
587
from rest_framework.views import exception_handler as drf_exception_handler
588
from rest_framework.response import Response
589
import logging
590
591
logger = logging.getLogger(__name__)
592
593
def custom_exception_handler(exc: Exception, context: dict[str, Any]) -> Response | None:
594
"""Custom exception handler with enhanced error formatting."""
595
596
# Get the standard error response
597
response = drf_exception_handler(exc, context)
598
599
if response is not None:
600
# Log the error
601
request = context.get('request')
602
view = context.get('view')
603
604
logger.error(
605
f"API Error: {exc.__class__.__name__} - {str(exc)} - "
606
f"View: {view.__class__.__name__ if view else 'Unknown'} - "
607
f"User: {getattr(request, 'user', 'Anonymous') if request else 'Unknown'} - "
608
f"Path: {getattr(request, 'path', 'Unknown') if request else 'Unknown'}"
609
)
610
611
# Customize error response format
612
custom_response_data = {
613
'success': False,
614
'error': {
615
'type': exc.__class__.__name__,
616
'message': str(exc),
617
'status_code': response.status_code,
618
'timestamp': timezone.now().isoformat(),
619
}
620
}
621
622
# Add detailed error information for validation errors
623
if isinstance(exc, ValidationError) and hasattr(exc, 'detail'):
624
custom_response_data['error']['details'] = exc.detail
625
626
# Add field-specific error codes
627
if hasattr(exc, 'get_codes'):
628
custom_response_data['error']['codes'] = exc.get_codes()
629
630
# Add request context in debug mode
631
if settings.DEBUG and request:
632
custom_response_data['debug'] = {
633
'request_method': request.method,
634
'request_path': request.path,
635
'user': str(request.user) if hasattr(request, 'user') else 'Unknown',
636
'view_name': view.__class__.__name__ if view else 'Unknown',
637
}
638
639
response.data = custom_response_data
640
641
return response
642
643
# Configure in settings.py:
644
# REST_FRAMEWORK = {
645
# 'EXCEPTION_HANDLER': 'myapp.exceptions.custom_exception_handler'
646
# }
647
```
648
649
### Exception Response Middleware
650
651
```python { .api }
652
class ExceptionResponseMiddleware:
653
"""Middleware for consistent exception response formatting."""
654
655
def __init__(self, get_response: Callable) -> None:
656
self.get_response = get_response
657
658
def __call__(self, request: HttpRequest) -> HttpResponse:
659
response = self.get_response(request)
660
return response
661
662
def process_exception(self, request: HttpRequest, exception: Exception) -> JsonResponse | None:
663
"""Handle exceptions not caught by DRF."""
664
665
if not request.path.startswith('/api/'):
666
return None # Let Django handle non-API requests
667
668
# Handle specific exception types
669
if isinstance(exception, ValueError):
670
return JsonResponse({
671
'success': False,
672
'error': {
673
'type': 'ValueError',
674
'message': 'Invalid input data',
675
'status_code': 400,
676
'timestamp': timezone.now().isoformat()
677
}
678
}, status=400)
679
680
elif isinstance(exception, PermissionError):
681
return JsonResponse({
682
'success': False,
683
'error': {
684
'type': 'PermissionError',
685
'message': 'Permission denied',
686
'status_code': 403,
687
'timestamp': timezone.now().isoformat()
688
}
689
}, status=403)
690
691
# Handle unexpected exceptions
692
logger.exception(f"Unhandled exception in API: {exception}")
693
694
if not settings.DEBUG:
695
return JsonResponse({
696
'success': False,
697
'error': {
698
'type': 'InternalServerError',
699
'message': 'An unexpected error occurred',
700
'status_code': 500,
701
'timestamp': timezone.now().isoformat()
702
}
703
}, status=500)
704
705
return None # Let Django's debug handler take over in debug mode
706
```
707
708
## Testing Exception Handling
709
710
### Exception Testing
711
712
```python { .api }
713
from rest_framework.test import APITestCase
714
from rest_framework import status
715
716
class ExceptionHandlingTests(APITestCase):
717
"""Test exception handling behavior."""
718
719
def test_not_found_exception(self) -> None:
720
"""Test 404 exception handling."""
721
response = self.client.get('/api/books/999/')
722
723
self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
724
self.assertIn('error', response.data)
725
self.assertEqual(response.data['error']['type'], 'NotFound')
726
727
def test_validation_exception(self) -> None:
728
"""Test validation error handling."""
729
invalid_data = {
730
'title': '', # Empty title should fail validation
731
'isbn': '123', # Invalid ISBN length
732
}
733
734
response = self.client.post('/api/books/', invalid_data)
735
736
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
737
self.assertIn('details', response.data['error'])
738
self.assertIn('title', response.data['error']['details'])
739
self.assertIn('isbn', response.data['error']['details'])
740
741
def test_permission_exception(self) -> None:
742
"""Test permission denied handling."""
743
# Create a private book owned by another user
744
other_user = User.objects.create_user('other', 'other@example.com', 'password')
745
book = Book.objects.create(title='Private Book', owner=other_user, is_private=True)
746
747
response = self.client.get(f'/api/books/{book.id}/')
748
749
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
750
self.assertEqual(response.data['error']['type'], 'PermissionDenied')
751
752
def test_custom_exception(self) -> None:
753
"""Test custom exception handling."""
754
# Create scenario that triggers custom exception
755
book = Book.objects.create(title='Test Book', is_available=False)
756
757
response = self.client.post(f'/api/books/{book.id}/download/', {'format': 'pdf'})
758
759
self.assertEqual(response.status_code, status.HTTP_409_CONFLICT)
760
self.assertEqual(response.data['error']['type'], 'BookNotAvailableError')
761
```
762
763
This comprehensive exception and status system provides type-safe error handling with full mypy support, enabling confident implementation of robust error management and HTTP status code handling in Django REST Framework applications.