or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

Files

docs

authentication-permissions.mdexceptions-status.mdfields-relations.mdindex.mdpagination-filtering.mdrequests-responses.mdrouters-urls.mdserializers.mdviews-viewsets.md

exceptions-status.mddocs/

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.