Repository: litestar-org/litestar

Issue:
Bug: late failure where `DTOData` is used without a DTO
### Description

Identified in this issue in fullstack: https://github.com/litestar-org/litestar-fullstack/issues/105#issue-2012835424

Where a `DTOData` annotation is applied to a route without a DTO, we should raise a configuration error.

The linked issue reported a cryptic error during typescript generation, but it also appears to fail with a 400 at request time upon POST request.

We should detect this early and raise an error.

### MCVE

```python
"""Minimal Litestar application."""
from __future__ import annotations

from dataclasses import dataclass
from typing import TYPE_CHECKING

from litestar import Litestar, post

if TYPE_CHECKING:
    from litestar.dto import DTOData

__all__ = ("async_hello_world",)


@dataclass
class Model:
    """Example dataclass model."""

    hello: str


@post("/")
async def async_hello_world(data: DTOData[Model]) -> Model:
    """Route Handler that outputs hello world."""
    return data.create_instance()


app = Litestar(route_handlers=[async_hello_world])
```

response from post request

```json
{
  "status_code": 400,
  "detail": "Validation failed for POST http://localhost:8000/",
  "extra": [
    {
      "message": "Subscripted generics cannot be used with class and instance checks",
      "key": "data",
      "source": "body"
    }
  ]
}
```
### Litestar Version

2.4

### Platform

- [X] 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 Litestar 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/2779">
<picture>
  <source media="(prefers-color-scheme: dark)" srcset="https://polar.sh/api/github/litestar-org/litestar/issues/2779/pledge.svg?darkmode=1">
  <img alt="Fund with Polar" src="https://polar.sh/api/github/litestar-org/litestar/issues/2779/pledge.svg">
</picture>
</a>
<!-- POLAR PLEDGE BADGE END -->



Discussion:

Resolving patch:
diff --git a/litestar/handlers/base.py b/litestar/handlers/base.py
--- a/litestar/handlers/base.py
+++ b/litestar/handlers/base.py
@@ -7,6 +7,7 @@
 from litestar._signature import SignatureModel
 from litestar.config.app import ExperimentalFeatures
 from litestar.di import Provide
+from litestar.dto import DTOData
 from litestar.exceptions import ImproperlyConfiguredException
 from litestar.serialization import default_deserializer, default_serializer
 from litestar.types import (
@@ -526,6 +527,15 @@ def on_registration(self, app: Litestar) -> None:
 
     def _validate_handler_function(self) -> None:
         """Validate the route handler function once set by inspecting its return annotations."""
+        if (
+            self.parsed_data_field is not None
+            and self.parsed_data_field.is_subclass_of(DTOData)
+            and not self.resolve_data_dto()
+        ):
+            raise ImproperlyConfiguredException(
+                f"Handler function {self.handler_name} has a data parameter that is a subclass of DTOData but no "
+                "DTO has been registered for it."
+            )
 
     def __str__(self) -> str:
         """Return a unique identifier for the route handler.


Test patch:
diff --git a/tests/unit/test_handlers/test_base_handlers/test_validations.py b/tests/unit/test_handlers/test_base_handlers/test_validations.py
--- a/tests/unit/test_handlers/test_base_handlers/test_validations.py
+++ b/tests/unit/test_handlers/test_base_handlers/test_validations.py
@@ -1,5 +1,9 @@
+from dataclasses import dataclass
+
 import pytest
 
+from litestar import Litestar, post
+from litestar.dto import DTOData
 from litestar.exceptions import ImproperlyConfiguredException
 from litestar.handlers.base import BaseRouteHandler
 
@@ -9,3 +13,19 @@ def test_raise_no_fn_validation() -> None:
 
     with pytest.raises(ImproperlyConfiguredException):
         handler.fn
+
+
+def test_dto_data_annotation_with_no_resolved_dto() -> None:
+    @dataclass
+    class Model:
+        """Example dataclass model."""
+
+        hello: str
+
+    @post("/")
+    async def async_hello_world(data: DTOData[Model]) -> Model:
+        """Route Handler that outputs hello world."""
+        return data.create_instance()
+
+    with pytest.raises(ImproperlyConfiguredException):
+        Litestar(route_handlers=[async_hello_world])