Exception Handling - NFSandbox/sh_trade_backend GitHub Wiki

Basic Conception

All custom error class and general custom error handling logic should be written inside exception directory.

We use FastAPI Error Handler feature to handle all custom error derived from BaseError. So it's strongly recommended to create a new subclass when a custom error is needed.

Unified the format of custom error is also of benifit for frontend developing, which allow us to handle error raised by API using a general error handler.

About BaseClass

Structure

There are three members:

  • name The display name of this error, may be shown at frontend.
  • message The brief description of this error, may be shown at frontend.
  • status Used by the FastAPI Custom Error Handler to determine the response HTTP Status Code.

Keep in mind that API response does NOT contain any class hierarchy info, the name will be the unique identifier of different errors.

Naming Convention

  • name Should only contain lowercase alphabets and _. E.g.: auth_error, token_expired.

Just for beauty.

  • One name could only be used to represent one error. Don't let different errors have same name, even if they are in same category.

For example token error could have several different subcategory, for example the token is invalid or token is expired. In this case, do NOT using something like token_error to represent all token error. Instead, using token_expired, invalid_token etc to represent each sub error.

Pydantic Support

The BaseError class is a sub-class of Python Exception, which means it's competible with Python error handling process.

However in order to make our custom error system works with FastAPI, we need a Pydantic version of our error class, which is BaseErrorOut.

But anyway when throwing error or creating subclasses, we don't need to consider this part of mechanism, and we only need to create new class from BaseError.

Validate Errors In API Returns

In one word, always use detail.name to validate a certain type of error when requesting API endpoints.

Although our custom BaseError and BaseErrorOut provide a custom HTTP Status Code options, never validate error based on HTTP Status Code in production environments. There are several reasons of this rules, one is that when using CDN services (e.g. Cloudflare) to proxy the API endpoints, some of HTTP Status Code may be intercepted and redirect to a custom page. For example the 404 Not Found error will generally be intercepted by Cloudflare and redirect to a 502 Bad Gateway error page, which will cause error when you try to validate some error using 404 status code.

Creating Custom Error Sub-class

Generally, the only thing we should override in subclass of BaseError is the __init__() function. In which:

  • Deal with initialize logic of this kind of error. Determine name, message and status based on the logic.
  • Call super().__init__(name=..., message=..., status=...)

Recommend checking out TokenError subclass for more info.

Code Snippet
class TokenError(BaseError):
    """
    Raise when error occurred while verifying token.

    Check out __init__() for more info.
    """

    def __init__(
            self,
            message: str | None = None,
            expired: bool | None = None,
            role_not_match: bool | None = None,
            no_token: bool | None = None,
    ) -> None:
        final_name = 'token_error'
        final_message = message
        """
        Create an `TokenError` instance.
        :param message:
        :param expired: If `true`, indicates the token is expired.
        :param role_not_match: If `true`, indicates the role are not match the requirements.
        """
        if message is None:
            message = 'Could not verify the user tokens'

        if expired:
            final_name = 'token_expired'
            message = 'Token expired, try login again to get a new token'

        if role_not_match:
            final_name = 'token_role_not_match'
            message = 'Current role are not match the requirements to perform this operation or access this resources'

        if no_token:
            final_name = 'token_required'
            message = 'Could not found a valid token, try login to an valid account'

        # only when message is None, then use presets, otherwise always use the original message passed.
        if final_message is None:
            final_message = message

        super().__init__(
            name=final_name,
            message=final_message,
            status=401
        )

As you see, although we say that we need to use different name for every sub-category error, we can still use a same class to deal with errors in same category based on the actual requirements.

Validate Error When Testing

This project using APIDog as third party testing tools. When using APIDog to validate the exception returned by FastAPI endpoints, you could make use of PostProcessor module in APIDogs to check the return body of the Response.

As the BaseErrorOut Pydantic model indicates, all custom error in this project will be converted to the following JSON schema:

{
	"detail": {
		"name": "error_name",
		"message": "A brief description of this error."
	}
}

Then you could use PostProcessor in APIDogs to validate a specific exception by making assertion to $.detail.name of the Response Body:

Using PostProcessor to validate exceptions

The example image above is showing how to validate an identical_seller_buyer exception returned by API server.

When using PostProcessor to validate the exception, make sure you turned off "Validate Response":

Turn off ValidateResponse

⚠️ **GitHub.com Fallback** ⚠️