]> git.ipfire.org Git - thirdparty/fastapi/fastapi.git/commitdiff
:sparkles: add body to RequestValidationError for easier debugging (#853)
authorAviram Hassan <41201924+aviramha@users.noreply.github.com>
Fri, 17 Jan 2020 11:37:44 +0000 (13:37 +0200)
committerSebastián Ramírez <tiangolo@gmail.com>
Fri, 17 Jan 2020 11:37:44 +0000 (12:37 +0100)
docs/src/handling_errors/tutorial005.py
docs/src/handling_errors/tutorial006.py [new file with mode: 0644]
docs/tutorial/custom-request-and-route.md
docs/tutorial/handling-errors.md
fastapi/exceptions.py
fastapi/routing.py
tests/test_tutorial/test_handling_errors/test_tutorial005.py
tests/test_tutorial/test_handling_errors/test_tutorial006.py [new file with mode: 0644]

index 8cabc9c24b47bd638647060ce991295ce92f221e..38a2c0a08d42c28183a118f7fb230c2d36255878 100644 (file)
@@ -1,28 +1,27 @@
-from fastapi import FastAPI, HTTPException
-from fastapi.exception_handlers import (
-    http_exception_handler,
-    request_validation_exception_handler,
-)
+from fastapi import FastAPI
+from fastapi.encoders import jsonable_encoder
 from fastapi.exceptions import RequestValidationError
-from starlette.exceptions import HTTPException as StarletteHTTPException
+from pydantic import BaseModel
+from starlette import status
+from starlette.requests import Request
+from starlette.responses import JSONResponse
 
 app = FastAPI()
 
 
-@app.exception_handler(StarletteHTTPException)
-async def custom_http_exception_handler(request, exc):
-    print(f"OMG! An HTTP error!: {exc}")
-    return await http_exception_handler(request, exc)
+@app.exception_handler(RequestValidationError)
+async def validation_exception_handler(request: Request, exc: RequestValidationError):
+    return JSONResponse(
+        status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
+        content=jsonable_encoder({"detail": exc.errors(), "body": exc.body}),
+    )
 
 
-@app.exception_handler(RequestValidationError)
-async def validation_exception_handler(request, exc):
-    print(f"OMG! The client sent invalid data!: {exc}")
-    return await request_validation_exception_handler(request, exc)
+class Item(BaseModel):
+    title: str
+    size: int
 
 
-@app.get("/items/{item_id}")
-async def read_item(item_id: int):
-    if item_id == 3:
-        raise HTTPException(status_code=418, detail="Nope! I don't like 3.")
-    return {"item_id": item_id}
+@app.post("/items/")
+async def create_item(item: Item):
+    return item
diff --git a/docs/src/handling_errors/tutorial006.py b/docs/src/handling_errors/tutorial006.py
new file mode 100644 (file)
index 0000000..8cabc9c
--- /dev/null
@@ -0,0 +1,28 @@
+from fastapi import FastAPI, HTTPException
+from fastapi.exception_handlers import (
+    http_exception_handler,
+    request_validation_exception_handler,
+)
+from fastapi.exceptions import RequestValidationError
+from starlette.exceptions import HTTPException as StarletteHTTPException
+
+app = FastAPI()
+
+
+@app.exception_handler(StarletteHTTPException)
+async def custom_http_exception_handler(request, exc):
+    print(f"OMG! An HTTP error!: {exc}")
+    return await http_exception_handler(request, exc)
+
+
+@app.exception_handler(RequestValidationError)
+async def validation_exception_handler(request, exc):
+    print(f"OMG! The client sent invalid data!: {exc}")
+    return await request_validation_exception_handler(request, exc)
+
+
+@app.get("/items/{item_id}")
+async def read_item(item_id: int):
+    if item_id == 3:
+        raise HTTPException(status_code=418, detail="Nope! I don't like 3.")
+    return {"item_id": item_id}
index 49cca992fad4dfdda2144c36787698f8cc803fe9..300bbb0544cea40a190e71ec1564ab4021e85a28 100644 (file)
@@ -16,7 +16,6 @@ Some use cases include:
 * Converting non-JSON request bodies to JSON (e.g. [`msgpack`](https://msgpack.org/index.html)).
 * Decompressing gzip-compressed request bodies.
 * Automatically logging all request bodies.
-* Accessing the request body in an exception handler.
 
 ## Handling custom request body encodings
 
@@ -71,6 +70,11 @@ But because of our changes in `GzipRequest.body`, the request body will be autom
 
 ## Accessing the request body in an exception handler
 
+!!! tip
+    To solve this same problem, it's probably a lot easier to [use the `body` in a custom handler for `RequestValidationError`](https://fastapi.tiangolo.com/tutorial/handling-errors/#use-the-requestvalidationerror-body).
+
+    But this example is still valid and it shows how to interact with the internal components.
+
 We can also use this same approach to access the request body in an exception handler.
 
 All we need to do is handle the request inside a `try`/`except` block:
@@ -89,12 +93,12 @@ If an exception occurs, the`Request` instance will still be in scope, so we can
 
 You can also set the `route_class` parameter of an `APIRouter`:
 
-```Python hl_lines="25"
+```Python hl_lines="28"
 {!./src/custom_request_and_route/tutorial003.py!}
 ```
 
 In this example, the *path operations* under the `router` will use the custom `TimedRoute` class, and will have an extra `X-Response-Time` header in the response with the time it took to generate the response:
 
-```Python hl_lines="15 16 17 18 19"
+```Python hl_lines="15 16 17 18 19 20 21 22"
 {!./src/custom_request_and_route/tutorial003.py!}
 ```
index 5d364fd0b8237452f0291d7c6961a86a3f6daebb..d4f1d5486a9c04c666d4aaf09bbaee5def5035c1 100644 (file)
@@ -176,6 +176,47 @@ For example, you could want to return a plain text response instead of JSON for
 {!./src/handling_errors/tutorial004.py!}
 ```
 
+### Use the `RequestValidationError` body
+
+The `RequestValidationError` contains the `body` it received with invalid data.
+
+You could use it while developing your app to log the body and debug it, return it to the user, etc.
+
+```Python hl_lines="16"
+{!./src/handling_errors/tutorial005.py!}
+```
+
+Now try sending an invalid item like:
+
+```JSON
+{
+  "title": "towel",
+  "size": "XL"
+}
+```
+
+You will receive a response telling you that the data is invalid containing the received body:
+
+```JSON hl_lines="13 14 15 16"
+{
+  "detail": [
+    {
+      "loc": [
+        "body",
+        "item",
+        "size"
+      ],
+      "msg": "value is not a valid integer",
+      "type": "type_error.integer"
+    }
+  ],
+  "body": {
+    "title": "towel",
+    "size": "XL"
+  }
+}
+```
+
 #### FastAPI's `HTTPException` vs Starlette's `HTTPException`
 
 **FastAPI** has its own `HTTPException`.
index d4b1329d7d8909a54179f0a1a90ef550bad287c8..ac002205a3762a4eafbc9f259ff58642ee262e25 100644 (file)
@@ -21,7 +21,8 @@ WebSocketErrorModel = create_model("WebSocket")
 
 
 class RequestValidationError(ValidationError):
-    def __init__(self, errors: Sequence[ErrorList]) -> None:
+    def __init__(self, errors: Sequence[ErrorList], *, body: Any = None) -> None:
+        self.body = body
         if PYDANTIC_1:
             super().__init__(errors, RequestErrorModel)
         else:
index 4e08c61b7ebabb2a4d324bc0c766cf2cee874d0a..0358653887b1074d449245bd25da7c78a39f3f87 100644 (file)
@@ -120,7 +120,7 @@ def get_request_handler(
         )
         values, errors, background_tasks, sub_response, _ = solved_result
         if errors:
-            raise RequestValidationError(errors)
+            raise RequestValidationError(errors, body=body)
         else:
             assert dependant.call is not None, "dependant.call must be a function"
             if is_coroutine:
index 4813201c83bfb46f8dc7a3a3ec4a0b775c5099a3..25b56dd2218218e130d5363be005db1d65bfe521 100644 (file)
@@ -8,8 +8,18 @@ openapi_schema = {
     "openapi": "3.0.2",
     "info": {"title": "Fast API", "version": "0.1.0"},
     "paths": {
-        "/items/{item_id}": {
-            "get": {
+        "/items/": {
+            "post": {
+                "summary": "Create Item",
+                "operationId": "create_item_items__post",
+                "requestBody": {
+                    "content": {
+                        "application/json": {
+                            "schema": {"$ref": "#/components/schemas/Item"}
+                        }
+                    },
+                    "required": True,
+                },
                 "responses": {
                     "200": {
                         "description": "Successful Response",
@@ -26,21 +36,31 @@ openapi_schema = {
                         },
                     },
                 },
-                "summary": "Read Item",
-                "operationId": "read_item_items__item_id__get",
-                "parameters": [
-                    {
-                        "required": True,
-                        "schema": {"title": "Item Id", "type": "integer"},
-                        "name": "item_id",
-                        "in": "path",
-                    }
-                ],
             }
         }
     },
     "components": {
         "schemas": {
+            "HTTPValidationError": {
+                "title": "HTTPValidationError",
+                "type": "object",
+                "properties": {
+                    "detail": {
+                        "title": "Detail",
+                        "type": "array",
+                        "items": {"$ref": "#/components/schemas/ValidationError"},
+                    }
+                },
+            },
+            "Item": {
+                "title": "Item",
+                "required": ["title", "size"],
+                "type": "object",
+                "properties": {
+                    "title": {"title": "Title", "type": "string"},
+                    "size": {"title": "Size", "type": "integer"},
+                },
+            },
             "ValidationError": {
                 "title": "ValidationError",
                 "required": ["loc", "msg", "type"],
@@ -55,17 +75,6 @@ openapi_schema = {
                     "type": {"title": "Error Type", "type": "string"},
                 },
             },
-            "HTTPValidationError": {
-                "title": "HTTPValidationError",
-                "type": "object",
-                "properties": {
-                    "detail": {
-                        "title": "Detail",
-                        "type": "array",
-                        "items": {"$ref": "#/components/schemas/ValidationError"},
-                    }
-                },
-            },
         }
     },
 }
@@ -77,27 +86,23 @@ def test_openapi_schema():
     assert response.json() == openapi_schema
 
 
-def test_get_validation_error():
-    response = client.get("/items/foo")
+def test_post_validation_error():
+    response = client.post("/items/", json={"title": "towel", "size": "XL"})
     assert response.status_code == 422
     assert response.json() == {
         "detail": [
             {
-                "loc": ["path", "item_id"],
+                "loc": ["body", "item", "size"],
                 "msg": "value is not a valid integer",
                 "type": "type_error.integer",
             }
-        ]
+        ],
+        "body": {"title": "towel", "size": "XL"},
     }
 
 
-def test_get_http_error():
-    response = client.get("/items/3")
-    assert response.status_code == 418
-    assert response.json() == {"detail": "Nope! I don't like 3."}
-
-
-def test_get():
-    response = client.get("/items/2")
+def test_post():
+    data = {"title": "towel", "size": 5}
+    response = client.post("/items/", json=data)
     assert response.status_code == 200
-    assert response.json() == {"item_id": 2}
+    assert response.json() == data
diff --git a/tests/test_tutorial/test_handling_errors/test_tutorial006.py b/tests/test_tutorial/test_handling_errors/test_tutorial006.py
new file mode 100644 (file)
index 0000000..fe6c401
--- /dev/null
@@ -0,0 +1,103 @@
+from starlette.testclient import TestClient
+
+from handling_errors.tutorial006 import app
+
+client = TestClient(app)
+
+openapi_schema = {
+    "openapi": "3.0.2",
+    "info": {"title": "Fast API", "version": "0.1.0"},
+    "paths": {
+        "/items/{item_id}": {
+            "get": {
+                "responses": {
+                    "200": {
+                        "description": "Successful Response",
+                        "content": {"application/json": {"schema": {}}},
+                    },
+                    "422": {
+                        "description": "Validation Error",
+                        "content": {
+                            "application/json": {
+                                "schema": {
+                                    "$ref": "#/components/schemas/HTTPValidationError"
+                                }
+                            }
+                        },
+                    },
+                },
+                "summary": "Read Item",
+                "operationId": "read_item_items__item_id__get",
+                "parameters": [
+                    {
+                        "required": True,
+                        "schema": {"title": "Item Id", "type": "integer"},
+                        "name": "item_id",
+                        "in": "path",
+                    }
+                ],
+            }
+        }
+    },
+    "components": {
+        "schemas": {
+            "ValidationError": {
+                "title": "ValidationError",
+                "required": ["loc", "msg", "type"],
+                "type": "object",
+                "properties": {
+                    "loc": {
+                        "title": "Location",
+                        "type": "array",
+                        "items": {"type": "string"},
+                    },
+                    "msg": {"title": "Message", "type": "string"},
+                    "type": {"title": "Error Type", "type": "string"},
+                },
+            },
+            "HTTPValidationError": {
+                "title": "HTTPValidationError",
+                "type": "object",
+                "properties": {
+                    "detail": {
+                        "title": "Detail",
+                        "type": "array",
+                        "items": {"$ref": "#/components/schemas/ValidationError"},
+                    }
+                },
+            },
+        }
+    },
+}
+
+
+def test_openapi_schema():
+    response = client.get("/openapi.json")
+    assert response.status_code == 200
+    assert response.json() == openapi_schema
+
+
+def test_get_validation_error():
+    response = client.get("/items/foo")
+    assert response.status_code == 422
+    assert response.json() == {
+        "detail": [
+            {
+                "loc": ["path", "item_id"],
+                "msg": "value is not a valid integer",
+                "type": "type_error.integer",
+            }
+        ]
+    }
+
+
+def test_get_http_error():
+    response = client.get("/items/3")
+    assert response.status_code == 418
+    assert response.json() == {"detail": "Nope! I don't like 3."}
+
+
+def test_get():
+    response = client.get("/items/2")
+    assert response.status_code == 200
+    assert response.json() == {"item_id": 2}