Merge pull request #21 from Maillol/add-a-hook-on-the-errors

Add a hook to intercept ValidationError
This commit is contained in:
MAILLOL Vincent 2021-07-26 08:41:50 +02:00 committed by GitHub
commit 258a5cddf6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 100 additions and 8 deletions

View File

@ -310,6 +310,26 @@ Open Api Specification.
self.request.app["model"].remove_pet(id) self.request.app["model"].remove_pet(id)
return web.Response(status=204) return web.Response(status=204)
Custom Validation error
-----------------------
You can redefine the on_validation_error hook in your PydanticView
.. code-block:: python3
class PetView(PydanticView):
async def on_validation_error(self,
exception: ValidationError,
context: str):
errors = exception.errors()
for error in errors:
error["in"] = context # context is "body", "headers", "path" or "query string"
error["custom"] = "your custom field ..."
return json_response(data=errors, status=400)
Demo Demo
---- ----

View File

@ -2,7 +2,7 @@ import abc
import typing import typing
from inspect import signature from inspect import signature
from json.decoder import JSONDecodeError from json.decoder import JSONDecodeError
from typing import Callable, Tuple from typing import Callable, Tuple, Literal
from aiohttp.web_exceptions import HTTPBadRequest from aiohttp.web_exceptions import HTTPBadRequest
from aiohttp.web_request import BaseRequest from aiohttp.web_request import BaseRequest
@ -12,6 +12,9 @@ from pydantic import BaseModel
from .utils import is_pydantic_base_model from .utils import is_pydantic_base_model
CONTEXT = Literal["body", "headers", "path", "query string"]
class AbstractInjector(metaclass=abc.ABCMeta): class AbstractInjector(metaclass=abc.ABCMeta):
""" """
An injector parse HTTP request and inject params to the view. An injector parse HTTP request and inject params to the view.
@ -19,7 +22,7 @@ class AbstractInjector(metaclass=abc.ABCMeta):
@property @property
@abc.abstractmethod @abc.abstractmethod
def context(self) -> str: def context(self) -> CONTEXT:
""" """
The name of part of parsed request The name of part of parsed request
i.e "HTTP header", "URL path", ... i.e "HTTP header", "URL path", ...

View File

@ -1,6 +1,6 @@
from functools import update_wrapper from functools import update_wrapper
from inspect import iscoroutinefunction from inspect import iscoroutinefunction
from typing import Any, Callable, Generator, Iterable, Set, ClassVar from typing import Any, Callable, Generator, Iterable, Set, ClassVar, Literal
import warnings import warnings
from aiohttp.abc import AbstractView from aiohttp.abc import AbstractView
@ -17,6 +17,7 @@ from .injectors import (
MatchInfoGetter, MatchInfoGetter,
QueryGetter, QueryGetter,
_parse_func_signature, _parse_func_signature,
CONTEXT,
) )
@ -88,6 +89,22 @@ class PydanticView(AbstractView):
injectors.append(HeadersGetter(header_args, default_value(header_args))) injectors.append(HeadersGetter(header_args, default_value(header_args)))
return injectors return injectors
async def on_validation_error(
self, exception: ValidationError, context: CONTEXT
) -> StreamResponse:
"""
This method is a hook to intercept ValidationError.
This hook can be redefined to return a custom HTTP response error.
The exception is a pydantic.ValidationError and the context is "body",
"headers", "path" or "query string"
"""
errors = exception.errors()
for error in errors:
error["in"] = context
return json_response(data=errors, status=400)
def inject_params( def inject_params(
handler, parse_func_signature: Callable[[Callable], Iterable[AbstractInjector]] handler, parse_func_signature: Callable[[Callable], Iterable[AbstractInjector]]
@ -109,11 +126,7 @@ def inject_params(
else: else:
injector.inject(self.request, args, kwargs) injector.inject(self.request, args, kwargs)
except ValidationError as error: except ValidationError as error:
errors = error.errors() return await self.on_validation_error(error, injector.context)
for error in errors:
error["in"] = injector.context
return json_response(data=errors, status=400)
return await handler(self, *args, **kwargs) return await handler(self, *args, **kwargs)

View File

@ -0,0 +1,56 @@
from typing import Iterator, List, Optional
from aiohttp import web
from aiohttp.web_response import json_response
from pydantic import BaseModel
from aiohttp_pydantic import PydanticView
class ArticleModel(BaseModel):
name: str
nb_page: Optional[int]
class ArticleModels(BaseModel):
__root__: List[ArticleModel]
def __iter__(self) -> Iterator[ArticleModel]:
return iter(self.__root__)
class ArticleView(PydanticView):
async def post(self, article: ArticleModel):
return web.json_response(article.dict())
async def put(self, articles: ArticleModels):
return web.json_response([article.dict() for article in articles])
async def on_validation_error(self, exception, context):
errors = exception.errors()
for error in errors:
error["in"] = context
error["custom"] = "custom"
return json_response(data=errors, status=400)
async def test_post_an_article_with_wrong_type_field_should_return_an_error_message(
aiohttp_client, loop
):
app = web.Application()
app.router.add_view("/article", ArticleView)
client = await aiohttp_client(app)
resp = await client.post("/article", json={"name": "foo", "nb_page": "foo"})
assert resp.status == 400
assert resp.content_type == "application/json"
assert await resp.json() == [
{
"in": "body",
"loc": ["nb_page"],
"msg": "value is not a valid integer",
"custom": "custom",
"type": "type_error.integer",
}
]