Repository: litestar-org/litestar

Issue:
Bug: Pydantic types that cannot be instantiated - deserialization
### Description

When deserializing object, Litestar checks if a given `value` matches a given `target_type`. Additional `type_decoders` can be provided to convert a type given after deserialization to the expected type.

The problem is - when using Pydantic models and PydanticDTO's - Pydantic has some types that cannot be instantiated and are really not supposed to be used elsewhere other than model declaration - for example `EmailStr`.

When deserializing an object like:
```
{
  "email": "user@example.com"
}
```
using a DTO given in the MCVE -> the expected type of `email` during runtime is `EmailStr` (see screenshot). Msgspec after deserialization returns a `str` so a decoder is needed. However, [there is no way to instantiate `EmailStr`](https://github.com/pydantic/pydantic/blob/main/pydantic/networks.py#L171) - it is only meant to be used as a type annotation for a Pydantic model. 
`cast` doesn't change the type at runtime it is only useful for type checkers. So such decoder is impossible.

The solution should be changing the `target_type` to `str` in the `default_deserializer` function - [line 91 of `litestar/serialization/msgspec_hooks.py`](https://github.com/litestar-org/litestar/blob/main/litestar/serialization/msgspec_hooks.py#L91) and consequently in the call to [`msgspec.json.decode` at line 187 of the same file](https://github.com/litestar-org/litestar/blob/main/litestar/serialization/msgspec_hooks.py#L187). Internally by Pydantic, they're treated as a `str` after validation is performed.

Which should be handled by the PydanticDTO. Also there's more types in Pydantic that cannot be instantiated like `ImportStr` and maybe others. The target type for these should always be they're naive counterpart.

### URL to code causing the issue

_No response_

### MCVE

```python
import logging

from litestar import Litestar, post
from litestar.contrib.pydantic import PydanticDTO
from litestar.dto import DTOConfig
from pydantic import BaseModel, EmailStr

from typing import cast


class EmailModel(BaseModel):
    email: EmailStr


class EmailDTO(PydanticDTO[EmailModel]):
    config = DTOConfig()


@post("/email", dto=EmailDTO)
async def accept_email(data: EmailModel) -> None:
    logging.info(data)


def is_emailstr_type(obj_type: type[EmailStr]) -> bool:
    return obj_type is EmailStr


def emailstr_decoder(obj_type: type[EmailStr], value: str) -> EmailStr:
    # How?
    
    return cast(EmailStr, value)


app = Litestar(
    route_handlers=[accept_email], type_decoders=[(is_emailstr_type, emailstr_decoder)]
)
```


### Steps to reproduce

1. Run the Litestar app from the MCVE
2. Send POST request with the body of `{"email": "user@example.com"}` to `/email`
3. `msgspec.ValidationError` occurs
4. Also when removing `type_decoders`, a similar error appears


### Screenshots

![Expected type cannot be instantiated](https://i.imgur.com/5wW0979.png)


### Logs
<details>
<summary>Full traceback</summary>

```
INFO:     127.0.0.1:58390 - "POST /email HTTP/1.1" 400 Bad Request
ERROR - 2023-09-22 21:00:43,846 - litestar - config - exception raised on http connection to route /email

Traceback (most recent call last):
Traceback (most recent call last):
  File "/home/geeshta/prog/litestar/pydbug/.venv/lib/python3.11/site-packages/litestar/serialization/msgspec_hooks.py", line 187, in decode_json
    return msgspec.json.decode(
           ^^^^^^^^^^^^^^^^^^^^
msgspec.ValidationError: decoding to str: need a bytes-like object, type found - at `$.email`

The above exception was the direct cause of the following exception:

Traceback (most recent call last):
  File "/home/geeshta/prog/litestar/pydbug/.venv/lib/python3.11/site-packages/litestar/routes/http.py", line 184, in _get_response_data
    kwargs["data"] = await kwargs["data"]
                     ^^^^^^^^^^^^^^^^^^^^
  File "/home/geeshta/prog/litestar/pydbug/.venv/lib/python3.11/site-packages/litestar/_kwargs/extractors.py", line 427, in dto_extractor
    return data_dto(connection).decode_bytes(body)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/geeshta/prog/litestar/pydbug/.venv/lib/python3.11/site-packages/litestar/dto/base_dto.py", line 96, in decode_bytes
    return backend.populate_data_from_raw(value, self.asgi_connection)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/geeshta/prog/litestar/pydbug/.venv/lib/python3.11/site-packages/litestar/dto/_backend.py", line 299, in populate_data_from_raw
    source_data=self.parse_raw(raw, asgi_connection),
                ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/geeshta/prog/litestar/pydbug/.venv/lib/python3.11/site-packages/litestar/dto/_backend.py", line 206, in parse_raw
    result = decode_json(value=raw, target_type=self.annotation, type_decoders=type_decoders)
             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/geeshta/prog/litestar/pydbug/.venv/lib/python3.11/site-packages/litestar/serialization/msgspec_hooks.py", line 191, in decode_json
    raise SerializationException(str(msgspec_error)) from msgspec_error
litestar.exceptions.base_exceptions.SerializationException: decoding to str: need a bytes-like object, type found - at `$.email`

The above exception was the direct cause of the following exception:

Traceback (most recent call last):
  File "/home/geeshta/prog/litestar/pydbug/.venv/lib/python3.11/site-packages/litestar/middleware/exceptions/middleware.py", line 191, in __call__
    await self.app(scope, receive, send)
  File "/home/geeshta/prog/litestar/pydbug/.venv/lib/python3.11/site-packages/litestar/routes/http.py", line 79, in handle
    response = await self._get_response_for_request(
               ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/geeshta/prog/litestar/pydbug/.venv/lib/python3.11/site-packages/litestar/routes/http.py", line 131, in _get_response_for_request
    response = await self._call_handler_function(
               ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/geeshta/prog/litestar/pydbug/.venv/lib/python3.11/site-packages/litestar/routes/http.py", line 160, in _call_handler_function
    response_data, cleanup_group = await self._get_response_data(
                                   ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/geeshta/prog/litestar/pydbug/.venv/lib/python3.11/site-packages/litestar/routes/http.py", line 186, in _get_response_data
    raise ClientException(str(e)) from e
litestar.exceptions.http_exceptions.ClientException: 400: decoding to str: need a bytes-like object, type found - at `$.email`
Traceback (most recent call last):
  File "/home/geeshta/prog/litestar/pydbug/.venv/lib/python3.11/site-packages/litestar/serialization/msgspec_hooks.py", line 113, in default_deserializer
    return decoder(target_type, value)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^
TypeError: decoding to str: need a bytes-like object, type found

The above exception was the direct cause of the following exception:

Traceback (most recent call last):
  File "/home/geeshta/prog/litestar/pydbug/.venv/lib/python3.11/site-packages/litestar/serialization/msgspec_hooks.py", line 187, in decode_json
    return msgspec.json.decode(
           ^^^^^^^^^^^^^^^^^^^^
msgspec.ValidationError: decoding to str: need a bytes-like object, type found - at `$.email`

The above exception was the direct cause of the following exception:

Traceback (most recent call last):
  File "/home/geeshta/prog/litestar/pydbug/.venv/lib/python3.11/site-packages/litestar/routes/http.py", line 184, in _get_response_data
    kwargs["data"] = await kwargs["data"]
                     ^^^^^^^^^^^^^^^^^^^^
  File "/home/geeshta/prog/litestar/pydbug/.venv/lib/python3.11/site-packages/litestar/_kwargs/extractors.py", line 427, in dto_extractor
    return data_dto(connection).decode_bytes(body)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/geeshta/prog/litestar/pydbug/.venv/lib/python3.11/site-packages/litestar/dto/base_dto.py", line 96, in decode_bytes
    return backend.populate_data_from_raw(value, self.asgi_connection)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/geeshta/prog/litestar/pydbug/.venv/lib/python3.11/site-packages/litestar/dto/_backend.py", line 299, in populate_data_from_raw
    source_data=self.parse_raw(raw, asgi_connection),
                ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/geeshta/prog/litestar/pydbug/.venv/lib/python3.11/site-packages/litestar/dto/_backend.py", line 206, in parse_raw
    result = decode_json(value=raw, target_type=self.annotation, type_decoders=type_decoders)
             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/geeshta/prog/litestar/pydbug/.venv/lib/python3.11/site-packages/litestar/serialization/msgspec_hooks.py", line 191, in decode_json
    raise SerializationException(str(msgspec_error)) from msgspec_error
litestar.exceptions.base_exceptions.SerializationException: decoding to str: need a bytes-like object, type found - at `$.email`

The above exception was the direct cause of the following exception:

Traceback (most recent call last):
  File "/home/geeshta/prog/litestar/pydbug/.venv/lib/python3.11/site-packages/litestar/middleware/exceptions/middleware.py", line 191, in __call__
    await self.app(scope, receive, send)
  File "/home/geeshta/prog/litestar/pydbug/.venv/lib/python3.11/site-packages/litestar/routes/http.py", line 79, in handle
    response = await self._get_response_for_request(
               ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/geeshta/prog/litestar/pydbug/.venv/lib/python3.11/site-packages/litestar/routes/http.py", line 131, in _get_response_for_request
    response = await self._call_handler_function(
               ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/geeshta/prog/litestar/pydbug/.venv/lib/python3.11/site-packages/litestar/routes/http.py", line 160, in _call_handler_function
    response_data, cleanup_group = await self._get_response_data(
                                   ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/geeshta/prog/litestar/pydbug/.venv/lib/python3.11/site-packages/litestar/routes/http.py", line 186, in _get_response_data
    raise ClientException(str(e)) from e
litestar.exceptions.http_exceptions.ClientException: 400: decoding to str: need a bytes-like object, type found - at `$.email`
```
</details>
### Litestar Version

2.0.1

### Platform

- [X] Linux
- [ ] Mac
- [ ] Windows
- [ ] Other (Please specify in the description above)

<!-- 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/2334">
<picture>
  <source media="(prefers-color-scheme: dark)" srcset="https://polar.sh/api/github/litestar-org/litestar/issues/2334/pledge.svg?darkmode=1">
  <img alt="Fund with Polar" src="https://polar.sh/api/github/litestar-org/litestar/issues/2334/pledge.svg">
</picture>
</a>
<!-- POLAR PLEDGE BADGE END -->

Bug: RapiDoc and Stoplight Elements fail when used as root schema
### Description

Setting `root_schema_site="rapidoc"` or `root_schema_site="elements"` in the `OpenAPIConfig` results in an error when visiting `/schema`. Both `/schema/rapidoc` and `/schema/elements` work as expected.

My guess is that this might be related to the fact that both RapiDoc and StopLight elements [are configured with `spec-url`](https://github.com/litestar-org/litestar/blob/747fb90772721e7867f2e1ff2da6636f8442a6fb/litestar/openapi/controller.py#L494) whereas Swagger UI and Redoc HTML has the OpenAPI JSON inlined via a call to `OpenAPIController._get_schema_as_json`.

### MCVE

```python
from litestar import Litestar, get
from litestar.openapi import OpenAPIConfig


@get("/")
async def hello_world() -> str:
    return "Hello world!"


app = Litestar(
    route_handlers=[hello_world],
    openapi_config=OpenAPIConfig(
        title="Test app", version="0.0.1", root_schema_site="rapidoc"
    ),
)
```


### Steps to reproduce

```text
1. Run app with `litestar run`
2. Visit `localhost:8000/schema` in your browser.
```


### Screenshots

<img width="859" alt="image" src="https://github.com/litestar-org/litestar/assets/15220906/c962e9b0-acba-40bd-bf29-e0603756d91c">

### Litestar Version

2.5.0

### Platform

- [X] Linux
- [X] Mac
- [ ] Windows
- [ ] Other (Please specify in the description above)


Discussion:
Schema validation of Pydantic and msgspec can't be used transparently/interchangeably: like pointed out by @geeshta, the type ```EmailStr``` is used by Pydantic for validation, effectively processing ```str.``` However, this type cannot be used in the schema validation of msgspec, as it expects an instance of ```EmailStr```, which is not feasible.

As far as I understand, currently in Litestar msgspec always does schema validation, irrespective if there's a validation later on by a plugin, e.g. Pydantic?

The problem is that when a PydanticDTO is used, BOTH Pydantic and msgspec do a schema validation, which poses a problem for types like ```EmailStr```. In this case, msgspec shouldn't do any schema validation. Schema validation by msgspec can be disabled by removing the ```type``` argument from  ```msgspec.json.decode``` in ```msgspec_hooks.py```. 

I think the behaviour should be that msgspec only does schema validation if there's no other plugin doing that instead. Would you agree? 
When the msgspec transfer models are created for the pydantic type, the type on the transfer model should be annotated `str`, and the validation to email string should only happen when the pydantic model is instantiated with the data that has been decoded and validated by msgspec.

E.g., pydantic model:

```py
class WithEmail(BaseModel):
    email: EmailStr
```

Transfer model produced by DTO should be:

```py
class WithEmailTransfer(msgspec.Struct):
    email: str
```

Given:
- `{"email": 1}` - msgspec should fail this on decoding
- `{"email": "abc"}` - pydantic should fail this on model instantiation
- `{"email": "foo@bar.com"}` - should pass both

The down-typing from `EmailStr` to `str` should be able to be handled in https://github.com/litestar-org/litestar/blob/7c3d24f9b70731df372b29bff9eb5cf359055b3a/litestar/contrib/pydantic/pydantic_dto_factory.py#L54-L56
@peterschutt thanks for the hints!
I just made a pull-request
Just ran into this, any update or workaround?

EDIT: Not a general fix but at least for EmailStr this workaround seems to work for me:
```python
from pydantic import AfterValidator, validate_email

class EmailModel(BaseModel):
    email: Annotated[str, AfterValidator(lambda v: validate_email(v)[1])]
```

Can you update to the latest version (`pip install -U litestar==2.5.3`) and confirm this issue persists?
Upgraded, and unfortunately the issue persists with `2.5.3` installed.
Using the provided MCVE I don't seem to be able to reproduce :\
```
~ via 🐋 colima via  pyenv
➜ ntp -v 3047
Directory /tmp/testing/3047 created and switched to.
Virtual environment created and activated.
➜ vim mcve.py
➜ pip install litestar[standard] &&  litestar run --reload --reload-dir . --debug
➜ browse 127.0.0.1/schema
```
result:
<img width="1201" alt="image" src="https://github.com/litestar-org/litestar/assets/45884264/a39060ad-0e03-4d95-8172-791564cc71bd">
and same for Stoplight.

tested on release in 2.5 - 2.6.

Am i doing something dumb here?
Wow, that's really strange... I just tested again at my end with a clean virtual environment and the above example and saw the same issue.

```
pip install -U "litestar[standard]"
litestar run --reload --reload-dir . --debug
open http://127.0.0.1:8000/schema
```

The request is being received by the backend as expected

```
INFO:     127.0.0.1:55586 - "GET /schema HTTP/1.1" 200 OK
INFO:     127.0.0.1:55586 - "GET /openapi.json HTTP/1.1" 404 Not Found
ERROR - 2024-02-11 10:46:25,461 - litestar - config - exception raised on http connection to route /openapi.json
```

Issue does appear to be `/openapi.json` route as if I visit `/schema/rapidoc` instead I get

```
INFO:     127.0.0.1:55628 - "GET /schema/rapidoc HTTP/1.1" 200 OK
INFO:     127.0.0.1:55628 - "GET /schema/openapi.json HTTP/1.1" 200 OK
```

I tried private windows and force refreshing the page but always the same result

<img width="864" alt="image" src="https://github.com/litestar-org/litestar/assets/15220906/6aae1341-07b9-453c-9883-46ed004bc1be">

In case it helps I tested just now with Python 3.11.4 and this is the result of `pip freeze`.
[pip-freeze.txt](https://github.com/litestar-org/litestar/files/14232018/pip-freeze.txt)

No idea what's going on but I'm also very open to the possibility that I'm doing something dumb on this end 😅 
@tcbegley I also can't reproduce this. Are you sure that you're running the correct application file?
Hey @provinzkraut, thanks for also looking into this.

I noticed from @JacobCoffee's screenshot that he has `/schema/` in his address bar whereas I have `/schema`. Sure enough if I visit `/schema/` then I don't get the error. By contrast `/schema/rapidoc` works but `/schema/rapidoc/` does not. I guess an issue with relative paths resolving to the wrong place? Ditto for StopLight Elements. Are you able to reproduce by visiting `/schema`?

To answer your original question, definitely running the right application file. To double check I added

```python
if __name__ == "__main__":
    import uvicorn
    uvicorn.run(app)
```

to the bottom of the above example and ran `python app.py` and observed the same result.
Yes, I was able to reproduce this with the help of @Alc-Alc who came to the same conclusion as you.

I'm not yet sure if this is an upstream issue or a bug on our side, but he provided some additional insight:

```
INFO:     127.0.0.1:49743 - "GET /schema HTTP/1.1" 200 OK
INFO:     127.0.0.1:49743 - "GET /openapi.json HTTP/1.1" 404 Not Found
INFO:     127.0.0.1:49743 - "GET /schema HTTP/1.1" 200 OK
INFO:     127.0.0.1:49743 - "GET /schema/openapi.json HTTP/1.1" 200 OK
```

these are the paths requested with/without the trailing slash, so something in there is not building them correctly.

Resolving patch:
diff --git a/litestar/app.py b/litestar/app.py
--- a/litestar/app.py
+++ b/litestar/app.py
@@ -198,6 +198,7 @@ def __init__(
         openapi_config: OpenAPIConfig | None = DEFAULT_OPENAPI_CONFIG,
         opt: Mapping[str, Any] | None = None,
         parameters: ParametersMap | None = None,
+        path: str | None = None,
         plugins: Sequence[PluginProtocol] | None = None,
         request_class: type[Request] | None = None,
         response_cache_config: ResponseCacheConfig | None = None,
@@ -277,6 +278,10 @@ def __init__(
                 :class:`ASGI Scope <.types.Scope>`.
             parameters: A mapping of :class:`Parameter <.params.Parameter>` definitions available to all application
                 paths.
+            path: A path fragment that is prefixed to all route handlers, controllers and routers associated
+                with the application instance.
+
+                .. versionadded:: 2.8.0
             pdb_on_exception: Drop into the PDB when an exception occurs.
             plugins: Sequence of plugins.
             request_class: An optional subclass of :class:`Request <.connection.Request>` to use for http connections.
@@ -350,6 +355,7 @@ def __init__(
             on_startup=list(on_startup or []),
             openapi_config=openapi_config,
             opt=dict(opt or {}),
+            path=path or "",
             parameters=parameters or {},
             pdb_on_exception=pdb_on_exception,
             plugins=self._get_default_plugins(list(plugins or [])),
@@ -455,7 +461,7 @@ def __init__(
             middleware=config.middleware,
             opt=config.opt,
             parameters=config.parameters,
-            path="",
+            path=config.path,
             request_class=self.request_class,
             response_class=config.response_class,
             response_cookies=config.response_cookies,
diff --git a/litestar/config/app.py b/litestar/config/app.py
--- a/litestar/config/app.py
+++ b/litestar/config/app.py
@@ -151,6 +151,12 @@ class AppConfig:
     """
     parameters: ParametersMap = field(default_factory=dict)
     """A mapping of :class:`Parameter <.params.Parameter>` definitions available to all application paths."""
+    path: str = field(default="")
+    """A base path that prefixed to all route handlers, controllers and routers associated with the
+    application instance.
+
+    .. versionadded:: 2.8.0
+    """
     pdb_on_exception: bool = field(default=False)
     """Drop into the PDB on an exception"""
     plugins: list[PluginProtocol] = field(default_factory=list)


Test patch:
diff --git a/litestar/testing/helpers.py b/litestar/testing/helpers.py
--- a/litestar/testing/helpers.py
+++ b/litestar/testing/helpers.py
@@ -86,6 +86,7 @@ def create_test_client(
     openapi_config: OpenAPIConfig | None = DEFAULT_OPENAPI_CONFIG,
     opt: Mapping[str, Any] | None = None,
     parameters: ParametersMap | None = None,
+    path: str | None = None,
     plugins: Sequence[PluginProtocol] | None = None,
     lifespan: list[Callable[[Litestar], AbstractAsyncContextManager] | AbstractAsyncContextManager] | None = None,
     raise_server_exceptions: bool = True,
@@ -201,6 +202,10 @@ def test_my_handler() -> None:
             :class:`ASGI Scope <.types.Scope>`.
         parameters: A mapping of :class:`Parameter <.params.Parameter>` definitions available to all application
             paths.
+        path: A path fragment that is prefixed to all route handlers, controllers and routers associated
+            with the application instance.
+
+            .. versionadded:: 2.8.0
         pdb_on_exception: Drop into the PDB when an exception occurs.
         plugins: Sequence of plugins.
         request_class: An optional subclass of :class:`Request <.connection.Request>` to use for http connections.
@@ -273,6 +278,7 @@ def test_my_handler() -> None:
         openapi_config=openapi_config,
         opt=opt,
         parameters=parameters,
+        path=path,
         pdb_on_exception=pdb_on_exception,
         plugins=plugins,
         request_class=request_class,
@@ -343,6 +349,7 @@ def create_async_test_client(
     opt: Mapping[str, Any] | None = None,
     parameters: ParametersMap | None = None,
     pdb_on_exception: bool | None = None,
+    path: str | None = None,
     plugins: Sequence[PluginProtocol] | None = None,
     raise_server_exceptions: bool = True,
     request_class: type[Request] | None = None,
@@ -456,6 +463,10 @@ async def test_my_handler() -> None:
             :class:`ASGI Scope <.types.Scope>`.
         parameters: A mapping of :class:`Parameter <.params.Parameter>` definitions available to all application
             paths.
+        path: A path fragment that is prefixed to all route handlers, controllers and routers associated
+            with the application instance.
+
+            .. versionadded:: 2.8.0
         pdb_on_exception: Drop into the PDB when an exception occurs.
         plugins: Sequence of plugins.
         request_class: An optional subclass of :class:`Request <.connection.Request>` to use for http connections.
@@ -527,6 +538,7 @@ async def test_my_handler() -> None:
         openapi_config=openapi_config,
         opt=opt,
         parameters=parameters,
+        path=path,
         pdb_on_exception=pdb_on_exception,
         plugins=plugins,
         request_class=request_class,
diff --git a/tests/unit/test_app.py b/tests/unit/test_app.py
--- a/tests/unit/test_app.py
+++ b/tests/unit/test_app.py
@@ -450,3 +450,12 @@ async def hook_b(app: Litestar) -> None:
 def test_use_dto_codegen_feature_flag_warns() -> None:
     with pytest.warns(LitestarWarning, match="Use of redundant experimental feature flag DTO_CODEGEN"):
         Litestar(experimental_features=[ExperimentalFeatures.DTO_CODEGEN])
+
+
+def test_using_custom_path_parameter() -> None:
+    @get()
+    def my_route_handler() -> None: ...
+
+    with create_test_client(my_route_handler, path="/abc") as client:
+        response = client.get("/abc")
+        assert response.status_code == HTTP_200_OK