litestar-org / litestar

Production-ready, Light, Flexible and Extensible ASGI API framework | Effortlessly Build Performant APIs
https://litestar.dev/
MIT License
5.53k stars 378 forks source link

Enhancement: Allow customizing `HTTPException` content in OpenAPI spec #3020

Open tuukkamustonen opened 9 months ago

tuukkamustonen commented 9 months ago

Summary

How HTTPExceptions get into OpenAPI spec is done is in https://github.com/litestar-org/litestar/blob/14759558cf231c61049fdd5b65a39eccb8841616/litestar/_openapi/responses.py#L280-L285

The fields written (status_code, detail, extra) is hard-coded there.

Allow generating the schema from a callable, that can be customized.

Basic Example

Just to give an idea:

def generate_schema(exc: HTTPException):
    ...the logic here...

app = Litestar(exception_schema_generator=generate_schema)

Decoupling schema generation from the exception itself would work also for the builtin errors that Litestar might throw, like ValidationException.

Drawbacks and Impact

No response

Unresolved questions

No response


[!NOTE]
While we are open for sponsoring on GitHub Sponsors and OpenCollective, we also utilize Polar.sh to engage in pledge-based sponsorship.

Check out all issues funded or available for funding on our Polar.sh dashboard

  • If you would like to see an issue prioritized, make a pledge towards it!
  • We receive the pledge once the issue is completed & verified
  • This, along with engagement in the community, helps us know which features are a priority to our users.

Fund with Polar

tuukkamustonen commented 9 months ago

Thinking out loud, for the rationale here, we often add error or similar field to the responses:

{
    "status_code": 409,
    "detail": "User already has thing X",
    "error": "AFP00146"
}
{
    "status_code": 409,
    "detail": "Some other case which leads to conflict",
    "error": "AFP00147"
}

This will allow the caller to identify the error, due to the more granular error compared to 409 status code.

Two things here:

  1. Need to add that error to the OpenAPI spec (what this PR is about)
  2. Would need to somehow render this in OpenAPI GUIs

Regarding (2), when you configure:

class UserAlreadyHasThingX(HTTPException):
    status_code: Literal[409] = HTTP_409_CONFLICT
    error: Literal["AFP00146"] = "AFP00146"

class SomeOtherConflict(HTTPException):
    status_code: Literal[409] = HTTP_409_CONFLICT
    error: Literal["AFP00147"] = "AFP00147"  # or this could be `some_other_conflict`, as long as it's identifier

@get(..., raises=[UserAlreadyHasThingX, SomeOtherConflict])
def endpoint(): ...

This will generate oneOf array in the OpenAPI schema, which is nice. All GUIs know how to show them, e.g..

image

The problem is, that both schemas are similar (except their description), so there's no benefit. We'd need to generate literal/enum AFP00146/AFP00147 for the error fields, so that reader would understand that despite the similar fields (in all error responses) the values (and descriptions) for certain errors are different.


Alternatively we could use detail field to represent the error code (rather than human-readable string, what's usually put there).

However, Litestar would still need to pick those fields up when generating the schema. For example:

class UserAlreadyHasThingX(HTTPException):
    status_code: int = HTTP_409_CONFLICT
    error: Literal["user_already_has_thing_x"] = "user_already_has_thing_x"

This should rather generate something like:

image

A problem with this is that there's no room for human-readable error text now, as detail was reserved for other purposes... would still need to add a custom field.

Anyway, customizing the schema generation should make this possible.