Django: manejo de excepciones, mejores prácticas y envío de mensajes de error personalizados

Estoy empezando a pensar en el manejo apropiado de excepciones en mi aplicación Django, y mi objective es hacerlo lo más fácil de usar posible. Por la facilidad de uso, sugiero que el usuario siempre debe obtener una aclaración detallada sobre qué fue exactamente lo que salió mal. Siguiendo en este post , la mejor práctica es

use una respuesta JSON con estado 200 para sus respuestas normales y devuelva una respuesta (si corresponde) 4xx / 5xx para los errores. Estos también pueden llevar la carga JSON, por lo que su servidor puede agregar detalles adicionales sobre el error.

Intenté buscar en Google por las palabras clave en esta respuesta, aún tengo más preguntas que respuestas en mi cabeza.

  1. ¿Cómo decido qué código de error, 400 o 500, devolver? Quiero decir, Django tiene muchos tipos de error predefinidos, y ¿cómo puedo implementar esta asignación entre los tipos de excepción de Django y el código de error 400-500 para hacer que los bloques de manejo de excepciones sean tan SECOS y reutilizables como sea posible?
  2. ¿Puede considerarse viable el enfoque con middleware sugerido por @Reorx en la publicación ? (La respuesta solo obtuvo un voto positivo, lo que me hizo reacio a profundizar en los detalles e implementarlo en mi proyecto.
  3. Lo más importante es que, a veces, es posible que desee generar un error relacionado con la lógica de negocios, en lugar de una syntax incorrecta o algo estándar como un valor nulo. Por ejemplo, si no hay un CEO en mi entidad legal, es posible que desee prohibir al usuario que agregue un contrato. ¿Cuál debería ser el estado de error en este caso, y cómo lanzo un error con mi explicación detallada del error para el usuario?

Considerémoslo en una vista simple

def test_view (request): try: # Some code .... if my_business_logic_is_violated(): # How do I raise the error error_msg = "You violated bussiness logic because..." # How do I pass error_msg my_response = {'my_field' : value} except ExpectedError as e: # what is the most appropriate way to pass both error status and custom message # How do I list all possible error types here (instead of ExpectedError to make the exception handling block as DRY and reusable as possible return JsonResponse({'status':'false','message':message}, status=500) 

En primer lugar, debes pensar en qué errores quieres exponer:

  • Por lo general, los errores 4xx (errores que se atribuyen al lado del cliente) se revelan para que el usuario pueda corregir la solicitud.

  • Por otro lado, los errores 5xx (los errores que se atribuyen al lado del servidor) generalmente solo se presentan sin información. En mi opinión, para aquellos que deberían usar herramientas como Sentry monitorea y resuelve estos errores, que pueden tener problemas de seguridad incrustados.

Teniendo esto en cuenta, en mi opinión, para una solicitud correcta de Ajax, debe devolver un código de estado y luego algunos json para ayudar a comprender qué sucedió como un mensaje y una explicación (cuando corresponda).

Si su objective es utilizar ajax para enviar información, sugiero que configure un formulario para lo que desea. De esta manera, podrá superar fácilmente el proceso de validación. Asumiré que el caso es este en el ejemplo.

Primero – ¿Es correcta la solicitud?

 def test_view(request): message = None explanation = None status_code = 500 # First, is the request correct? if request.is_ajax() and request.method == "POST": .... else: status_code = 400 message = "The request is not valid." # You should log this error because this usually means your front end has a bug. # do you whant to explain anything? explanation = "The server could not accept your request because it was not valid. Please try again and if the error keeps happening get in contact with us." return JsonResponse({'message':message,'explanation':explanation}, status=status_code) 

Segundo – ¿Hay errores en el formulario?

 form = TestForm(request.POST) if form.is_valid(): ... else: message = "The form has errors" explanation = form.errors.as_data() # Also incorrect request but this time the only flag for you should be that maybe JavaScript validation can be used. status_code = 400 

Incluso puede obtener un error campo por campo para que pueda presentarse mejor en el formulario.

Tercero – Vamos a procesar la solicitud

  try: test_method(form.cleaned_data) except `PermissionError` as e: status_code= 403 message= "Your account doesn't have permissions to go so far!" except `Conflict` as e: status_code= 409 message= "Other user is working in the same information, he got there first" .... else: status_code= 201 message= "Object created with success!" 

Dependiendo de las excepciones que defina, pueden requerirse diferentes códigos. Ir a Wikipedia y consultar la lista. No olvides que la respuesta también varía en el código. Si agrega algo a la base de datos, debe devolver un 201 . Si acaba de obtener información, entonces estaba buscando una solicitud GET.

Respondiendo a las preguntas

  1. Las excepciones de Django devolverán 500 errores si no se tratan, porque si no sabe que va a ocurrir una excepción, entonces es un error en el servidor. Con la excepción de 404 y los requisitos de inicio de sesión, try catch bloques para todo. (Para 404, puede subirlo y si lo hace @login_required o un permiso requerido, django responderá con el código apropiado sin que usted haga nada).

  2. No estoy completamente de acuerdo con el enfoque. Como usted dijo, los errores deben ser explícitos, por lo que siempre debe saber qué se supone que debe suceder y cómo explicarlo, y hacerlo depender de la operación realizada.

  3. Yo diría que un error 400 está bien para eso. Es una mala solicitud, solo necesita explicar por qué, el código de error es para usted y para su código js, ​​así que sea consistente.

  4. (ejemplo proporcionado) – En la vista de text_view , debe tener el test_method como en el tercer ejemplo.

El método de prueba debe tener la siguiente estructura:

 def test_method(validated_data): try: my_business_logic_is_violated(): catch BusinessLogicViolation: raise else: ... #your code 

El en mi ejemplo:

  try: test_method(form.cleaned_data) except `BusinessLogicViolation` as e: status_code= 400 message= "You violated the business logic" explanation = e.explanation ... 

Consideré que la violación de la lógica de negocios era un error del cliente, porque si se necesita algo antes de esa solicitud, el cliente debería tenerlo en cuenta y pedirle al usuario que lo haga primero. (De la definición de error ):

El código de estado 400 (Solicitud incorrecta) indica que el servidor no puede o no procesará la solicitud debido a algo que se percibe como un error del cliente (por ejemplo, syntax de la solicitud mal formada, solicitud no válida)
encuadre del mensaje, o enrutamiento de solicitud engañosa).

Por cierto, puedes ver los documentos de Python en excepciones definidas por el usuario para que puedas dar los mensajes de error apropiados. La idea detrás de este ejemplo es que genera una excepción BusinessLogicViolation con un mensaje diferente en my_business_logic_is_violated() acuerdo con el lugar donde se generó.

Los códigos de estado están muy bien definidos en el estándar HTTP. Puedes encontrar una lista muy legible en Wikipedia . Básicamente, los errores en el rango 4XX son errores cometidos por el cliente, es decir, si solicitan un recurso que no existe, etc. Los errores en el rango 5XX deben devolverse si se encuentra un error en el lado del servidor.

Con respecto al punto número 3, debe elegir un error 4XX para el caso en el que no se cumpla una condición previa, por ejemplo 428 Precondition Required previo, pero devolver un error 5XX cuando un servidor genere un error de syntax.

Uno de los problemas con su ejemplo es que no se devuelve ninguna respuesta a menos que el servidor genere una excepción específica, es decir, cuando el código se ejecuta normalmente y no se genera ninguna excepción, ni el mensaje ni el código de estado se envían explícitamente al cliente. Esto se puede solucionar a través de un bloque finally, para que esa parte del código sea lo más genérica posible.

Según su ejemplo:

 def test_view (request): try: # Some code .... status = 200 msg = 'Everything is ok.' if my_business_logic_is_violated(): # Here we're handling client side errors, and hence we return # status codes in the 4XX range status = 428 msg = 'You violated bussiness logic because a precondition was not met'. except SomeException as e: # Here, we assume that exceptions raised are because of server # errors and hence we return status codes in the 5XX range status = 500 msg = 'Server error, yo' finally: # Here we return the response to the client, regardless of whether # it was created in the try or the except block return JsonResponse({'message': msg}, status=status) 

Sin embargo, como se indica en los comentarios, tendría más sentido hacer las dos validaciones de la misma manera, es decir, a través de excepciones, de este modo:

 def test_view (request): try: # Some code .... status = 200 msg = 'Everything is ok.' if my_business_logic_is_violated(): raise MyPreconditionException() except MyPreconditionException as e: # Here we're handling client side errors, and hence we return # status codes in the 4XX range status = 428 msg = 'Precondition not met.' except MyServerException as e: # Here, we assume that exceptions raised are because of server # errors and hence we return status codes in the 5XX range status = 500 msg = 'Server error, yo.' finally: # Here we return the response to the client, regardless of whether # it was created in the try or the except block return JsonResponse({'message': msg}, status=status)