Merge pull request #21 from Maillol/add-a-hook-on-the-errors
Add a hook to intercept ValidationError
This commit is contained in:
		
							
								
								
									
										20
									
								
								README.rst
									
									
									
									
									
								
							
							
						
						
									
										20
									
								
								README.rst
									
									
									
									
									
								
							@@ -310,6 +310,26 @@ Open Api Specification.
 | 
			
		||||
            self.request.app["model"].remove_pet(id)
 | 
			
		||||
            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
 | 
			
		||||
----
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -2,7 +2,7 @@ import abc
 | 
			
		||||
import typing
 | 
			
		||||
from inspect import signature
 | 
			
		||||
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_request import BaseRequest
 | 
			
		||||
@@ -12,6 +12,9 @@ from pydantic import BaseModel
 | 
			
		||||
from .utils import is_pydantic_base_model
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
CONTEXT = Literal["body", "headers", "path", "query string"]
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class AbstractInjector(metaclass=abc.ABCMeta):
 | 
			
		||||
    """
 | 
			
		||||
    An injector parse HTTP request and inject params to the view.
 | 
			
		||||
@@ -19,7 +22,7 @@ class AbstractInjector(metaclass=abc.ABCMeta):
 | 
			
		||||
 | 
			
		||||
    @property
 | 
			
		||||
    @abc.abstractmethod
 | 
			
		||||
    def context(self) -> str:
 | 
			
		||||
    def context(self) -> CONTEXT:
 | 
			
		||||
        """
 | 
			
		||||
        The name of part of parsed request
 | 
			
		||||
        i.e "HTTP header", "URL path", ...
 | 
			
		||||
 
 | 
			
		||||
@@ -1,6 +1,6 @@
 | 
			
		||||
from functools import update_wrapper
 | 
			
		||||
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
 | 
			
		||||
 | 
			
		||||
from aiohttp.abc import AbstractView
 | 
			
		||||
@@ -17,6 +17,7 @@ from .injectors import (
 | 
			
		||||
    MatchInfoGetter,
 | 
			
		||||
    QueryGetter,
 | 
			
		||||
    _parse_func_signature,
 | 
			
		||||
    CONTEXT,
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@@ -88,6 +89,22 @@ class PydanticView(AbstractView):
 | 
			
		||||
            injectors.append(HeadersGetter(header_args, default_value(header_args)))
 | 
			
		||||
        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(
 | 
			
		||||
    handler, parse_func_signature: Callable[[Callable], Iterable[AbstractInjector]]
 | 
			
		||||
@@ -109,11 +126,7 @@ def inject_params(
 | 
			
		||||
                else:
 | 
			
		||||
                    injector.inject(self.request, args, kwargs)
 | 
			
		||||
            except ValidationError as error:
 | 
			
		||||
                errors = error.errors()
 | 
			
		||||
                for error in errors:
 | 
			
		||||
                    error["in"] = injector.context
 | 
			
		||||
 | 
			
		||||
                return json_response(data=errors, status=400)
 | 
			
		||||
                return await self.on_validation_error(error, injector.context)
 | 
			
		||||
 | 
			
		||||
        return await handler(self, *args, **kwargs)
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										56
									
								
								tests/test_hook_to_custom_response.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										56
									
								
								tests/test_hook_to_custom_response.py
									
									
									
									
									
										Normal 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",
 | 
			
		||||
        }
 | 
			
		||||
    ]
 | 
			
		||||
		Reference in New Issue
	
	Block a user