drf-standardized-errors icon indicating copy to clipboard operation
drf-standardized-errors copied to clipboard

404 Exception not showing in OpenAPI Schema

Open MaxDev98 opened this issue 1 year ago • 1 comments

I'm implementing drf-standardized-errors in my DRF project and encountering an issue where 404 exceptions are not being included in the OpenAPI schema, despite being correctly configured and working in the actual API responses.

Environment:

  • Python 3.11
  • Django 5.0.9
  • django-rest-framework 3.15.2
  • drf-spectacular 0.27.2
  • drf-standardized-errors 0.14.1 (with [openapi] extras)

views.py:

class StocktakeScanView(APIView):

    @extend_schema(
        summary="xxxxxxxxx",
        description="""xxxxxxxxx""",
        parameters=[StocktakeScanRequestSerializer],
        responses={
            200: StocktakeScanResultSerializer,
        },
    )
    def get(self, request):
        try:
            serializer = StocktakeScanRequestSerializer(data=request.query_params)
            if not serializer.is_valid():
                raise ValidationError(serializer.errors)

            validated_data = serializer.validated_data
            try:
                device = InventoryService.get_by_search_string(validated_data["search_string"])
            except NotFound as e:
                raise DeviceNotFoundException(field="search_string", value=validated_data["search_string"])

            stocktake_result = scan_init_workflow(device, **validated_data)
            response_serializer = StocktakeScanResultSerializer(stocktake_result)
            return Response(response_serializer.data)

        except Exception as e:
            if not isinstance(e, (ValidationError, DeviceNotFoundException)):
                raise ValidationError({"error": str(e)})
            raise

exceptions.py:

from rest_framework import status
from rest_framework.exceptions import NotFound
from typing import Any

class DeviceNotFoundException(NotFound):
    default_detail = "Device not found"
    default_code = "device_not_found"
    status_code = status.HTTP_404_NOT_FOUND

    def __init__(self, field: str, value: Any):
        """
        Args:
            field: The field/attribute name that caused the error (e.g., 'search_string')
            value: The value that was not found
        """
        detail = f"Device with {field} '{value}' not found"
        super().__init__(detail={field: detail})

settings.py

REST_FRAMEWORK = {
    "DEFAULT_SCHEMA_CLASS": "drf_standardized_errors.openapi.AutoSchema",
    "DEFAULT_PAGINATION_CLASS": "rest_framework.pagination.LimitOffsetPagination",
    "PAGE_SIZE": 10,
    "DEFAULT_AUTHENTICATION_CLASSES": ("rest_framework_simplejwt.authentication.JWTAuthentication",),
    "EXCEPTION_HANDLER": "drf_standardized_errors.handler.exception_handler",
}

SPECTACULAR_SETTINGS = {
    "TITLE": "xxxxxxxxx",
    "DESCRIPTION": "xxxxxxxxx",
    "VERSION": "xxxxxxxxx",
    "SERVE_INCLUDE_SCHEMA": False,
    "SWAGGER_UI_DIST": "SIDECAR",
    "SWAGGER_UI_FAVICON_HREF": "SIDECAR",
    "REDOC_DIST": "SIDECAR",
    "ENUM_NAME_OVERRIDES": {
        "ValidationErrorEnum": "drf_standardized_errors.openapi_serializers.ValidationErrorEnum.choices",
        "ClientErrorEnum": "drf_standardized_errors.openapi_serializers.ClientErrorEnum.choices",
        "ServerErrorEnum": "drf_standardized_errors.openapi_serializers.ServerErrorEnum.choices",
        "ErrorCode401Enum": "drf_standardized_errors.openapi_serializers.ErrorCode401Enum.choices",
        "ErrorCode403Enum": "drf_standardized_errors.openapi_serializers.ErrorCode403Enum.choices",
        "ErrorCode404Enum": "drf_standardized_errors.openapi_serializers.ErrorCode404Enum.choices",
        "ErrorCode405Enum": "drf_standardized_errors.openapi_serializers.ErrorCode405Enum.choices",
        "ErrorCode406Enum": "drf_standardized_errors.openapi_serializers.ErrorCode406Enum.choices",
        "ErrorCode415Enum": "drf_standardized_errors.openapi_serializers.ErrorCode415Enum.choices",
        "ErrorCode429Enum": "drf_standardized_errors.openapi_serializers.ErrorCode429Enum.choices",
        "ErrorCode500Enum": "drf_standardized_errors.openapi_serializers.ErrorCode500Enum.choices",
    },
    "POSTPROCESSING_HOOKS": ["drf_standardized_errors.openapi_hooks.postprocess_schema_enums"],
}

Excpected Behavior: The 404 error response should be automatically included in the OpenAPI schema.

Actual Behavior: The 404 error response is missing from the OpenAPI schema, although the API correctly returns 404 responses when appropriate.

OpenAPI Schema screenshot (missing 404) Screenshot 2024-11-26 172254

Actual API response (404) Screenshot 2024-11-26 172444

Additional Context: The API correctly returns 404 responses Other error status codes (400, etc.) are showing up in the schema No custom error serializers are being used

Is there additional configuration needed to include 404 responses in the OpenAPI schema? Should I be explicitly defining the 404 response schema with this exception somewhere?

Thanks!

MaxDev98 avatar Nov 26 '24 16:11 MaxDev98

Thank you for the detailed issue report.

The short answer is yes, if you want to show a 404 in your case, you should add the 404 response to extend_schema. The longer one is below.

It seems like you misunderstood how the package generates error responses. Taking 404 as an example, the package adds the error response on specific pagination classes or versioning classes, or if the view url has path params. It does not check your view code and automatically add a 404 when you raise an exception that is a subclass of NotFound. You can read below the code that adds 404 responses (and I encourage you to read the rest of _should_add_httpxxx_error_response)

https://github.com/ghazi-git/drf-standardized-errors/blob/ce03d4a71c7ea29f59d6b021490698df59df6933/drf_standardized_errors/openapi.py#L185-L210

The package behavior is in line with what drf-spectacular does. For example, in the code you submitted, drf-spectacular was not able to figure out the response serializer by itself (because serializer_class is not set), and you had to set responses={200: StocktakeScanResultSerializer}. The same thing applies for 404, you need to add the 404 response to extend_schema responses.

from drf_standardized_errors.openapi_serializers import ErrorResponse404Serializer

class StocktakeScanView(APIView):

    @extend_schema(
        ...,
        # create a serializer similar to ErrorResponse404Serializer so that you can show the error code `device_not_found`
        responses={200: StocktakeScanResultSerializer, 404: ErrorResponse404Serializer},
        # you might want to add a 404 example as well, `get_example_from_exception` can be helpful with that
        # https://github.com/ghazi-git/drf-standardized-errors/blob/ce03d4a71c7ea29f59d6b021490698df59df6933/drf_standardized_errors/openapi_utils.py#L490
        examples=[get_example_from_exception(DeviceNotFoundException('search_string', '999999'))]
    )
    def get(self, request):
        ...

Also, you can subclass drf_standardized_errors.openapi.AutoSchema._should_add_http404_error_response to implement logic that automatically adds 404 errors based on specific conditions.

ghazi-git avatar Nov 27 '24 20:11 ghazi-git