Browse Source
Allow scoping exception handlers to specific routers instead of only at the app level. Router-level handlers override app-level handlers for routes within that router, with precedence: route > child router > parent router > app-level middleware.pull/15067/head
committed by
Michał Połtyn
10 changed files with 827 additions and 2 deletions
@ -0,0 +1,79 @@ |
|||||
|
# Router-Level Exception Handlers { #router-level-exception-handlers } |
||||
|
|
||||
|
In the [Tutorial - Handling Errors](../tutorial/handling-errors.md){.internal-link target=_blank} you learned how to add custom exception handlers to your **FastAPI** application using `@app.exception_handler()`. |
||||
|
|
||||
|
Those handlers apply **globally** to all routes in the application. |
||||
|
|
||||
|
But sometimes you want to handle exceptions differently depending on which part of your application raised them. For example, a group of routes related to payments might need different error handling than routes for user profiles. |
||||
|
|
||||
|
You can do this by adding **exception handlers directly to an `APIRouter`**. |
||||
|
|
||||
|
## Add Exception Handlers to a Router { #add-exception-handlers-to-a-router } |
||||
|
|
||||
|
You can pass `exception_handlers` when creating an `APIRouter`, using the same format as with the `FastAPI` app — a dictionary where keys are exception classes (or status codes) and values are handler functions: |
||||
|
|
||||
|
{* ../../docs_src/handling_errors/tutorial007_py310.py hl[10:20] *} |
||||
|
|
||||
|
Now, if a `UnicornException` is raised in any route within this router, the router's handler will catch it. |
||||
|
|
||||
|
Routes outside this router are **not affected** by the router's exception handlers. |
||||
|
|
||||
|
## Router Handlers Override App Handlers { #router-handlers-override-app-handlers } |
||||
|
|
||||
|
If both the app and a router define a handler for the same exception, the **router's handler takes priority** for routes within that router. The app-level handler still applies to all other routes. |
||||
|
|
||||
|
{* ../../docs_src/handling_errors/tutorial008_py310.py hl[13:21,44:49] *} |
||||
|
|
||||
|
In this example: |
||||
|
|
||||
|
* A request to `/magic/unicorns/yolo` uses the **global** handler (the `magic_router` doesn't define its own). |
||||
|
* A request to `/special/unicorns/yolo` uses the **router-level** handler (defined on the `special_router`). |
||||
|
|
||||
|
This lets you customize error handling per section of your API while keeping a sensible default at the app level. |
||||
|
|
||||
|
## Using `add_exception_handler` { #using-add-exception-handler } |
||||
|
|
||||
|
You can also add exception handlers to a router after creation, using the `add_exception_handler()` method: |
||||
|
|
||||
|
```python |
||||
|
router = APIRouter() |
||||
|
router.add_exception_handler(UnicornException, unicorn_exception_handler) |
||||
|
``` |
||||
|
|
||||
|
This works the same as passing `exception_handlers` in the constructor. |
||||
|
|
||||
|
## Nested Router Precedence { #nested-router-precedence } |
||||
|
|
||||
|
When routers are nested (a router includes another router), exception handlers follow this precedence order (highest to lowest): |
||||
|
|
||||
|
1. The **innermost (child) router**'s handlers |
||||
|
2. The **parent router**'s handlers |
||||
|
3. The **app-level** handlers |
||||
|
|
||||
|
This means a child router can override its parent's handlers for its own routes. |
||||
|
|
||||
|
## Status Code Handlers { #status-code-handlers } |
||||
|
|
||||
|
Just like app-level handlers, you can use **integer status codes** as keys to handle specific HTTP error responses: |
||||
|
|
||||
|
```python |
||||
|
from starlette.exceptions import HTTPException |
||||
|
|
||||
|
def not_found_handler(request, exc): |
||||
|
return JSONResponse( |
||||
|
status_code=404, |
||||
|
content={"message": "Custom 404: resource not found"}, |
||||
|
) |
||||
|
|
||||
|
router = APIRouter( |
||||
|
exception_handlers={404: not_found_handler} |
||||
|
) |
||||
|
``` |
||||
|
|
||||
|
## Recap { #recap } |
||||
|
|
||||
|
* Pass `exception_handlers` to `APIRouter()` to scope handlers to that router's routes. |
||||
|
* Use `router.add_exception_handler()` to add handlers after creation. |
||||
|
* Router-level handlers **override** app-level handlers for the same exception type. |
||||
|
* Routes **outside** the router are unaffected. |
||||
|
* Nested routers follow **child > parent > app** precedence. |
||||
@ -0,0 +1,31 @@ |
|||||
|
from fastapi import APIRouter, FastAPI |
||||
|
from fastapi.responses import JSONResponse |
||||
|
|
||||
|
|
||||
|
class UnicornException(Exception): |
||||
|
def __init__(self, name: str): |
||||
|
self.name = name |
||||
|
|
||||
|
|
||||
|
router = APIRouter( |
||||
|
prefix="/unicorns", |
||||
|
exception_handlers={ |
||||
|
UnicornException: lambda request, exc: JSONResponse( |
||||
|
status_code=418, |
||||
|
content={ |
||||
|
"message": f"Oops! {exc.name} did something. There goes a rainbow..." |
||||
|
}, |
||||
|
) |
||||
|
}, |
||||
|
) |
||||
|
|
||||
|
|
||||
|
@router.get("/{name}") |
||||
|
async def read_unicorn(name: str): |
||||
|
if name == "yolo": |
||||
|
raise UnicornException(name=name) |
||||
|
return {"unicorn_name": name} |
||||
|
|
||||
|
|
||||
|
app = FastAPI() |
||||
|
app.include_router(router) |
||||
@ -0,0 +1,52 @@ |
|||||
|
from fastapi import APIRouter, FastAPI, Request |
||||
|
from fastapi.responses import JSONResponse |
||||
|
|
||||
|
|
||||
|
class UnicornException(Exception): |
||||
|
def __init__(self, name: str): |
||||
|
self.name = name |
||||
|
|
||||
|
|
||||
|
app = FastAPI() |
||||
|
|
||||
|
|
||||
|
@app.exception_handler(UnicornException) |
||||
|
async def global_unicorn_exception_handler(request: Request, exc: UnicornException): |
||||
|
return JSONResponse( |
||||
|
status_code=418, |
||||
|
content={"message": f"Global handler: {exc.name} did something."}, |
||||
|
) |
||||
|
|
||||
|
|
||||
|
magic_router = APIRouter(prefix="/magic") |
||||
|
|
||||
|
|
||||
|
@magic_router.get("/unicorns/{name}") |
||||
|
async def read_magic_unicorn(name: str): |
||||
|
if name == "yolo": |
||||
|
raise UnicornException(name=name) |
||||
|
return {"unicorn_name": name} |
||||
|
|
||||
|
|
||||
|
def custom_unicorn_handler(request: Request, exc: UnicornException): |
||||
|
return JSONResponse( |
||||
|
status_code=418, |
||||
|
content={"message": f"Special handler: {exc.name} did something magical!"}, |
||||
|
) |
||||
|
|
||||
|
|
||||
|
special_router = APIRouter( |
||||
|
prefix="/special", |
||||
|
exception_handlers={UnicornException: custom_unicorn_handler}, |
||||
|
) |
||||
|
|
||||
|
|
||||
|
@special_router.get("/unicorns/{name}") |
||||
|
async def read_special_unicorn(name: str): |
||||
|
if name == "yolo": |
||||
|
raise UnicornException(name=name) |
||||
|
return {"unicorn_name": name} |
||||
|
|
||||
|
|
||||
|
app.include_router(magic_router) |
||||
|
app.include_router(special_router) |
||||
@ -0,0 +1,321 @@ |
|||||
|
from fastapi import APIRouter, FastAPI |
||||
|
from fastapi.responses import JSONResponse |
||||
|
from fastapi.testclient import TestClient |
||||
|
from fastapi.websockets import WebSocket |
||||
|
from starlette.requests import Request |
||||
|
|
||||
|
|
||||
|
class CustomExcA(Exception): |
||||
|
pass |
||||
|
|
||||
|
|
||||
|
class CustomExcB(Exception): |
||||
|
pass |
||||
|
|
||||
|
|
||||
|
def exc_a_handler(request: Request, exc: CustomExcA) -> JSONResponse: |
||||
|
return JSONResponse({"error": "handled_a"}, status_code=400) |
||||
|
|
||||
|
|
||||
|
def exc_b_handler(request: Request, exc: CustomExcB) -> JSONResponse: |
||||
|
return JSONResponse({"error": "handled_b"}, status_code=400) |
||||
|
|
||||
|
|
||||
|
def test_basic_router_exception_handler(): |
||||
|
app = FastAPI() |
||||
|
router = APIRouter(exception_handlers={CustomExcA: exc_a_handler}) |
||||
|
|
||||
|
@router.get("/fail") |
||||
|
def fail(): |
||||
|
raise CustomExcA() |
||||
|
|
||||
|
app.include_router(router) |
||||
|
client = TestClient(app) |
||||
|
resp = client.get("/fail") |
||||
|
assert resp.status_code == 400 |
||||
|
assert resp.json() == {"error": "handled_a"} |
||||
|
|
||||
|
|
||||
|
def test_isolation_between_routers(): |
||||
|
app = FastAPI() |
||||
|
router1 = APIRouter(prefix="/r1", exception_handlers={CustomExcA: exc_a_handler}) |
||||
|
router2 = APIRouter(prefix="/r2") |
||||
|
|
||||
|
@router1.get("/fail") |
||||
|
def fail1(): |
||||
|
raise CustomExcA() |
||||
|
|
||||
|
@router2.get("/fail") |
||||
|
def fail2(): |
||||
|
raise CustomExcA() |
||||
|
|
||||
|
app.include_router(router1) |
||||
|
app.include_router(router2) |
||||
|
client = TestClient(app, raise_server_exceptions=False) |
||||
|
|
||||
|
resp1 = client.get("/r1/fail") |
||||
|
assert resp1.status_code == 400 |
||||
|
assert resp1.json() == {"error": "handled_a"} |
||||
|
|
||||
|
resp2 = client.get("/r2/fail") |
||||
|
assert resp2.status_code == 500 |
||||
|
|
||||
|
|
||||
|
def test_router_overrides_app(): |
||||
|
def app_handler(request: Request, exc: CustomExcA) -> JSONResponse: |
||||
|
return JSONResponse({"error": "app_handled"}, status_code=400) |
||||
|
|
||||
|
def router_handler(request: Request, exc: CustomExcA) -> JSONResponse: |
||||
|
return JSONResponse({"error": "router_handled"}, status_code=400) |
||||
|
|
||||
|
app = FastAPI(exception_handlers={CustomExcA: app_handler}) |
||||
|
router = APIRouter(prefix="/r", exception_handlers={CustomExcA: router_handler}) |
||||
|
|
||||
|
@router.get("/fail") |
||||
|
def fail(): |
||||
|
raise CustomExcA() |
||||
|
|
||||
|
@app.get("/app-fail") |
||||
|
def app_fail(): |
||||
|
raise CustomExcA() |
||||
|
|
||||
|
app.include_router(router) |
||||
|
client = TestClient(app) |
||||
|
|
||||
|
resp = client.get("/r/fail") |
||||
|
assert resp.status_code == 400 |
||||
|
assert resp.json() == {"error": "router_handled"} |
||||
|
|
||||
|
resp_app = client.get("/app-fail") |
||||
|
assert resp_app.status_code == 400 |
||||
|
assert resp_app.json() == {"error": "app_handled"} |
||||
|
|
||||
|
|
||||
|
def test_app_fallback(): |
||||
|
def app_handler(request: Request, exc: CustomExcA) -> JSONResponse: |
||||
|
return JSONResponse({"error": "app_handled"}, status_code=400) |
||||
|
|
||||
|
app = FastAPI(exception_handlers={CustomExcA: app_handler}) |
||||
|
router = APIRouter(prefix="/r") |
||||
|
|
||||
|
@router.get("/fail") |
||||
|
def fail(): |
||||
|
raise CustomExcA() |
||||
|
|
||||
|
app.include_router(router) |
||||
|
client = TestClient(app) |
||||
|
|
||||
|
resp = client.get("/r/fail") |
||||
|
assert resp.status_code == 400 |
||||
|
assert resp.json() == {"error": "app_handled"} |
||||
|
|
||||
|
|
||||
|
def test_nested_router_precedence(): |
||||
|
def handler_a(request: Request, exc: CustomExcA) -> JSONResponse: |
||||
|
return JSONResponse({"error": "A"}, status_code=400) |
||||
|
|
||||
|
def handler_b(request: Request, exc: CustomExcA) -> JSONResponse: |
||||
|
return JSONResponse({"error": "B"}, status_code=400) |
||||
|
|
||||
|
router_a = APIRouter(prefix="/a", exception_handlers={CustomExcA: handler_a}) |
||||
|
router_b = APIRouter(prefix="/b", exception_handlers={CustomExcA: handler_b}) |
||||
|
|
||||
|
@router_a.get("/fail") |
||||
|
def fail_a(): |
||||
|
raise CustomExcA() |
||||
|
|
||||
|
@router_b.get("/fail") |
||||
|
def fail_b(): |
||||
|
raise CustomExcA() |
||||
|
|
||||
|
router_a.include_router(router_b) |
||||
|
app = FastAPI() |
||||
|
app.include_router(router_a) |
||||
|
client = TestClient(app) |
||||
|
|
||||
|
resp_b = client.get("/a/b/fail") |
||||
|
assert resp_b.status_code == 400 |
||||
|
assert resp_b.json() == {"error": "B"} |
||||
|
|
||||
|
resp_a = client.get("/a/fail") |
||||
|
assert resp_a.status_code == 400 |
||||
|
assert resp_a.json() == {"error": "A"} |
||||
|
|
||||
|
|
||||
|
def test_deep_nesting(): |
||||
|
def handler_a(request: Request, exc: CustomExcA) -> JSONResponse: |
||||
|
return JSONResponse({"error": "A"}, status_code=400) |
||||
|
|
||||
|
def handler_b(request: Request, exc: CustomExcA) -> JSONResponse: |
||||
|
return JSONResponse({"error": "B"}, status_code=400) |
||||
|
|
||||
|
def handler_c(request: Request, exc: CustomExcA) -> JSONResponse: |
||||
|
return JSONResponse({"error": "C"}, status_code=400) |
||||
|
|
||||
|
router_a = APIRouter(prefix="/a", exception_handlers={CustomExcA: handler_a}) |
||||
|
router_b = APIRouter(prefix="/b", exception_handlers={CustomExcA: handler_b}) |
||||
|
router_c = APIRouter(prefix="/c", exception_handlers={CustomExcA: handler_c}) |
||||
|
|
||||
|
@router_a.get("/fail") |
||||
|
def fail_a(): |
||||
|
raise CustomExcA() |
||||
|
|
||||
|
@router_b.get("/fail") |
||||
|
def fail_b(): |
||||
|
raise CustomExcA() |
||||
|
|
||||
|
@router_c.get("/fail") |
||||
|
def fail_c(): |
||||
|
raise CustomExcA() |
||||
|
|
||||
|
router_b.include_router(router_c) |
||||
|
router_a.include_router(router_b) |
||||
|
app = FastAPI() |
||||
|
app.include_router(router_a) |
||||
|
client = TestClient(app) |
||||
|
|
||||
|
resp_c = client.get("/a/b/c/fail") |
||||
|
assert resp_c.status_code == 400 |
||||
|
assert resp_c.json() == {"error": "C"} |
||||
|
|
||||
|
resp_b = client.get("/a/b/fail") |
||||
|
assert resp_b.status_code == 400 |
||||
|
assert resp_b.json() == {"error": "B"} |
||||
|
|
||||
|
resp_a = client.get("/a/fail") |
||||
|
assert resp_a.status_code == 400 |
||||
|
assert resp_a.json() == {"error": "A"} |
||||
|
|
||||
|
|
||||
|
def test_add_exception_handler_method(): |
||||
|
app = FastAPI() |
||||
|
router = APIRouter() |
||||
|
router.add_exception_handler(CustomExcA, exc_a_handler) |
||||
|
|
||||
|
@router.get("/fail") |
||||
|
def fail(): |
||||
|
raise CustomExcA() |
||||
|
|
||||
|
app.include_router(router) |
||||
|
client = TestClient(app) |
||||
|
|
||||
|
resp = client.get("/fail") |
||||
|
assert resp.status_code == 400 |
||||
|
assert resp.json() == {"error": "handled_a"} |
||||
|
|
||||
|
|
||||
|
def test_status_code_handler(): |
||||
|
from starlette.exceptions import HTTPException |
||||
|
|
||||
|
def not_found_handler(request: Request, exc: HTTPException) -> JSONResponse: |
||||
|
return JSONResponse({"error": "custom_404"}, status_code=404) |
||||
|
|
||||
|
app = FastAPI() |
||||
|
router = APIRouter(prefix="/r", exception_handlers={404: not_found_handler}) |
||||
|
|
||||
|
@router.get("/fail") |
||||
|
def fail(): |
||||
|
raise HTTPException(status_code=404, detail="not found") |
||||
|
|
||||
|
app.include_router(router) |
||||
|
client = TestClient(app) |
||||
|
|
||||
|
resp = client.get("/r/fail") |
||||
|
assert resp.status_code == 404 |
||||
|
assert resp.json() == {"error": "custom_404"} |
||||
|
|
||||
|
|
||||
|
def test_app_routes_unaffected(): |
||||
|
app = FastAPI() |
||||
|
router = APIRouter(prefix="/r", exception_handlers={CustomExcA: exc_a_handler}) |
||||
|
|
||||
|
@router.get("/fail") |
||||
|
def router_fail(): |
||||
|
raise CustomExcA() |
||||
|
|
||||
|
@app.get("/fail") |
||||
|
def app_fail(): |
||||
|
raise CustomExcA() |
||||
|
|
||||
|
app.include_router(router) |
||||
|
client = TestClient(app, raise_server_exceptions=False) |
||||
|
|
||||
|
resp_router = client.get("/r/fail") |
||||
|
assert resp_router.status_code == 400 |
||||
|
assert resp_router.json() == {"error": "handled_a"} |
||||
|
|
||||
|
resp_app = client.get("/fail") |
||||
|
assert resp_app.status_code == 500 |
||||
|
|
||||
|
|
||||
|
def test_multiple_exception_types(): |
||||
|
app = FastAPI() |
||||
|
router = APIRouter( |
||||
|
exception_handlers={ |
||||
|
CustomExcA: exc_a_handler, |
||||
|
CustomExcB: exc_b_handler, |
||||
|
} |
||||
|
) |
||||
|
|
||||
|
@router.get("/fail-a") |
||||
|
def fail_a(): |
||||
|
raise CustomExcA() |
||||
|
|
||||
|
@router.get("/fail-b") |
||||
|
def fail_b(): |
||||
|
raise CustomExcB() |
||||
|
|
||||
|
app.include_router(router) |
||||
|
client = TestClient(app) |
||||
|
|
||||
|
resp_a = client.get("/fail-a") |
||||
|
assert resp_a.status_code == 400 |
||||
|
assert resp_a.json() == {"error": "handled_a"} |
||||
|
|
||||
|
resp_b = client.get("/fail-b") |
||||
|
assert resp_b.status_code == 400 |
||||
|
assert resp_b.json() == {"error": "handled_b"} |
||||
|
|
||||
|
|
||||
|
def test_websocket_router_exception_handler(): |
||||
|
app = FastAPI() |
||||
|
router = APIRouter( |
||||
|
exception_handlers={CustomExcA: exc_a_handler, 404: exc_a_handler} |
||||
|
) |
||||
|
|
||||
|
@router.websocket("/ws") |
||||
|
async def ws_endpoint(websocket: WebSocket): |
||||
|
await websocket.accept() |
||||
|
raise CustomExcA() |
||||
|
|
||||
|
app.include_router(router) |
||||
|
client = TestClient(app) |
||||
|
|
||||
|
with client.websocket_connect("/ws") as ws: |
||||
|
# The exception handler returns a JSON response, but for websockets |
||||
|
# the connection should close with an error |
||||
|
try: |
||||
|
ws.receive_text() |
||||
|
except Exception: |
||||
|
pass |
||||
|
|
||||
|
|
||||
|
def test_websocket_nested_router_exception_handler(): |
||||
|
app = FastAPI() |
||||
|
router_a = APIRouter(prefix="/a", exception_handlers={CustomExcA: exc_a_handler}) |
||||
|
router_b = APIRouter(prefix="/b", exception_handlers={CustomExcA: exc_a_handler}) |
||||
|
|
||||
|
@router_b.websocket("/ws") |
||||
|
async def ws_endpoint(websocket: WebSocket): |
||||
|
await websocket.accept() |
||||
|
raise CustomExcA() |
||||
|
|
||||
|
router_a.include_router(router_b) |
||||
|
app.include_router(router_a) |
||||
|
client = TestClient(app) |
||||
|
|
||||
|
with client.websocket_connect("/a/b/ws") as ws: |
||||
|
try: |
||||
|
ws.receive_text() |
||||
|
except Exception: |
||||
|
pass |
||||
@ -0,0 +1,101 @@ |
|||||
|
from fastapi.testclient import TestClient |
||||
|
from inline_snapshot import snapshot |
||||
|
|
||||
|
from docs_src.handling_errors.tutorial007_py310 import app |
||||
|
|
||||
|
client = TestClient(app) |
||||
|
|
||||
|
|
||||
|
def test_get(): |
||||
|
response = client.get("/unicorns/shinny") |
||||
|
assert response.status_code == 200, response.text |
||||
|
assert response.json() == {"unicorn_name": "shinny"} |
||||
|
|
||||
|
|
||||
|
def test_get_exception(): |
||||
|
response = client.get("/unicorns/yolo") |
||||
|
assert response.status_code == 418, response.text |
||||
|
assert response.json() == { |
||||
|
"message": "Oops! yolo did something. There goes a rainbow..." |
||||
|
} |
||||
|
|
||||
|
|
||||
|
def test_openapi_schema(): |
||||
|
response = client.get("/openapi.json") |
||||
|
assert response.status_code == 200, response.text |
||||
|
assert response.json() == snapshot( |
||||
|
{ |
||||
|
"openapi": "3.1.0", |
||||
|
"info": {"title": "FastAPI", "version": "0.1.0"}, |
||||
|
"paths": { |
||||
|
"/unicorns/{name}": { |
||||
|
"get": { |
||||
|
"responses": { |
||||
|
"200": { |
||||
|
"description": "Successful Response", |
||||
|
"content": {"application/json": {"schema": {}}}, |
||||
|
}, |
||||
|
"422": { |
||||
|
"description": "Validation Error", |
||||
|
"content": { |
||||
|
"application/json": { |
||||
|
"schema": { |
||||
|
"$ref": "#/components/schemas/HTTPValidationError" |
||||
|
} |
||||
|
} |
||||
|
}, |
||||
|
}, |
||||
|
}, |
||||
|
"summary": "Read Unicorn", |
||||
|
"operationId": "read_unicorn_unicorns__name__get", |
||||
|
"parameters": [ |
||||
|
{ |
||||
|
"required": True, |
||||
|
"schema": {"title": "Name", "type": "string"}, |
||||
|
"name": "name", |
||||
|
"in": "path", |
||||
|
} |
||||
|
], |
||||
|
} |
||||
|
} |
||||
|
}, |
||||
|
"components": { |
||||
|
"schemas": { |
||||
|
"ValidationError": { |
||||
|
"title": "ValidationError", |
||||
|
"required": ["loc", "msg", "type"], |
||||
|
"type": "object", |
||||
|
"properties": { |
||||
|
"loc": { |
||||
|
"title": "Location", |
||||
|
"type": "array", |
||||
|
"items": { |
||||
|
"anyOf": [ |
||||
|
{"type": "string"}, |
||||
|
{"type": "integer"}, |
||||
|
] |
||||
|
}, |
||||
|
}, |
||||
|
"msg": {"title": "Message", "type": "string"}, |
||||
|
"type": {"title": "Error Type", "type": "string"}, |
||||
|
"input": {"title": "Input"}, |
||||
|
"ctx": {"title": "Context", "type": "object"}, |
||||
|
}, |
||||
|
}, |
||||
|
"HTTPValidationError": { |
||||
|
"title": "HTTPValidationError", |
||||
|
"type": "object", |
||||
|
"properties": { |
||||
|
"detail": { |
||||
|
"title": "Detail", |
||||
|
"type": "array", |
||||
|
"items": { |
||||
|
"$ref": "#/components/schemas/ValidationError" |
||||
|
}, |
||||
|
} |
||||
|
}, |
||||
|
}, |
||||
|
} |
||||
|
}, |
||||
|
} |
||||
|
) |
||||
@ -0,0 +1,143 @@ |
|||||
|
from fastapi.testclient import TestClient |
||||
|
from inline_snapshot import snapshot |
||||
|
|
||||
|
from docs_src.handling_errors.tutorial008_py310 import app |
||||
|
|
||||
|
client = TestClient(app) |
||||
|
|
||||
|
|
||||
|
def test_get_magic(): |
||||
|
response = client.get("/magic/unicorns/shinny") |
||||
|
assert response.status_code == 200, response.text |
||||
|
assert response.json() == {"unicorn_name": "shinny"} |
||||
|
|
||||
|
|
||||
|
def test_get_magic_exception(): |
||||
|
response = client.get("/magic/unicorns/yolo") |
||||
|
assert response.status_code == 418, response.text |
||||
|
assert response.json() == {"message": "Global handler: yolo did something."} |
||||
|
|
||||
|
|
||||
|
def test_get_special(): |
||||
|
response = client.get("/special/unicorns/shinny") |
||||
|
assert response.status_code == 200, response.text |
||||
|
assert response.json() == {"unicorn_name": "shinny"} |
||||
|
|
||||
|
|
||||
|
def test_get_special_exception(): |
||||
|
response = client.get("/special/unicorns/yolo") |
||||
|
assert response.status_code == 418, response.text |
||||
|
assert response.json() == { |
||||
|
"message": "Special handler: yolo did something magical!" |
||||
|
} |
||||
|
|
||||
|
|
||||
|
def test_openapi_schema(): |
||||
|
response = client.get("/openapi.json") |
||||
|
assert response.status_code == 200, response.text |
||||
|
assert response.json() == snapshot( |
||||
|
{ |
||||
|
"openapi": "3.1.0", |
||||
|
"info": {"title": "FastAPI", "version": "0.1.0"}, |
||||
|
"paths": { |
||||
|
"/magic/unicorns/{name}": { |
||||
|
"get": { |
||||
|
"responses": { |
||||
|
"200": { |
||||
|
"description": "Successful Response", |
||||
|
"content": {"application/json": {"schema": {}}}, |
||||
|
}, |
||||
|
"422": { |
||||
|
"description": "Validation Error", |
||||
|
"content": { |
||||
|
"application/json": { |
||||
|
"schema": { |
||||
|
"$ref": "#/components/schemas/HTTPValidationError" |
||||
|
} |
||||
|
} |
||||
|
}, |
||||
|
}, |
||||
|
}, |
||||
|
"summary": "Read Magic Unicorn", |
||||
|
"operationId": "read_magic_unicorn_magic_unicorns__name__get", |
||||
|
"parameters": [ |
||||
|
{ |
||||
|
"required": True, |
||||
|
"schema": {"title": "Name", "type": "string"}, |
||||
|
"name": "name", |
||||
|
"in": "path", |
||||
|
} |
||||
|
], |
||||
|
} |
||||
|
}, |
||||
|
"/special/unicorns/{name}": { |
||||
|
"get": { |
||||
|
"responses": { |
||||
|
"200": { |
||||
|
"description": "Successful Response", |
||||
|
"content": {"application/json": {"schema": {}}}, |
||||
|
}, |
||||
|
"422": { |
||||
|
"description": "Validation Error", |
||||
|
"content": { |
||||
|
"application/json": { |
||||
|
"schema": { |
||||
|
"$ref": "#/components/schemas/HTTPValidationError" |
||||
|
} |
||||
|
} |
||||
|
}, |
||||
|
}, |
||||
|
}, |
||||
|
"summary": "Read Special Unicorn", |
||||
|
"operationId": "read_special_unicorn_special_unicorns__name__get", |
||||
|
"parameters": [ |
||||
|
{ |
||||
|
"required": True, |
||||
|
"schema": {"title": "Name", "type": "string"}, |
||||
|
"name": "name", |
||||
|
"in": "path", |
||||
|
} |
||||
|
], |
||||
|
} |
||||
|
}, |
||||
|
}, |
||||
|
"components": { |
||||
|
"schemas": { |
||||
|
"ValidationError": { |
||||
|
"title": "ValidationError", |
||||
|
"required": ["loc", "msg", "type"], |
||||
|
"type": "object", |
||||
|
"properties": { |
||||
|
"loc": { |
||||
|
"title": "Location", |
||||
|
"type": "array", |
||||
|
"items": { |
||||
|
"anyOf": [ |
||||
|
{"type": "string"}, |
||||
|
{"type": "integer"}, |
||||
|
] |
||||
|
}, |
||||
|
}, |
||||
|
"msg": {"title": "Message", "type": "string"}, |
||||
|
"type": {"title": "Error Type", "type": "string"}, |
||||
|
"input": {"title": "Input"}, |
||||
|
"ctx": {"title": "Context", "type": "object"}, |
||||
|
}, |
||||
|
}, |
||||
|
"HTTPValidationError": { |
||||
|
"title": "HTTPValidationError", |
||||
|
"type": "object", |
||||
|
"properties": { |
||||
|
"detail": { |
||||
|
"title": "Detail", |
||||
|
"type": "array", |
||||
|
"items": { |
||||
|
"$ref": "#/components/schemas/ValidationError" |
||||
|
}, |
||||
|
} |
||||
|
}, |
||||
|
}, |
||||
|
} |
||||
|
}, |
||||
|
} |
||||
|
) |
||||
Loading…
Reference in new issue