Repository: litestar-org/litestar

Issue:
Bug: status for dependency validation should be changed to 500.
Originally from #138

When a value returned from a provided dependency fails pydantic validation, user gets a 4xx, but it should really be a 5xx.

Ref: https://github.com/starlite-api/starlite/issues/138#issuecomment-1157066885_

<!-- POLAR PLEDGE BADGE START -->
---

## Funding
* If you would like to see an issue prioritized, make a pledge towards it!
* We receive the pledge once the issue is completed & verified

<a href="https://polar.sh/litestar-org/litestar/issues/194">
<picture>
  <source media="(prefers-color-scheme: dark)" srcset="https://polar.sh/api/github/litestar-org/litestar/issues/194/pledge.svg?darkmode=1">
  <img alt="Fund with Polar" src="https://polar.sh/api/github/litestar-org/litestar/issues/194/pledge.svg">
</picture>
</a>
<!-- POLAR PLEDGE BADGE END -->



Discussion:
Any advice on process for this one @Goldziher?
Basically there is one try/except block that handles this: https://github.com/starlite-api/starlite/blob/main/starlite/signature.py#L56

You can probably add an attribute on the `SignatueModel` that will tell you this info, see the attributes already there - which use a `ClassVar` so pydantic ignores them: https://github.com/starlite-api/starlite/blob/main/starlite/signature.py#L21
Yes OK - so perhaps a set of the names of all function parameters that are known dependencies as a classvar on the sig model and inspect that after the validation error to determine if 4xx or 5xx?
Thats how i'd approach it

Resolving patch:
diff --git a/starlite/signature.py b/starlite/signature.py
--- a/starlite/signature.py
+++ b/starlite/signature.py
@@ -22,7 +22,11 @@
 from typing_extensions import get_args
 
 from starlite.connection import Request, WebSocket
-from starlite.exceptions import ImproperlyConfiguredException, ValidationException
+from starlite.exceptions import (
+    ImproperlyConfiguredException,
+    InternalServerException,
+    ValidationException,
+)
 from starlite.plugins.base import PluginMapping, PluginProtocol, get_plugin_for_value
 from starlite.utils.dependency import is_dependency_field
 from starlite.utils.typing import detect_optional_union
@@ -37,6 +41,7 @@ class Config(BaseConfig):
     field_plugin_mappings: ClassVar[Dict[str, PluginMapping]]
     return_annotation: ClassVar[Any]
     has_kwargs: ClassVar[bool]
+    # this is the factory instance used to construct the model
     factory: ClassVar["SignatureModelFactory"]
 
     @classmethod
@@ -50,8 +55,8 @@ def parse_values_from_connection_kwargs(
         This is not equivalent to calling the '.dict'  method of the pydantic model,
         because it doesn't convert nested values into dictionary, just extracts the data from the signature model
         """
+        output: Dict[str, Any] = {}
         try:
-            output: Dict[str, Any] = {}
             modelled_signature = cls(**kwargs)
             for key in cls.__fields__:
                 value = modelled_signature.__getattribute__(key)  # pylint: disable=unnecessary-dunder-call
@@ -70,12 +75,47 @@ def parse_values_from_connection_kwargs(
                         )
                 else:
                     output[key] = value
-            return output
         except ValidationError as e:
-            raise ValidationException(
-                detail=f"Validation failed for {connection.method if isinstance(connection, Request) else 'websocket'} {connection.url}",
-                extra=e.errors(),
-            ) from e
+            raise cls.construct_exception(connection, e) from e
+        return output
+
+    @classmethod
+    def construct_exception(
+        cls, connection: Union[Request, WebSocket], exc: ValidationError
+    ) -> Union[InternalServerException, ValidationException]:
+        """
+        Distinguish between validation errors that arise from parameters and dependencies.
+
+        If both parameter and dependency values are invalid, we raise the client error first.
+
+        Parameters
+        ----------
+        connection : Request | WebSocket
+        exc : ValidationError
+
+        Returns
+        -------
+        ValidationException | InternalServerException
+        """
+        client_errors = []
+        server_errors = []
+        for error in exc.errors():
+            if error["loc"][-1] in cls.factory.dependency_name_set:
+                server_errors.append(error)
+            else:
+                client_errors.append(error)
+        method = connection.method if isinstance(connection, Request) else "websocket"
+
+        if client_errors:
+            return ValidationException(
+                detail=f"Validation failed for {method} {connection.url}",
+                extra=client_errors,
+            )
+
+        return InternalServerException(
+            detail=f"A dependency failed validation for {method} {connection.url}",
+            extra=server_errors,
+        )
 
 
 @dataclass


Test patch:
diff --git a/tests/test_signature.py b/tests/test_signature.py
--- a/tests/test_signature.py
+++ b/tests/test_signature.py
@@ -4,8 +4,10 @@
 import pytest
 from starlette.status import HTTP_204_NO_CONTENT
 
-from starlite import ImproperlyConfiguredException, get
+from starlite import ImproperlyConfiguredException, Provide, get
+from starlite.params import Dependency
 from starlite.signature import SignatureModelFactory
+from starlite.testing import create_test_client
 
 
 def test_create_function_signature_model_parameter_parsing() -> None:
@@ -50,3 +52,57 @@ async def health_check() -> None:
 def test_create_function_signature_model_validation() -> None:
     with pytest.raises(ImproperlyConfiguredException):
         SignatureModelFactory(lru_cache(maxsize=0)(lambda x: x), [], set()).model().dict()  # type: ignore
+
+
+def test_dependency_validation_failure_raises_500() -> None:
+
+    dependencies = {"dep": Provide(lambda: "thirteen")}
+
+    @get("/")
+    def test(dep: int, param: int, optional_dep: Optional[int] = Dependency()) -> None:
+        ...
+
+    with create_test_client(route_handlers=[test], dependencies=dependencies) as client:
+        resp = client.get("/?param=13")
+
+    assert resp.json() == {
+        "detail": "A dependency failed validation for GET http://testserver/?param=13",
+        "extra": [{"loc": ["dep"], "msg": "value is not a valid integer", "type": "type_error.integer"}],
+        "status_code": 500,
+    }
+
+
+def test_validation_failure_raises_400() -> None:
+
+    dependencies = {"dep": Provide(lambda: 13)}
+
+    @get("/")
+    def test(dep: int, param: int, optional_dep: Optional[int] = Dependency()) -> None:
+        ...
+
+    with create_test_client(route_handlers=[test], dependencies=dependencies) as client:
+        resp = client.get("/?param=thirteen")
+
+    assert resp.json() == {
+        "detail": "Validation failed for GET http://testserver/?param=thirteen",
+        "extra": [{"loc": ["param"], "msg": "value is not a valid integer", "type": "type_error.integer"}],
+        "status_code": 400,
+    }
+
+
+def test_client_error_precedence_over_server_error() -> None:
+
+    dependencies = {"dep": Provide(lambda: "thirteen"), "optional_dep": Provide(lambda: "thirty-one")}
+
+    @get("/")
+    def test(dep: int, param: int, optional_dep: Optional[int] = Dependency()) -> None:
+        ...
+
+    with create_test_client(route_handlers=[test], dependencies=dependencies) as client:
+        resp = client.get("/?param=thirteen")
+
+    assert resp.json() == {
+        "detail": "Validation failed for GET http://testserver/?param=thirteen",
+        "extra": [{"loc": ["param"], "msg": "value is not a valid integer", "type": "type_error.integer"}],
+        "status_code": 400,
+    }