mirror of
https://github.com/langgenius/dify.git
synced 2026-06-17 06:21:07 +08:00
feat(openapi): add canonical ErrorBody model and error-code enum
This commit is contained in:
parent
e3cfc4d40f
commit
3f53fa605e
66
api/controllers/openapi/_errors.py
Normal file
66
api/controllers/openapi/_errors.py
Normal file
@ -0,0 +1,66 @@
|
||||
"""Canonical error contract for the /openapi/v1 surface.
|
||||
|
||||
``ErrorBody`` is the only wire shape an /openapi/v1 endpoint may emit for a
|
||||
non-2xx response (RFC 8628 device-flow responses excepted — that shape is
|
||||
mandated by the OAuth spec). ``OpenApiErrorFormatter`` is injected into
|
||||
``ExternalApi`` so every error-handler path funnels through one builder, and
|
||||
it also rewrites ``e.data`` because flask-restx ``Api.handle_error`` lets a
|
||||
pre-existing ``e.data`` override the registered handler's return value.
|
||||
"""
|
||||
|
||||
from enum import StrEnum
|
||||
|
||||
from pydantic import BaseModel
|
||||
|
||||
|
||||
class OpenApiErrorCode(StrEnum):
|
||||
# transport-generic (resolved from HTTP status for plain werkzeug raises)
|
||||
BAD_REQUEST = "bad_request"
|
||||
UNAUTHORIZED = "unauthorized"
|
||||
FORBIDDEN = "forbidden"
|
||||
NOT_FOUND = "not_found"
|
||||
METHOD_NOT_ALLOWED = "method_not_allowed"
|
||||
NOT_ACCEPTABLE = "not_acceptable"
|
||||
CONFLICT = "conflict"
|
||||
REQUEST_TOO_LARGE = "request_entity_too_large"
|
||||
UNSUPPORTED_MEDIA_TYPE = "unsupported_media_type"
|
||||
INVALID_PARAM = "invalid_param"
|
||||
TOO_MANY_REQUESTS = "too_many_requests"
|
||||
INTERNAL_ERROR = "internal_server_error"
|
||||
BAD_GATEWAY = "bad_gateway"
|
||||
UNKNOWN = "unknown"
|
||||
# domain codes (carried by BaseHTTPException.error_code, values preserved
|
||||
# from the existing wire contract)
|
||||
APP_UNAVAILABLE = "app_unavailable"
|
||||
CONVERSATION_COMPLETED = "conversation_completed"
|
||||
PROVIDER_NOT_INITIALIZE = "provider_not_initialize"
|
||||
PROVIDER_QUOTA_EXCEEDED = "provider_quota_exceeded"
|
||||
MODEL_NOT_SUPPORTED = "model_currently_not_support" # legacy wire value — do not rename
|
||||
COMPLETION_REQUEST_ERROR = "completion_request_error"
|
||||
RATE_LIMIT_ERROR = "rate_limit_error"
|
||||
FILE_TOO_LARGE = "file_too_large"
|
||||
UNSUPPORTED_FILE_TYPE = "unsupported_file_type"
|
||||
NO_FILE_UPLOADED = "no_file_uploaded"
|
||||
TOO_MANY_FILES = "too_many_files"
|
||||
FILENAME_NOT_EXISTS = "filename_not_exists"
|
||||
FILE_EXTENSION_BLOCKED = "file_extension_blocked"
|
||||
MEMBER_LIMIT_EXCEEDED = "member_limit_exceeded"
|
||||
MEMBER_LICENSE_EXCEEDED = "member_license_exceeded"
|
||||
|
||||
|
||||
class ErrorDetail(BaseModel):
|
||||
type: str
|
||||
loc: list[str | int] = []
|
||||
msg: str
|
||||
|
||||
|
||||
class ErrorBody(BaseModel):
|
||||
"""Canonical non-2xx body. ``code`` is typed ``str`` (not the enum) so the
|
||||
generated client schema stays an open enum — old CLIs keep parsing when a
|
||||
future server adds a code. Formatter tests pin emitted values to the enum."""
|
||||
|
||||
code: str
|
||||
message: str
|
||||
status: int
|
||||
hint: str | None = None
|
||||
details: list[ErrorDetail] | None = None
|
||||
@ -0,0 +1,33 @@
|
||||
"""Wire-contract tests for the canonical /openapi/v1 error body."""
|
||||
|
||||
from controllers.openapi._errors import ErrorBody, ErrorDetail, OpenApiErrorCode
|
||||
|
||||
|
||||
class TestErrorBodyModel:
|
||||
def test_minimal_body_serializes_without_optional_fields(self):
|
||||
body = ErrorBody(code=OpenApiErrorCode.NOT_FOUND, message="app not found", status=404)
|
||||
|
||||
wire = body.model_dump(mode="json", exclude_none=True)
|
||||
|
||||
assert wire == {"code": "not_found", "message": "app not found", "status": 404}
|
||||
|
||||
def test_full_body_round_trips(self):
|
||||
body = ErrorBody(
|
||||
code=OpenApiErrorCode.INVALID_PARAM,
|
||||
message="Request validation failed",
|
||||
status=422,
|
||||
hint="check the request payload",
|
||||
details=[ErrorDetail(type="int_parsing", loc=["page"], msg="must be >= 1")],
|
||||
)
|
||||
|
||||
wire = body.model_dump(mode="json", exclude_none=True)
|
||||
|
||||
assert wire["details"] == [{"type": "int_parsing", "loc": ["page"], "msg": "must be >= 1"}]
|
||||
assert ErrorBody.model_validate(wire) == body
|
||||
|
||||
def test_code_field_is_open_string_for_forward_compat(self):
|
||||
# Old CLIs must not hard-fail when a future server adds a code, so the
|
||||
# schema type is str; enum membership is enforced by the formatter tests.
|
||||
body = ErrorBody.model_validate({"code": "some_future_code", "message": "x", "status": 400})
|
||||
|
||||
assert body.code == "some_future_code"
|
||||
Loading…
Reference in New Issue
Block a user