Repository: litestar-org/litestar

Issue:
Bug: CacheControlHeader not applying on create_static_files_router
### Description

When applying a `CacheControlHeader` to the [static files router](https://docs.litestar.dev/latest/reference/static_files.html#litestar.static_files.create_static_files_router), the cache headers are not applied to requests served from this router.

`CacheControlHeader` works correctly on regular routers.

### URL to code causing the issue

NA

### MCVE

```python
from litestar import Litestar, Router, get
from litestar.response import File
from litestar.static_files import create_static_files_router
from litestar.datastructures.headers import CacheControlHeader
import uvicorn

def create_static_router() -> Router:
    return create_static_files_router(
        path="/static",
        directories=["./static"],
        name="static",
        cache_control=CacheControlHeader(max_age=3600)
    )

def create_direct_route_handler() -> Router:
    @get("/example.txt")
    async def example_txt() -> File:
        return File("static/example.txt", content_disposition_type="inline", media_type="text/plain", filename="example.txt")
    
    return Router(
        path="/static",
        cache_control=CacheControlHeader(max_age=3600),
        route_handlers=[example_txt],
    )

app = Litestar(route_handlers=[create_static_router()]) # <- this does not work as expected
#app = Litestar(route_handlers=[create_direct_route_handler()]) # <- this works as expected

if __name__ == "__main__":
    uvicorn.run(
        app,
    )

# Expects a "static" folder in the same directory as this file and an example.txt file in the "static" folder
```


### Steps to reproduce

```bash
1. Put the MVCE beside a folder called static with example.txt inside it
2. Run the server and make a request to /static/example.txt
3. Observe that there is no cache header applied to the request
4. Change the code to the create_direct_route_handler() version and repeat
5. Observe the cache header applied as expected
```


### Screenshots

```bash
NA
```


### Logs

_No response_

### Litestar Version

2.6.1

### Platform

- [ ] Linux
- [ ] Mac
- [X] 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/3129">
<picture>
  <source media="(prefers-color-scheme: dark)" srcset="https://polar.sh/api/github/litestar-org/litestar/issues/3129/pledge.svg?darkmode=1">
  <img alt="Fund with Polar" src="https://polar.sh/api/github/litestar-org/litestar/issues/3129/pledge.svg">
</picture>
</a>
<!-- POLAR PLEDGE BADGE END -->



Discussion:

Resolving patch:
diff --git a/litestar/static_files/base.py b/litestar/static_files/base.py
--- a/litestar/static_files/base.py
+++ b/litestar/static_files/base.py
@@ -22,7 +22,7 @@
 class StaticFiles:
     """ASGI App that handles file sending."""
 
-    __slots__ = ("is_html_mode", "directories", "adapter", "send_as_attachment")
+    __slots__ = ("is_html_mode", "directories", "adapter", "send_as_attachment", "headers")
 
     def __init__(
         self,
@@ -31,6 +31,7 @@ def __init__(
         file_system: FileSystemProtocol,
         send_as_attachment: bool = False,
         resolve_symlinks: bool = True,
+        headers: dict[str, str] | None = None,
     ) -> None:
         """Initialize the Application.
 
@@ -41,11 +42,13 @@ def __init__(
             send_as_attachment: Whether to send the file with a ``content-disposition`` header of
              ``attachment`` or ``inline``
             resolve_symlinks: Resolve symlinks to the directories
+            headers: Headers that will be sent with every response.
         """
         self.adapter = FileSystemAdapter(file_system)
         self.directories = tuple(Path(p).resolve() if resolve_symlinks else Path(p) for p in directories)
         self.is_html_mode = is_html_mode
         self.send_as_attachment = send_as_attachment
+        self.headers = headers
 
     async def get_fs_info(
         self, directories: Sequence[PathType], file_path: PathType
@@ -111,6 +114,7 @@ async def handle(self, path: str, is_head_response: bool) -> ASGIFileResponse:
                 filename=filename,
                 content_disposition_type=content_disposition_type,
                 is_head_response=is_head_response,
+                headers=self.headers,
             )
 
         if self.is_html_mode:
@@ -129,6 +133,7 @@ async def handle(self, path: str, is_head_response: bool) -> ASGIFileResponse:
                     status_code=HTTP_404_NOT_FOUND,
                     content_disposition_type=content_disposition_type,
                     is_head_response=is_head_response,
+                    headers=self.headers,
                 )
 
         raise NotFoundException(
diff --git a/litestar/static_files/config.py b/litestar/static_files/config.py
--- a/litestar/static_files/config.py
+++ b/litestar/static_files/config.py
@@ -162,12 +162,17 @@ def create_static_files_router(
     _validate_config(path=path, directories=directories, file_system=file_system)
     path = normalize_path(path)
 
+    headers = None
+    if cache_control:
+        headers = {cache_control.HEADER_NAME: cache_control.to_header()}
+
     static_files = StaticFiles(
         is_html_mode=html_mode,
         directories=directories,
         file_system=file_system,
         send_as_attachment=send_as_attachment,
         resolve_symlinks=resolve_symlinks,
+        headers=headers,
     )
 
     @get("{file_path:path}", name=name)


Test patch:
diff --git a/tests/unit/test_static_files/test_create_static_router.py b/tests/unit/test_static_files/test_create_static_router.py
--- a/tests/unit/test_static_files/test_create_static_router.py
+++ b/tests/unit/test_static_files/test_create_static_router.py
@@ -1,4 +1,7 @@
-from typing import Any
+from pathlib import Path
+from typing import Any, Optional
+
+import pytest
 
 from litestar import Litestar, Request, Response, Router
 from litestar.connection import ASGIConnection
@@ -6,6 +9,8 @@
 from litestar.exceptions import ValidationException
 from litestar.handlers import BaseRouteHandler
 from litestar.static_files import create_static_files_router
+from litestar.status_codes import HTTP_200_OK
+from litestar.testing.helpers import create_test_client
 
 
 def test_route_reverse() -> None:
@@ -71,3 +76,21 @@ class MyRouter(Router):
 
     router = create_static_files_router("/", directories=["some"], router_class=MyRouter)
     assert isinstance(router, MyRouter)
+
+
+@pytest.mark.parametrize("cache_control", (None, CacheControlHeader(max_age=3600)))
+def test_cache_control(tmp_path: Path, cache_control: Optional[CacheControlHeader]) -> None:
+    static_dir = tmp_path / "foo"
+    static_dir.mkdir()
+    static_dir.joinpath("test.txt").write_text("hello")
+
+    router = create_static_files_router("/static", [static_dir], name="static", cache_control=cache_control)
+
+    with create_test_client([router]) as client:
+        response = client.get("static/test.txt")
+
+        assert response.status_code == HTTP_200_OK
+        if cache_control is not None:
+            assert response.headers["cache-control"] == cache_control.to_header()
+        else:
+            assert "cache-control" not in response.headers