Browse Source

Add RFC 9457 Problem Details support (opt-in via problem_details=True)

pull/15951/head
bigmikecreates 2 weeks ago
parent
commit
bba3933baa
  1. 2
      fastapi/__init__.py
  2. 79
      fastapi/applications.py
  3. 56
      fastapi/exception_handlers.py
  4. 17
      fastapi/exceptions.py
  5. 38
      fastapi/openapi/utils.py
  6. 113
      fastapi/problem_details.py
  7. 4
      fastapi/responses.py
  8. 510
      tests/test_problem_details.py

2
fastapi/__init__.py

@ -18,7 +18,9 @@ from .param_functions import Header as Header
from .param_functions import Path as Path
from .param_functions import Query as Query
from .param_functions import Security as Security
from .problem_details import ProblemDetails as ProblemDetails
from .requests import Request as Request
from .responses import ProblemDetailsResponse as ProblemDetailsResponse
from .responses import Response as Response
from .routing import APIRouter as APIRouter
from .websockets import WebSocket as WebSocket

79
fastapi/applications.py

@ -8,6 +8,8 @@ from fastapi import routing
from fastapi.datastructures import Default, DefaultPlaceholder
from fastapi.exception_handlers import (
http_exception_handler,
problem_details_http_exception_handler,
problem_details_request_validation_exception_handler,
request_validation_exception_handler,
websocket_request_validation_exception_handler,
)
@ -860,6 +862,67 @@ class FastAPI(Starlette):
"""
),
] = True,
problem_details: Annotated[
bool,
Doc(
"""
Enable RFC 9457 Problem Details for HTTP API error responses.
When `True`, the default exception handlers for `HTTPException`
and `RequestValidationError` return responses with
`Content-Type: application/problem+json` and the standard
Problem Details body (`type`, `title`, `status`, `detail`,
`instance`, and extension members). Read more about it in the
[RFC 9457](https://www.rfc-editor.org/rfc/rfc9457).
When `False` (default), the traditional `{"detail": ...}` format
is used.
"""
),
] = False,
problem_type_base_uri: Annotated[
str | None,
Doc(
"""
A base URI for problem type identifiers when using RFC 9457
Problem Details.
When set, it is combined with a path segment derived from the
error to form the `type` URI. For example,
`"https://example.com/errors"` and a 404 error produces
`"https://example.com/errors/not-found"`.
The resolution hierarchy is:
1. **Per-exception override**: if `type` is passed to
`HTTPException()`, it is used directly (absolute URIs as-is;
relative paths combined with this base).
2. **`problem_types` mapping**: a status code lookup in
`problem_types` overrides auto-derivation.
3. **Auto-derivation**: the HTTP reason phrase is slugified
(e.g., "Not Found" `not-found`).
4. If none of the above apply, `"about:blank"` is used.
Only meaningful when `problem_details=True`. Ignored otherwise.
"""
),
] = None,
problem_types: Annotated[
dict[int | type, str] | None,
Doc(
"""
A mapping of status codes to problem type path segments when
using RFC 9457 Problem Details.
For example, `{404: "order-not-found", 422: "bad-input"}`
produces `type` URIs like
`"https://example.com/errors/order-not-found"` when combined
with a `problem_type_base_uri`.
Only meaningful when `problem_details=True`. Ignored otherwise.
"""
),
] = None,
**extra: Annotated[
Any,
Doc(
@ -890,6 +953,9 @@ class FastAPI(Starlette):
self.separate_input_output_schemas = separate_input_output_schemas
self.openapi_external_docs = openapi_external_docs
self.extra = extra
self.problem_details = problem_details
self.problem_type_base_uri = problem_type_base_uri
self.problem_types = problem_types
self.openapi_version: Annotated[
str,
Doc(
@ -1000,9 +1066,17 @@ class FastAPI(Starlette):
self.exception_handlers: dict[
Any, Callable[[Request, Any], Response | Awaitable[Response]]
] = {} if exception_handlers is None else dict(exception_handlers)
self.exception_handlers.setdefault(HTTPException, http_exception_handler)
self.exception_handlers.setdefault(
RequestValidationError, request_validation_exception_handler
HTTPException,
problem_details_http_exception_handler
if problem_details
else http_exception_handler,
)
self.exception_handlers.setdefault(
RequestValidationError,
problem_details_request_validation_exception_handler
if problem_details
else request_validation_exception_handler,
)
# Starlette still has incorrect type specification for the handlers
@ -1098,6 +1172,7 @@ class FastAPI(Starlette):
servers=self.servers,
separate_input_output_schemas=self.separate_input_output_schemas,
external_docs=self.openapi_external_docs,
problem_details=self.problem_details,
)
self._openapi_routes_version = routes_version
return self.openapi_schema

56
fastapi/exception_handlers.py

@ -1,5 +1,12 @@
from collections.abc import Mapping
from fastapi.encoders import jsonable_encoder
from fastapi.exceptions import RequestValidationError, WebSocketRequestValidationError
from fastapi.problem_details import (
problem_details_from_http_exception,
problem_details_from_request_validation,
)
from fastapi.responses import ProblemDetailsResponse
from fastapi.utils import is_body_allowed_for_status_code
from fastapi.websockets import WebSocket
from starlette.exceptions import HTTPException
@ -32,3 +39,52 @@ async def websocket_request_validation_exception_handler(
await websocket.close(
code=WS_1008_POLICY_VIOLATION, reason=jsonable_encoder(exc.errors())
)
def _get_problem_details_config(
request: Request,
) -> tuple[str | None, Mapping[int | type, str] | None]:
try:
app = request.app
except (AttributeError, KeyError):
return None, None
return (
getattr(app, "problem_type_base_uri", None),
getattr(app, "problem_types", None),
)
async def problem_details_http_exception_handler(
request: Request, exc: HTTPException
) -> Response:
headers = getattr(exc, "headers", None)
if not is_body_allowed_for_status_code(exc.status_code):
return Response(status_code=exc.status_code, headers=headers)
type_base_uri, problem_types = _get_problem_details_config(request)
problem = problem_details_from_http_exception(
exc,
url=str(request.url),
type_base_uri=type_base_uri,
problem_types=problem_types,
)
return ProblemDetailsResponse(
content=problem.model_dump(exclude_none=True),
status_code=exc.status_code,
headers=headers,
)
async def problem_details_request_validation_exception_handler(
request: Request, exc: RequestValidationError
) -> ProblemDetailsResponse:
type_base_uri, problem_types = _get_problem_details_config(request)
problem = problem_details_from_request_validation(
exc.errors(),
url=str(request.url),
type_base_uri=type_base_uri,
problem_types=problem_types,
)
return ProblemDetailsResponse(
content=problem.model_dump(exclude_none=True),
status_code=422,
)

17
fastapi/exceptions.py

@ -79,8 +79,25 @@ class HTTPException(StarletteHTTPException):
"""
),
] = None,
type: Annotated[
str | None,
Doc(
"""
An optional URI reference identifying the problem type, as defined in
RFC 9457.
When set, this overrides the automatic problem type resolution in
the RFC 9457 error response (e.g., from `problem_type_base_uri`
or `problem_types`).
Read more about it in the
[RFC 9457 - Problem Details for HTTP APIs](https://www.rfc-editor.org/rfc/rfc9457).
"""
),
] = None,
) -> None:
super().__init__(status_code=status_code, detail=detail, headers=headers)
self.type: str | None = type
class WebSocketException(StarletteWebSocketException):

38
fastapi/openapi/utils.py

@ -68,6 +68,23 @@ validation_error_response_definition = {
},
}
problem_details_definition = {
"title": "ProblemDetails",
"type": "object",
"properties": {
"type": {
"title": "Type",
"type": "string",
"default": "about:blank",
},
"title": {"title": "Title", "type": "string"},
"status": {"title": "Status", "type": "integer"},
"detail": {"title": "Detail", "type": "string"},
"instance": {"title": "Instance", "type": "string"},
},
"additionalProperties": True,
}
status_code_ranges: dict[str, str] = {
"1XX": "Information",
"2XX": "Success",
@ -266,6 +283,7 @@ def get_openapi_path(
tuple[ModelField, Literal["validation", "serialization"]], dict[str, Any]
],
separate_input_output_schemas: bool = True,
problem_details: bool = False,
) -> tuple[dict[str, Any], dict[str, Any], dict[str, Any]]:
path = {}
security_schemes: dict[str, Any] = {}
@ -334,6 +352,7 @@ def get_openapi_path(
model_name_map=model_name_map,
field_mapping=field_mapping,
separate_input_output_schemas=separate_input_output_schemas,
problem_details=problem_details,
)
callbacks[callback.name] = {callback.path: cb_path}
operation["callbacks"] = callbacks
@ -457,6 +476,22 @@ def get_openapi_path(
status in operation["responses"]
for status in [http422, "4XX", "default"]
):
if problem_details:
operation["responses"][http422] = {
"description": "Validation Error",
"content": {
"application/problem+json": {
"schema": {"$ref": REF_PREFIX + "ProblemDetails"}
}
},
}
if "ProblemDetails" not in definitions:
definitions.update(
{
"ProblemDetails": problem_details_definition,
}
)
else:
operation["responses"][http422] = {
"description": "Validation Error",
"content": {
@ -536,6 +571,7 @@ def get_openapi(
license_info: dict[str, str | Any] | None = None,
separate_input_output_schemas: bool = True,
external_docs: dict[str, Any] | None = None,
problem_details: bool = False,
) -> dict[str, Any]:
info: dict[str, Any] = {"title": title, "version": version}
if summary:
@ -572,6 +608,7 @@ def get_openapi(
model_name_map=model_name_map,
field_mapping=field_mapping,
separate_input_output_schemas=separate_input_output_schemas,
problem_details=problem_details,
)
if result:
path, security_schemes, path_definitions = result
@ -592,6 +629,7 @@ def get_openapi(
model_name_map=model_name_map,
field_mapping=field_mapping,
separate_input_output_schemas=separate_input_output_schemas,
problem_details=problem_details,
)
if result:
path, security_schemes, path_definitions = result

113
fastapi/problem_details.py

@ -0,0 +1,113 @@
"""RFC 9457 Problem Details for HTTP APIs."""
import http.client
from collections.abc import Mapping, Sequence
from typing import Any
from fastapi.encoders import jsonable_encoder
from pydantic import BaseModel, ConfigDict
class ProblemDetails(BaseModel):
type: str = "about:blank"
title: str | None = None
status: int | None = None
detail: str | None = None
instance: str | None = None
model_config = ConfigDict(extra="allow")
def _slugify(text: str) -> str:
return text.lower().replace(" ", "-").replace("'", "")
def resolve_problem_type(
*,
explicit_type: str | None = None,
status_code: int | None = None,
error_kind: str | None = None,
type_base_uri: str | None = None,
problem_types: Mapping[int | type, str] | None = None,
) -> str:
segment: str | None = None
if explicit_type:
segment = explicit_type
elif problem_types and status_code is not None:
segment = problem_types.get(status_code)
if segment is not None:
if segment.startswith(("http://", "https://", "about:", "tag:", "urn:")):
return segment
if type_base_uri:
return f"{type_base_uri.rstrip('/')}/{segment.lstrip('/')}"
return segment
if error_kind:
if type_base_uri:
return f"{type_base_uri.rstrip('/')}/{error_kind}"
return error_kind
if type_base_uri and status_code is not None:
reason = http.client.responses.get(status_code)
if reason:
segment = _slugify(reason)
return f"{type_base_uri.rstrip('/')}/{segment}"
return "about:blank"
def problem_details_from_http_exception(
exc: Any,
*,
url: str | None = None,
type_base_uri: str | None = None,
problem_types: Mapping[int | type, str] | None = None,
) -> ProblemDetails:
status_code: int = exc.status_code
detail: Any = getattr(exc, "detail", None)
detail_str = detail if isinstance(detail, str) else None
kwargs: dict[str, Any] = {}
if not isinstance(detail, str) and detail is not None:
kwargs["errors"] = jsonable_encoder(detail)
problem_type = resolve_problem_type(
explicit_type=getattr(exc, "type", None),
status_code=status_code,
type_base_uri=type_base_uri,
problem_types=problem_types,
)
return ProblemDetails(
type=problem_type,
title=http.client.responses.get(status_code),
status=status_code,
detail=detail_str,
instance=url,
**kwargs,
)
def problem_details_from_request_validation(
errors: Sequence[Any],
*,
url: str | None = None,
type_base_uri: str | None = None,
problem_types: Mapping[int | type, str] | None = None,
) -> ProblemDetails:
errors_list = list(errors)
num_errors = len(errors_list)
problem_type = resolve_problem_type(
status_code=422,
error_kind="validation-error",
type_base_uri=type_base_uri,
problem_types=problem_types,
)
extra: dict[str, Any] = {"errors": jsonable_encoder(errors_list)}
return ProblemDetails(
type=problem_type,
title="Validation Error",
status=422,
detail=f"{num_errors} validation error{'s' if num_errors != 1 else ''}",
instance=url,
**extra,
)

4
fastapi/responses.py

@ -13,6 +13,10 @@ from starlette.responses import StreamingResponse as StreamingResponse # noqa
from typing_extensions import deprecated
class ProblemDetailsResponse(JSONResponse):
media_type = "application/problem+json"
class _UjsonModule(Protocol):
def dumps(self, __obj: Any, *, ensure_ascii: bool = ...) -> str: ...

510
tests/test_problem_details.py

@ -0,0 +1,510 @@
import http.client
from fastapi import FastAPI, HTTPException
from fastapi.exceptions import RequestValidationError
from fastapi.responses import ProblemDetailsResponse
from fastapi.testclient import TestClient
from inline_snapshot import snapshot
def test_problem_details_response_content_type():
app = FastAPI(problem_details=True)
@app.get("/error")
def raise_error():
raise HTTPException(status_code=404, detail="Item not found")
client = TestClient(app)
response = client.get("/error")
assert response.status_code == 404
assert response.headers["content-type"] == "application/problem+json"
def test_http_exception_string_detail():
app = FastAPI(problem_details=True)
@app.get("/error")
def raise_error():
raise HTTPException(status_code=404, detail="Item not found")
client = TestClient(app)
response = client.get("/error")
assert response.status_code == 404
data = response.json()
assert data["type"] == "about:blank"
assert data["title"] == "Not Found"
assert data["status"] == 404
assert data["detail"] == "Item not found"
assert "instance" in data
assert "errors" not in data
def test_http_exception_dict_detail():
app = FastAPI(problem_details=True)
@app.get("/error")
def raise_error():
raise HTTPException(
status_code=422,
detail={"code": "CUSTOM_ERROR", "message": "Something went wrong"},
)
client = TestClient(app)
response = client.get("/error")
assert response.status_code == 422
data = response.json()
assert data["title"] == http.client.responses.get(422)
assert data["status"] == 422
assert "detail" not in data
assert "errors" in data
assert data["errors"] == {"code": "CUSTOM_ERROR", "message": "Something went wrong"}
def test_http_exception_list_detail():
app = FastAPI(problem_details=True)
@app.get("/error")
def raise_error():
raise HTTPException(status_code=400, detail=["err1", "err2"])
client = TestClient(app)
response = client.get("/error")
assert response.status_code == 400
data = response.json()
assert "detail" not in data
assert data["errors"] == ["err1", "err2"]
def test_http_exception_with_headers():
app = FastAPI(problem_details=True)
@app.get("/error")
def raise_error():
raise HTTPException(
status_code=404,
detail="Not found",
headers={"X-Error": "custom"},
)
client = TestClient(app)
response = client.get("/error")
assert response.status_code == 404
assert response.headers.get("x-error") == "custom"
assert response.json()["detail"] == "Not found"
def test_no_body_status_code_204():
app = FastAPI(problem_details=True)
@app.get("/no-body")
def no_body():
raise HTTPException(status_code=204)
client = TestClient(app)
response = client.get("/no-body")
assert response.status_code == 204
assert not response.content
def test_no_body_status_code_304():
app = FastAPI(problem_details=True)
@app.get("/no-body")
def no_body():
raise HTTPException(status_code=304)
client = TestClient(app)
response = client.get("/no-body")
assert response.status_code == 304
assert not response.content
def test_no_body_status_code_with_detail():
app = FastAPI(problem_details=True)
@app.get("/no-body")
def no_body():
raise HTTPException(status_code=204, detail="should disappear")
client = TestClient(app)
response = client.get("/no-body")
assert response.status_code == 204
assert not response.content
def test_validation_error():
app = FastAPI(problem_details=True)
@app.get("/items/{item_id}")
def read_item(item_id: int):
return {"item_id": item_id}
client = TestClient(app)
response = client.get("/items/foo")
assert response.status_code == 422
data = response.json()
assert data["type"] == "validation-error"
assert data["title"] == "Validation Error"
assert data["status"] == 422
assert data["detail"] == "1 validation error"
assert "instance" in data
assert "errors" in data
assert len(data["errors"]) == 1
assert data["errors"][0]["loc"] == ["path", "item_id"]
assert data["errors"][0]["type"] == "int_parsing"
def test_multiple_validation_errors():
from pydantic import BaseModel
class Item(BaseModel):
name: str
price: float
app = FastAPI(problem_details=True)
@app.post("/items")
def create_item(item: Item):
return item
client = TestClient(app)
response = client.post("/items", json={})
assert response.status_code == 422
data = response.json()
assert data["detail"] == "2 validation errors"
assert len(data["errors"]) == 2
def test_validation_error_content_type():
app = FastAPI(problem_details=True)
@app.get("/items/{item_id}")
def read_item(item_id: int):
return {"item_id": item_id}
client = TestClient(app)
response = client.get("/items/foo")
assert response.headers["content-type"] == "application/problem+json"
def test_validation_error_overridden_by_custom_handler():
app = FastAPI(problem_details=True)
@app.exception_handler(RequestValidationError)
async def custom_handler(request, exc):
from starlette.responses import JSONResponse
return JSONResponse({"custom": "handler"}, status_code=422)
@app.get("/items/{item_id}")
def read_item(item_id: int):
return {"item_id": item_id}
client = TestClient(app)
response = client.get("/items/foo")
assert response.json() == {"custom": "handler"}
def test_http_exception_overridden_by_custom_handler():
app = FastAPI(problem_details=True)
@app.exception_handler(HTTPException)
async def custom_handler(request, exc):
from starlette.responses import JSONResponse
return JSONResponse({"custom": "http"}, status_code=exc.status_code)
@app.get("/error")
def raise_error():
raise HTTPException(status_code=404)
client = TestClient(app)
response = client.get("/error")
assert response.json() == {"custom": "http"}
def test_default_false_uses_legacy_format():
app = FastAPI(problem_details=False)
@app.get("/error")
def raise_error():
raise HTTPException(status_code=404, detail="Not found")
client = TestClient(app)
response = client.get("/error")
assert response.status_code == 404
assert response.headers["content-type"] == "application/json"
assert response.json() == {"detail": "Not found"}
def test_default_false_validation_uses_legacy_format():
app = FastAPI(problem_details=False)
@app.get("/items/{item_id}")
def read_item(item_id: int):
return {"item_id": item_id}
client = TestClient(app)
response = client.get("/items/foo")
assert response.status_code == 422
assert response.headers["content-type"] == "application/json"
data = response.json()
assert "detail" in data
assert isinstance(data["detail"], list)
def test_problem_details_not_inherited_by_default():
app = FastAPI()
@app.get("/error")
def raise_error():
raise HTTPException(status_code=404, detail="Not found")
client = TestClient(app)
response = client.get("/error")
assert response.headers["content-type"] == "application/json"
def test_openapi_schema_with_problem_details():
app = FastAPI(problem_details=True)
@app.get("/items/{item_id}")
def read_item(item_id: int):
return {"item_id": item_id}
client = TestClient(app)
schema = client.get("/openapi.json").json()
assert schema["paths"]["/items/{item_id}"]["get"]["responses"]["422"] == snapshot(
{
"description": "Validation Error",
"content": {
"application/problem+json": {
"schema": {"$ref": "#/components/schemas/ProblemDetails"}
}
},
}
)
assert "ProblemDetails" in schema["components"]["schemas"]
assert (
schema["components"]["schemas"]["ProblemDetails"]["title"] == "ProblemDetails"
)
def test_openapi_schema_without_problem_details():
app = FastAPI(problem_details=False)
@app.get("/items/{item_id}")
def read_item(item_id: int):
return {"item_id": item_id}
client = TestClient(app)
schema = client.get("/openapi.json").json()
assert (
schema["paths"]["/items/{item_id}"]["get"]["responses"]["422"]["content"][
"application/json"
]["schema"]["$ref"]
== "#/components/schemas/HTTPValidationError"
)
assert "HTTPValidationError" in schema["components"]["schemas"]
def test_openapi_schema_default():
app = FastAPI()
@app.get("/items/{item_id}")
def read_item(item_id: int):
return {"item_id": item_id}
client = TestClient(app)
schema = client.get("/openapi.json").json()
assert "HTTPValidationError" in schema["components"]["schemas"]
assert "ProblemDetails" not in schema["components"]["schemas"]
def test_problem_details_response_class():
response = ProblemDetailsResponse(
content={"type": "about:blank", "title": "Error", "status": 400},
status_code=400,
)
assert response.media_type == "application/problem+json"
assert response.status_code == 400
def test_problem_details_handler_request_without_app():
from fastapi.exception_handlers import _get_problem_details_config
from starlette.requests import Request
request = Request(
scope={"type": "http", "method": "GET", "path": "/", "headers": []}
)
result = _get_problem_details_config(request)
assert result == (None, None)
def test_unicode_status_code():
app = FastAPI(problem_details=True)
@app.get("/teapot")
def teapot():
raise HTTPException(status_code=418)
client = TestClient(app)
response = client.get("/teapot")
assert response.status_code == 418
data = response.json()
assert data["title"] == "I'm a Teapot"
assert data["status"] == 418
def test_instance_url_reflects_request():
app = FastAPI(problem_details=True)
@app.get("/items/{item_id}")
def read_item(item_id: int):
return {"item_id": item_id}
client = TestClient(app, base_url="http://testserver")
response = client.get("/items/abc")
data = response.json()
assert data["instance"] == "http://testserver/items/abc"
def test_explicit_type_on_http_exception():
app = FastAPI(problem_details=True)
@app.get("/error")
def raise_error():
raise HTTPException(
status_code=404, detail="Not found", type="my-custom-problem"
)
client = TestClient(app)
response = client.get("/error")
data = response.json()
assert data["type"] == "my-custom-problem"
def test_explicit_absolute_type_uri():
app = FastAPI(problem_details=True)
@app.get("/error")
def raise_error():
raise HTTPException(
status_code=404,
detail="Not found",
type="https://example.com/errors/custom",
)
client = TestClient(app)
response = client.get("/error")
data = response.json()
assert data["type"] == "https://example.com/errors/custom"
def test_type_base_uri_auto_derivation():
app = FastAPI(
problem_details=True,
problem_type_base_uri="https://example.com/errors",
)
@app.get("/error")
def raise_error():
raise HTTPException(status_code=404, detail="Not found")
client = TestClient(app)
response = client.get("/error")
data = response.json()
assert data["type"] == "https://example.com/errors/not-found"
def test_type_base_uri_with_problem_types_map():
app = FastAPI(
problem_details=True,
problem_type_base_uri="https://example.com/errors",
problem_types={404: "order-not-found"},
)
@app.get("/error")
def raise_error():
raise HTTPException(status_code=404, detail="Not found")
client = TestClient(app)
response = client.get("/error")
data = response.json()
assert data["type"] == "https://example.com/errors/order-not-found"
def test_explicit_type_overrides_problem_types_map():
app = FastAPI(
problem_details=True,
problem_type_base_uri="https://example.com/errors",
problem_types={404: "order-not-found"},
)
@app.get("/error")
def raise_error():
raise HTTPException(status_code=404, detail="Not found", type="custom-override")
client = TestClient(app)
response = client.get("/error")
data = response.json()
assert data["type"] == "https://example.com/errors/custom-override"
def test_validation_error_type_with_base_uri():
app = FastAPI(
problem_details=True,
problem_type_base_uri="https://example.com/errors",
)
@app.get("/items/{item_id}")
def read_item(item_id: int):
return {"item_id": item_id}
client = TestClient(app)
response = client.get("/items/foo")
data = response.json()
assert data["type"] == "https://example.com/errors/validation-error"
def test_validation_error_problem_types_map():
app = FastAPI(
problem_details=True,
problem_type_base_uri="https://example.com/errors",
problem_types={422: "bad-input"},
)
@app.get("/items/{item_id}")
def read_item(item_id: int):
return {"item_id": item_id}
client = TestClient(app)
response = client.get("/items/foo")
data = response.json()
assert data["type"] == "https://example.com/errors/bad-input"
def test_no_base_uri_falls_back_to_segment():
app = FastAPI(problem_details=True)
@app.get("/error")
def raise_error():
raise HTTPException(status_code=404, detail="Not found")
client = TestClient(app)
response = client.get("/error")
data = response.json()
assert data["type"] == "about:blank"
def test_openapi_schema_problem_details_has_additional_properties():
app = FastAPI(problem_details=True)
@app.get("/items/{item_id}")
def read_item(item_id: int):
return {"item_id": item_id}
client = TestClient(app)
schema = client.get("/openapi.json").json()
pd_schema = schema["components"]["schemas"]["ProblemDetails"]
assert pd_schema.get("additionalProperties") is True
Loading…
Cancel
Save