Repository: litestar-org/litestar

Issue:
Weird Response when set both "Template and Redirect" on same route as return value type
### Discussed in https://github.com/orgs/litestar-org/discussions/1646

<div type='discussions-op-text'>

<sup>Originally posted by **4n1qz5skwv** May 10, 2023</sup>
```python
from pathlib import Path

from litestar import Litestar, Request, get, HttpMethod, route
from litestar.contrib.jinja import JinjaTemplateEngine
from litestar.response_containers import Template, Redirect
from litestar.template.config import TemplateConfig


@get("/home")
async def aaa() -> str:
    return 'home!'


@route(path="/some-path", http_method=[HttpMethod.GET, HttpMethod.POST])
async def bbb(request: Request) -> Template | Redirect:
    if request.method == "POST":
        # may be login and move to home
        return Redirect(path='/home', status_code=302)

    # show template on opening page
    return Template(name="abc.html")


app = Litestar(route_handlers=[aaa, bbb],
               template_config=TemplateConfig(directory=Path(__file__).parent, engine=JinjaTemplateEngine))

if __name__ == '__main__':
    import uvicorn

    uvicorn.run('0:app')
```

**curl**
```
Microsoft Windows [Version 10.0.19044.2846]
(c) Microsoft Corporation. All rights reserved.

C:\Users\User-PC>curl http://127.0.0.1:8000/some-path -X GET
{"name":"abc.html","context":{},"background":null,"headers":{},"cookies":[],"media_type":null,"encoding":"utf-8"}
C:\Users\User-PC>curl http://127.0.0.1:8000/some-path -X POST
{"path":"/home","background":null,"headers":{},"cookies":[],"media_type":null,"encoding":"utf-8","status_code":302}
C:\Users\User-PC>
```

**logs**
```
INFO:     Started server process [14912]
INFO:     Waiting for application startup.
INFO:     Application startup complete.
INFO:     Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit)
INFO:     127.0.0.1:52708 - "GET /some-path HTTP/1.1" 201 Created
INFO:     127.0.0.1:52709 - "POST /some-path HTTP/1.1" 201 Created
INFO:     Shutting down
INFO:     Waiting for application shutdown.
INFO:     Application shutdown complete.
INFO:     Finished server process [14912]

```

</div>
StaticFilesConfig and virtual directories
I'm trying to write a ``FileSystemProtocol`` to load files from the package data using [importlib_resources](https://importlib-resources.readthedocs.io/en/latest/using.html#). But because ``directories`` is defined as ``DirectoryPath``, pydantic checks if the given directories exist in the local filesystem. 

This is not generally true, especially in any kind of virtual filesystem (e.g. a zipped package). I think this condition should be relaxed to support virtual filesystems.

https://github.com/starlite-api/starlite/blob/9bb6dcd57c10a591377cf8e3a537e9292566d5b9/starlite/config/static_files.py#L32


Discussion:
This case is fairly straightforward if we consider that a union is a subtype of `X` if all inner types of the union are also subtypes of `X`, which on the surface seems OK to me.

Specific to this case, can we say that `Template | Redirect` is a subtype of `Response` because both `Template` and `Redirect` _are_ subtypes of `Response`.

This is relevant because:

 https://github.com/litestar-org/litestar/blob/f27b33c2142d64c3ec660b2f5f194803100550ae/litestar/handlers/http_handlers/base.py#L424-L427

At the moment, `return_type.is_subclass_of(Response)` is returning `False` - however, if that is allowed to return `True` than this "just works" because both `Template` and `Redirect` are serviced by the same response handler function.

It was a deliberate choice at the time to have any union type return `False` from `ParsedType.is_subclass_of()` with the intention of waiting for some real world cases to arrive - now we have one.

@litestar-org/maintainers WDT? Is `Union[SubclassOfX1, SubclassOfX2]` a subtype of `X`?
> This case is fairly straightforward if we consider that a union is a subtype of `X` if all inner types of the union are also subtypes of `X`, which on the surface seems OK to me.
> 
> Specific to this case, can we say that `Template | Redirect` is a subtype of `Response` because both `Template` and `Redirect` _are_ subtypes of `Response`.
> 
> This is relevant because:
> 
>  https://github.com/litestar-org/litestar/blob/f27b33c2142d64c3ec660b2f5f194803100550ae/litestar/handlers/http_handlers/base.py#L424-L427
> 
> At the moment, `return_type.is_subclass_of(Response)` is returning `False` - however, if that is allowed to return `True` than this "just works" because both `Template` and `Redirect` are serviced by the same response handler function.
> 
> It was a deliberate choice at the time to have any union type return `False` from `ParsedType.is_subclass_of()` with the intention of waiting for some real world cases to arrive - now we have one.
> 
> @litestar-org/maintainers WDT? Is `Union[SubclassOfX1, SubclassOfX2]` a subtype of `X`?

Sure
I agree. Do you want to submit a PR?

Resolving patch:
diff --git a/litestar/handlers/http_handlers/base.py b/litestar/handlers/http_handlers/base.py
--- a/litestar/handlers/http_handlers/base.py
+++ b/litestar/handlers/http_handlers/base.py
@@ -417,14 +417,12 @@ def get_response_handler(self, is_response_type_data: bool = False) -> Callable[
                 handler_return_type = before_request_handler.parsed_signature.return_type
                 if not handler_return_type.is_subclass_of((Empty, NoneType)):
                     return_annotation = handler_return_type.annotation
-            self._response_handler_mapping["response_type_handler"] = create_response_handler(
+            self._response_handler_mapping["response_type_handler"] = response_type_handler = create_response_handler(
                 cookies=cookies, after_request=after_request
             )
 
             if return_type.is_subclass_of(Response):
-                self._response_handler_mapping["default_handler"] = self._response_handler_mapping[
-                    "response_type_handler"
-                ]
+                self._response_handler_mapping["default_handler"] = response_type_handler
             elif return_type.is_subclass_of(ResponseContainer):
                 self._response_handler_mapping["default_handler"] = create_response_container_handler(
                     after_request=after_request,
diff --git a/litestar/typing.py b/litestar/typing.py
--- a/litestar/typing.py
+++ b/litestar/typing.py
@@ -124,8 +124,8 @@ def is_collection(self) -> bool:
     def is_subclass_of(self, cl: type[Any] | tuple[type[Any], ...]) -> bool:
         """Whether the annotation is a subclass of the given type.
 
-        Where ``self.annotation`` is a union type, this method will always return ``False``. While this is not
-        strictly correct, we intend on revisiting this once a concrete use-case is to hand.
+        Where ``self.annotation`` is a union type, this method will return ``True`` when all members of the union are
+        a subtype of ``cl``, otherwise, ``False``.
 
         Args:
             cl: The type to check, or tuple of types. Passed as 2nd argument to ``issubclass()``.
@@ -134,7 +134,11 @@ def is_subclass_of(self, cl: type[Any] | tuple[type[Any], ...]) -> bool:
             Whether the annotation is a subtype of the given type(s).
         """
         if self.origin:
+            if self.origin in UNION_TYPES:
+                return all(t.is_subclass_of(cl) for t in self.inner_types)
+
             return self.origin not in UNION_TYPES and issubclass(self.origin, cl)
+
         if self.annotation is AnyStr:
             return issubclass(str, cl) or issubclass(bytes, cl)
         return self.annotation is not Any and not self.is_type_var and issubclass(self.annotation, cl)


Test patch:
diff --git a/tests/utils/test_signature.py b/tests/utils/test_signature.py
--- a/tests/utils/test_signature.py
+++ b/tests/utils/test_signature.py
@@ -308,6 +308,7 @@ def test_parsed_type_is_subclass_of() -> None:
     assert ParsedType(List[int]).is_subclass_of(list) is True
     assert ParsedType(List[int]).is_subclass_of(int) is False
     assert ParsedType(Optional[int]).is_subclass_of(int) is False
+    assert ParsedType(Union[bool, int]).is_subclass_of(int) is True
 
 
 def test_parsed_type_has_inner_subclass_of() -> None: