Manejo de Errores
Un manejo de errores consistente es lo que separa una API amateur de una profesional. Cuando todos los errores tienen el mismo formato, los clientes (frontend, mobile, otros servicios) pueden parsearlos y reaccionar sin adivinar.
1. ValidationError en Serializers
El primer filtro de datos inválidos. DRF lo maneja automáticamente en los serializers:
from rest_framework import serializers
class ProductSerializer(serializers.ModelSerializer):
class Meta:
model = Product
fields = '__all__'
# Validación por campo
def validate_price(self, value):
if value <= 0:
raise serializers.ValidationError("El precio debe ser mayor a 0.")
return value
# Validación cross-field
def validate(self, attrs):
if attrs.get('discount') and attrs.get('discount') > attrs.get('price', 0):
raise serializers.ValidationError(
"El descuento no puede ser mayor al precio."
)
return attrs
2. Excepciones estándar de DRF
| Excepción | Código | Cuándo usarla |
|---|---|---|
NotFound |
404 | Recurso no existe |
PermissionDenied |
403 | Sin permisos |
AuthenticationFailed |
401 | Credenciales inválidas |
MethodNotAllowed |
405 | Método HTTP no soportado |
Throttled |
429 | Límite de tasa excedido |
ValidationError |
400 | Datos inválidos (en serializers) |
from rest_framework.exceptions import NotFound, PermissionDenied
class ProductDetailView(APIView):
def get(self, request, pk):
try:
product = Product.objects.get(pk=pk)
except Product.DoesNotExist:
raise NotFound("Producto no encontrado.")
if not request.user.has_perm('view_product', product):
raise PermissionDenied("No tenés permiso para ver este producto.")
return Response(ProductSerializer(product).data)
3. Excepciones de negocio personalizadas
Cuando las reglas del dominio requieren errores específicos:
from rest_framework.exceptions import APIException
class ResourceLockedException(APIException):
status_code = 423
default_detail = "El recurso está bloqueado."
default_code = "resource_locked"
class InsufficientStockException(APIException):
status_code = 400
default_detail = "Stock insuficiente para completar la operación."
default_code = "insufficient_stock"
class DuplicateResourceException(APIException):
status_code = 409
default_detail = "El recurso ya existe."
default_code = "duplicate"
Con mensaje dinámico:
class BusinessRuleException(APIException):
status_code = 422
default_code = "business_rule_violation"
def __init__(self, message, code=None):
self.detail = message
if code:
self.default_code = code
4. Custom Exception Handler global
Estandariza el formato de TODAS las respuestas de error en un solo lugar:
# core/handlers.py
from rest_framework.views import exception_handler
def custom_exception_handler(exc, context):
"""
Normaliza todas las respuestas de error al mismo formato.
"""
response = exception_handler(exc, context)
if response is not None:
response.data = {
'success': False,
'error': {
'code': exc.__class__.__name__,
'message': _extract_message(exc),
'status_code': response.status_code,
}
}
return response
def _extract_message(exc):
"""Extrae el mensaje de forma segura."""
if hasattr(exc, 'detail'):
if isinstance(exc.detail, dict):
return exc.detail # Errores por campo
return str(exc.detail)
return str(exc)
Registralo en settings:
# settings.py
REST_FRAMEWORK = {
'EXCEPTION_HANDLER': 'core.handlers.custom_exception_handler',
}
Sin handler:
{"detail": "No encontrado."}
Con handler:
{
"success": false,
"error": {
"code": "NotFound",
"message": "No encontrado.",
"status_code": 404
}
}
Cuándo usar cada mecanismo
| Mecanismo | Para qué | Status |
|---|---|---|
ValidationError en serializer |
Datos inválidos del request | 400 |
| Excepciones built-in | Casos estándar (404, 403, 401) | Varios |
| Excepciones de negocio | Reglas de dominio violadas | 400-422 |
| Custom handler global | Estandarizar formato de TODOS los errores | — |
Errores comunes
- No registrar el handler: Sin la config en
settings.py, no se usa. - Excepciones redundantes: No crear un
NotFoundpropio si DRF ya lo tiene. - Mezclar validación con reglas de negocio:
ValidationErrores para datos mal formados; las reglas de dominio van en excepciones custom. - Olvidar
if response is not None: Si la excepción no es de DRF, el handler debe retornarNonepara que Django la maneje como 500.