Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature request: add exception handlers return types to OpenAPI responses #4928

Open
1 of 2 tasks
ThomasLeedsLRH opened this issue Aug 12, 2024 · 3 comments
Open
1 of 2 tasks
Assignees
Labels
feature-request feature request help wanted Could use a second pair of eyes/hands need-customer-feedback Requires more customers feedback before making or revisiting a decision revisit-in-3-months Requires more customers feedback before making or revisiting a decision

Comments

@ThomasLeedsLRH
Copy link

ThomasLeedsLRH commented Aug 12, 2024

Use case

I use an API Gateway event handler with validation enabled and I'd like exception handlers response types to be automatically add to the openAPI schema. I can add responses parameter to my endpoint definition, however it's not ideal as it adds a lot off repetition, duplication and introduce potential for incorrect openAPI schemas.

Solution/User Experience

import requests
from typing import List, Optional

from pydantic import BaseModel, Field
from aws_lambda_powertools.utilities.typing import LambdaContext
from aws_lambda_powertools.event_handler import APIGatewayRestResolver

app = APIGatewayRestResolver(enable_validation=True)


class Todo(BaseModel):
   user_id: int = Field(alias="userId")
   id: int
   title: str
   completed: bool


class Error(BaseModel):
   error: str
   detail: Optional[list[dict]]


@app.exception_handler(Exception)
def internal_server_error(error: Exception) -> Error:
   return Error(error="internal_server_error")


@app.get("/todos")
def get_todos() -> List[Todo]:
   todo = requests.get("https://jsonplaceholder.typicode.com/todos")
   todo.raise_for_status()

   return todo.json()


def lambda_handler(event: dict, context: LambdaContext) -> dict:
   return app.resolve(event, context)

This is what currently gets generated:

openapi: 3.0.3
info:
title: Powertools API
version: 1.0.0
servers:
- url: /
paths:
/todos:
   get:
     operationId: get_todos_get
     responses:
       '200':
         description: Successful Response
         content:
           application/json:
             schema:
               items:
                 $ref: '#/components/schemas/Todo'
               type: array
               title: Return
       '422':
         description: Validation Error
         content:
           application/json:
             schema:
               $ref: '#/components/schemas/HTTPValidationError'
components:
schemas:
   HTTPValidationError:
     properties:
       detail:
         items:
           $ref: '#/components/schemas/ValidationError'
         type: array
         title: Detail
     type: object
     title: HTTPValidationError
   Todo:
     properties:
       userId:
         type: integer
         title: Userid
       id:
         type: integer
         title: Id
       title:
         type: string
         title: Title
       completed:
         type: boolean
         title: Completed
     type: object
     required:
       - userId
       - id
       - title
       - completed
     title: Todo
   ValidationError:
     properties:
       loc:
         items:
           anyOf:
             - type: string
             - type: integer
         type: array
         title: Location
       type:
         type: string
         title: Error Type
     type: object
     required:
       - loc
       - msg
       - type
     title: ValidationError

ideally this would be genrated:

openapi: 3.0.3
info:
title: Powertools API
version: 1.0.0
servers:
- url: /
paths:
/todos:
   get:
     operationId: get_todos_get
     responses:
       '200':
         description: Successful Response
         content:
           application/json:
             schema:
               items:
                 $ref: '#/components/schemas/Todo'
               type: array
               title: Return
       '422':
         description: Validation Error
         content:
           application/json:
             schema:
               $ref: '#/components/schemas/HTTPValidationError'
       '500':
         description: Internal Server Error
         content:
           application/json:
             schema:
               $ref: '#/components/schemas/Error'
components:
schemas:
   HTTPValidationError:
     properties:
       detail:
         items:
           $ref: '#/components/schemas/ValidationError'
         type: array
         title: Detail
     type: object
     title: HTTPValidationError
   Todo:
     properties:
       userId:
         type: integer
         title: Userid
       id:
         type: integer
         title: Id
       title:
         type: string
         title: Title
       completed:
         type: boolean
         title: Completed
     type: object
     required:
       - userId
       - id
       - title
       - completed
     title: Todo
   ValidationError:
     properties:
       loc:
         items:
           anyOf:
             - type: string
             - type: integer
         type: array
         title: Location
       type:
         type: string
         title: Error Type
     type: object
     required:
       - loc
       - msg
       - type
     title: ValidationError
   Error:
     properties:
       error:
         type: string
         title: Error
       detail:
         type: array
         items:
           type: object
         title: Detail
     type: object
     required:
       - error

This would require a status code to be attached to each exception_handler and potentially some other context.

Alternative solutions

No response

Acknowledgment

@ThomasLeedsLRH ThomasLeedsLRH added feature-request feature request triage Pending triage from maintainers labels Aug 12, 2024
Copy link

boring-cyborg bot commented Aug 12, 2024

Thanks for opening your first issue here! We'll come back to you as soon as we can.
In the meantime, check out the #python channel on our Powertools for AWS Lambda Discord: Invite link

@leandrodamascena
Copy link
Contributor

Hi @ThomasLeedsLRH! Thanks for opening this issue and bringing this suggestion.

I don't think we should include exception_handler in the OpenAPI schema. The exception_handler feature is intended to catch general/custom Python exceptions, not define the responses in the OpenAPI schema. I realize there may be some overlap between these features, and the code may get repetitive if you define both, but if we assume we always should include exception_handler in the OpenAPI schema, we may be defining unwanted behavior for some routes that want to treat exceptions in a different way.

I will leave this issue open to hear more from you and other customers.

@leandrodamascena leandrodamascena added help wanted Could use a second pair of eyes/hands need-customer-feedback Requires more customers feedback before making or revisiting a decision and removed triage Pending triage from maintainers labels Aug 12, 2024
@leandrodamascena leandrodamascena self-assigned this Aug 12, 2024
@leandrodamascena leandrodamascena added the revisit-in-3-months Requires more customers feedback before making or revisiting a decision label Aug 15, 2024
@Hatter1337
Copy link

Hi @leandrodamascena,
my issue is quite similar to this one, so I decided to comment here instead of opening a new issue.

The problem is that the "Validation Error" response is always included in the OpenAPI schema (primarily due to this code), which can lead to incorrect documentation generation when using @app.exception_handler(RequestValidationError).

In my case, I want to return validation errors with a 400 status code and in a different format, like this:

{
    "error": {
        "statusCode": 400,
        "message": "Invalid request parameters",
        "description": {
            "body.user_email": "Field required"
        }
    }
}

To achieve this, I am using a custom exception handler and a custom class:

def validation_error_description(exc: RequestValidationError):
    """
    Extracts and formats validation error messages from a RequestValidationError.
    It creates a dictionary where each key represents the location of the validation error
    in the request (e.g., "body.email"), and the corresponding value is the error message.

    Args:
        exc (RequestValidationError): The exception raised during request validation.

    Returns:
        dict: A dictionary containing detailed descriptions of each validation error.

    """
    error_description = {}

    for error in exc.errors():
        # Creating a string representation of the location (path) of each error in the request
        field = ".".join([str(elem) for elem in error["loc"]])
        # Mapping the error location to its message
        error_description[field] = error["msg"]

    return error_description
    

class ExceptionHandlers:
    """
    A class to handle common exceptions for AWS Lambda functions using AWS Lambda Powertools.

    Attributes:
        app (LambdaPowertoolsApp): An instance of the LambdaPowertoolsApp.
        logger (Logger): An instance of the Powertools Logger.

    """

    def __init__(self, app, logger=None):
        self.app = app
        self.logger = logger or Logger()

    # 400 Bad Request
    def invalid_params(self, exc):
        """
        Handles RequestValidationError exceptions
            by logging the error and returning a custom Response.

        Args:
            exc (RequestValidationError): The exception object.

        Returns:
            Response: A custom response with a status code of 400, indicating a bad request.

        """
        if exc.__class__.__name__ == "TypeError" and "JSON object must be" in str(exc):
            error_description = {"body": "Invalid or empty request body"}
        else:
            if isinstance(exc, RequestValidationError):
                error_description = validation_error_description(exc)
            else:
                error_description = str(exc)

            self.logger.error(
                f"Data validation error: {error_description}",
                extra={
                    "path": self.app.current_event.path,
                    "query_strings": self.app.current_event.query_string_parameters,
                },
            )

        return Response(
            status_code=HTTPStatus.BAD_REQUEST.value,
            content_type=content_types.APPLICATION_JSON,
            body={
                "error": {
                    "statusCode": HTTPStatus.BAD_REQUEST.value,
                    "message": "Invalid request parameters",
                    "description": error_description,
                }
            },
        )

...
exception_handlers = ExceptionHandlers(app=app) 
...
@app.exception_handler([RequestValidationError, ValidationError, ValidationException])
def handle_invalid_params_wrapper(exc):
    return exception_handlers.invalid_params(exc)

However, when I generate the OpenAPI schema, I have to manually add the 400 response to each of my routers:

@router.get(
    "/user/<id>",
    summary="Get user data",
    responses={
        200: {...},
        400: {
            "description": "Bad request",
            "content": {"application/json": {"model": BadRequestResponse}},
      },
    ...

But I also end up with a 422 response in my OpenAPI schema, which shouldn't be there:
Screenshot 2024-09-10 at 14 42 34


Ideally, I would like to be able to reuse response models from exception_handler without having to define them for each router.

At the very least, the 422 response should not be added to the OpenAPI, as it can be overridden.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
feature-request feature request help wanted Could use a second pair of eyes/hands need-customer-feedback Requires more customers feedback before making or revisiting a decision revisit-in-3-months Requires more customers feedback before making or revisiting a decision
Projects
Development

No branches or pull requests

3 participants