Repository: litestar-org/litestar

Issue:
 Bug: Long cryptic error when excluding a required field from a DTO
### Description

As mentioned on Discord, excluding a required field from a DTO triggers a 500 Internal Server Error at runtime. This is hard to troubleshoot even at debug mode because the traceback is too long and ends with `litestar.exceptions.base_exceptions.SerializationException: Unsupported type: <class '...'>`, which is rather misleading

### URL to code causing the issue

_No response_

### MCVE

```python
from typing import Annotated

from litestar import Litestar, post
from litestar.contrib.pydantic import PydanticDTO
from litestar.dto import DTOConfig
from pydantic import BaseModel


class Parent(BaseModel):
    id: int


class Child(BaseModel):
    name: str
    parent: Parent


ChildDTO1 = PydanticDTO[Annotated[Child, DTOConfig(exclude={"name"})]]


class ChildDTO2(PydanticDTO[Child]):
    config = DTOConfig(exclude={"name"})


@post(path="/child1", dto=ChildDTO1, return_dto=None)
async def post_child1(data: Child) -> Child:
    return data


@post(path="/child2", dto=ChildDTO2, return_dto=None)
async def post_child2(data: Child) -> Child:
    return data


app = Litestar(route_handlers=[post_child1, post_child2])
```


### Steps to reproduce

```bash
1. Start the server
2. Run `curl -X POST http://localhost:8508/child1 -d '{"parent": {"id": 1}}'` (same with `/child2`)
```


### Screenshots

```bash
"![SCREENSHOT_DESCRIPTION](SCREENSHOT_LINK.png)"
```


### Logs

_No response_

### Litestar Version

2.4.3

### Platform

- [ ] Linux
- [ ] Mac
- [ ] Windows
- [ ] Other (Please specify in the description above)

<!-- POLAR PLEDGE BADGE START -->
---
> [!NOTE]  
> While we are open for sponsoring on [GitHub Sponsors](https://github.com/sponsors/litestar-org/) and 
> [OpenCollective](https://opencollective.com/litestar), we also utilize [Polar.sh](https://polar.sh/) to engage in pledge-based sponsorship.
>
> Check out all issues funded or available for funding [on our Polar.sh dashboard](https://polar.sh/litestar-org)
> * If you would like to see an issue prioritized, make a pledge towards it!
> * We receive the pledge once the issue is completed & verified
> * This, along with engagement in the community, helps us know which features are a priority to our users.

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



Discussion:
 Thanks for opening @gsakkis!

Resolving patch:
diff --git a/litestar/middleware/exceptions/_debug_response.py b/litestar/middleware/exceptions/_debug_response.py
--- a/litestar/middleware/exceptions/_debug_response.py
+++ b/litestar/middleware/exceptions/_debug_response.py
@@ -26,6 +26,7 @@
     from inspect import FrameInfo
 
     from litestar.connection import Request
+    from litestar.types import TypeEncodersMap
 
 tpl_dir = Path(__file__).parent / "templates"
 
@@ -191,4 +192,19 @@ def create_debug_response(request: Request, exc: Exception) -> Response:
         content = create_plain_text_response_content(exc)
         media_type = MediaType.TEXT
 
-    return Response(content=content, media_type=media_type, status_code=HTTP_500_INTERNAL_SERVER_ERROR)
+    return Response(
+        content=content,
+        media_type=media_type,
+        status_code=HTTP_500_INTERNAL_SERVER_ERROR,
+        type_encoders=_get_type_encoders_for_request(request) if request is not None else None,
+    )
+
+
+def _get_type_encoders_for_request(request: Request) -> TypeEncodersMap | None:
+    try:
+        return request.route_handler.resolve_type_encoders()
+    # we might be in a 404, or before we could resolve the handler, so this
+    # could potentially error out. In this case we fall back on the application
+    # type encoders
+    except (KeyError, AttributeError):
+        return request.app.type_encoders
diff --git a/litestar/middleware/exceptions/middleware.py b/litestar/middleware/exceptions/middleware.py
--- a/litestar/middleware/exceptions/middleware.py
+++ b/litestar/middleware/exceptions/middleware.py
@@ -11,7 +11,7 @@
 from litestar.enums import MediaType, ScopeType
 from litestar.exceptions import WebSocketException
 from litestar.middleware.cors import CORSMiddleware
-from litestar.middleware.exceptions._debug_response import create_debug_response
+from litestar.middleware.exceptions._debug_response import _get_type_encoders_for_request, create_debug_response
 from litestar.serialization import encode_json
 from litestar.status_codes import HTTP_500_INTERNAL_SERVER_ERROR
 from litestar.utils.deprecation import warn_deprecation
@@ -85,7 +85,7 @@ class ExceptionResponseContent:
     extra: dict[str, Any] | list[Any] | None = field(default=None)
     """An extra mapping to attach to the exception."""
 
-    def to_response(self) -> Response:
+    def to_response(self, request: Request | None = None) -> Response:
         """Create a response from the model attributes.
 
         Returns:
@@ -103,6 +103,7 @@ def to_response(self) -> Response:
             headers=self.headers,
             status_code=self.status_code,
             media_type=self.media_type,
+            type_encoders=_get_type_encoders_for_request(request) if request is not None else None,
         )
 
 
@@ -139,7 +140,7 @@ def create_exception_response(request: Request[Any, Any, Any], exc: Exception) -
         extra=getattr(exc, "extra", None),
         media_type=media_type,
     )
-    return content.to_response()
+    return content.to_response(request=request)
 
 
 class ExceptionHandlerMiddleware:


Test patch:
diff --git a/tests/unit/test_middleware/test_exception_handler_middleware.py b/tests/unit/test_middleware/test_exception_handler_middleware.py
--- a/tests/unit/test_middleware/test_exception_handler_middleware.py
+++ b/tests/unit/test_middleware/test_exception_handler_middleware.py
@@ -362,3 +362,20 @@ def method(self) -> None:
     if exc is not None and exc.__traceback__ is not None:
         frame = getinnerframes(exc.__traceback__, 2)[-1]
         assert get_symbol_name(frame) == "Test.method"
+
+
+def test_serialize_custom_types() -> None:
+    # ensure type encoders are passed down to the created response so custom types that
+    # might end up as part of a ValidationException are handled properly
+    # https://github.com/litestar-org/litestar/issues/2867
+    class Foo:
+        def __init__(self, value: str) -> None:
+            self.value = value
+
+    @get()
+    def handler() -> None:
+        raise ValidationException(extra={"foo": Foo("bar")})
+
+    with create_test_client([handler], type_encoders={Foo: lambda f: f.value}) as client:
+        res = client.get("/")
+        assert res.json()["extra"] == {"foo": "bar"}