adapting exception handling and error responses for 400 responses. Using a simplified format, which only uses the keys 'error_field' and 'error_description'

This commit is contained in:
Max Metz 2024-09-10 17:37:08 +02:00
parent 590df30fef
commit 5b68ef95cb
2 changed files with 49 additions and 50 deletions

View File

@ -10,6 +10,7 @@ from ..services.auth_guard import check_jwt
from BreCal.database.update_database import evaluate_shipcall_state from BreCal.database.update_database import evaluate_shipcall_state
from BreCal.database.sql_queries import create_sql_query_shipcall_get, create_sql_query_shipcall_post, create_sql_query_shipcall_put, create_sql_query_history_post, create_sql_query_history_put, SQLQuery from BreCal.database.sql_queries import create_sql_query_shipcall_get, create_sql_query_shipcall_post, create_sql_query_shipcall_put, create_sql_query_history_post, create_sql_query_history_put, SQLQuery
from marshmallow import Schema, fields, ValidationError from marshmallow import Schema, fields, ValidationError
from BreCal.validators.validation_error import create_validation_error_response
def GetShipcalls(options): def GetShipcalls(options):
""" """
@ -152,9 +153,7 @@ def PostShipcalls(schemaModel):
return json.dumps({"id" : new_id}), 201, {'Content-Type': 'application/json; charset=utf-8'} return json.dumps({"id" : new_id}), 201, {'Content-Type': 'application/json; charset=utf-8'}
except ValidationError as ex: except ValidationError as ex:
logging.error(ex) return create_validation_error_response(ex, status_code=400, create_log=True)
print(ex)
return json.dumps({"message":f"bad format. \nError Messages: {ex.messages}. \nValid Data: {ex.valid_data}"}), 400
except Exception as ex: except Exception as ex:
logging.error(traceback.format_exc()) logging.error(traceback.format_exc())
@ -265,9 +264,7 @@ def PutShipcalls(schemaModel):
return json.dumps({"id" : schemaModel["id"]}), 200 return json.dumps({"id" : schemaModel["id"]}), 200
except ValidationError as ex: except ValidationError as ex:
logging.error(ex) return create_validation_error_response(ex, status_code=400, create_log=True)
print(ex)
return json.dumps({"message":f"bad format. \nError Messages: {ex.messages}. \nValid Data: {ex.valid_data}"}), 400
except Exception as ex: except Exception as ex:
logging.error(traceback.format_exc()) logging.error(traceback.format_exc())

View File

@ -1,58 +1,59 @@
import logging import logging
import typing import typing
import json import json
import sys
from marshmallow import ValidationError from marshmallow import ValidationError
import werkzeug
from werkzeug.exceptions import Forbidden from werkzeug.exceptions import Forbidden
def create_default_error_format(error_description): def create_default_json_response_format(error_field:str, error_description:str):
"""
The default format of a JSON response for exceptions is:
{
"message":str,
"errors":typing.Optional[
list[dict[str,list[str]]]
]
}
"""
json_response = { json_response = {
"message":f"{error_description}", "error_field":error_field,
"errors":[], "error_description":error_description
"valid_data":{}
} }
return json_response return json_response
def unbundle_(errors, unbundled=[]):
"""
unbundles a dictionary entry into separate items and appends them to the list {unbundled}.
Example:
errors = {"key1":{"keya":"keyb","keyc":{"keyc12":12}}}
Returns:
[{'error_field':'keya', 'error_description':['keyb']}, {'error_field':'keyc12', 'error_description':[12]}]
As can be seen, only the subkeys and their respective value are received. Each value is *always* a list.
"""
{k:unbundle_(v,unbundled=unbundled) if isinstance(v,dict) else unbundled.append({"error_field":k, "error_description":v[0] if isinstance(v,list) else str(v)}) for k,v in errors.items()}
return
def unbundle_validation_error_message(message):
"""
inputs:
message: ValidationError.messages object. A str, list or dictionary
"""
unbundled = []
unbundle_(message, unbundled=unbundled)
if len(unbundled)>0:
error_field = "ValidationError in the following field(s): " + " & ".join([unb["error_field"] for unb in unbundled])
error_description = "Error Description(s): " + " & ".join([unb["error_description"] for unb in unbundled])
else:
error_field = "ValidationError"
error_description = "unknown validation error"
return (error_field, error_description)
def create_validation_error_response(ex:ValidationError, status_code:int=400, create_log:bool=True)->typing.Tuple[str,int]: def create_validation_error_response(ex:ValidationError, status_code:int=400, create_log:bool=True)->typing.Tuple[str,int]:
# generate an overview the errors # unbundles ValidationError into a dictionary of {'error_field':str, 'error_description':str}-format
#example: message = ex.messages
# {'lock_time': ['The provided value must be in the future. Current Time: 2024-09-02 08:23:32.600791, Value: 2024-09-01 08:20:41.853000']} (error_field, error_description) = unbundle_validation_error_message(message)
# when the model schema returns an error, 'messages' is by default a dictionary. json_response = create_default_json_response_format(error_field=error_field, error_description=error_description)
# e.g., loadedModel = model.TimesSchema().load(data=content, many=False, partial=True)
# returns: {'eta_berth': ['The provided value must be in the future}
# when raising a custom ValidationError, it can return a string, list or dict.
# we would like to ensure, that the content of the .messages is a dictionary. This can be accomplished by calling
# raise ValidationError({"example_key_which_fails":"the respective error message"})
errors = ex.messages
# raise ValidationError("example error")
# creates a .messages object, which is an array. e.g., ex.messages = ["example error"]
# the following conversion snipped ensures a dictionary output
if isinstance(errors, (str,list)):
errors = {"undefined_schema":errors}
errors = {k:v if isinstance(v,list) else [v] for k,v in errors.items()}
# hence, errors always has the following type: list[dict[str, list[str]]]
errors = [errors]
# example:
# "Valid Data": {
# "id": 2894,
# "eta_berth": "datetime.datetime(2024, 9, 2, 11, 11, 43)",
# "eta_berth_fixed": false
# }
valid_data = ex.valid_data
message = "ValidationError"
json_response = create_default_error_format(error_description=message)
json_response.update({
"errors":errors,
"valid_data":valid_data
})
# json.dumps with default=str automatically converts non-serializable values to strings. Hence, datetime objects (which are not) # json.dumps with default=str automatically converts non-serializable values to strings. Hence, datetime objects (which are not)
# natively serializable are properly serialized. # natively serializable are properly serialized.
@ -67,7 +68,7 @@ def create_werkzeug_error_response(ex:Forbidden, status_code:int=403, create_log
# json.dumps with default=str automatically converts non-serializable values to strings. Hence, datetime objects (which are not) # json.dumps with default=str automatically converts non-serializable values to strings. Hence, datetime objects (which are not)
# natively serializable are properly serialized. # natively serializable are properly serialized.
message = ex.description message = ex.description
json_response = create_default_error_format(error_description=message) json_response = create_default_json_response_format(error_field=str(repr(ex)), error_description=message)
serialized_response = json.dumps(json_response, default=str) serialized_response = json.dumps(json_response, default=str)
if create_log: if create_log:
@ -77,7 +78,8 @@ def create_werkzeug_error_response(ex:Forbidden, status_code:int=403, create_log
def create_dynamic_exception_response(ex, status_code:int=400, message:typing.Optional[str]=None, create_log:bool=True): def create_dynamic_exception_response(ex, status_code:int=400, message:typing.Optional[str]=None, create_log:bool=True):
message = repr(ex) if message is None else message message = repr(ex) if message is None else message
json_response = create_default_error_format(error_description=message) json_response = create_default_json_response_format(error_field="Exception", error_description=message)
json_response["message"] = "call failed"
serialized_response = json.dumps(json_response, default=str) serialized_response = json.dumps(json_response, default=str)