Repository: litestar-org/litestar

Issue:
Bug: `FileResponse` doesn't set `content-encoding` headers on gzip/brotli compressed files
### Description

When using `StaticFilesConfig` to serve compressed files (think `styles.css.gz`), Litestar will happily serve the file and even properly infer the mimetype, but won't set the correct `content-encoding` header required by the browser.

I will provide a PR with a test.

### URL to code causing the issue

_No response_

### MCVE

_No response_

### Steps to reproduce

_No response_

### Screenshots

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


### Logs

_No response_

### Litestar Version

1.51.10

### Platform

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


Discussion:

Resolving patch:
diff --git a/litestar/response/file.py b/litestar/response/file.py
--- a/litestar/response/file.py
+++ b/litestar/response/file.py
@@ -2,7 +2,7 @@
 
 from email.utils import formatdate
 from inspect import iscoroutine
-from mimetypes import guess_type
+from mimetypes import encodings_map, guess_type
 from typing import TYPE_CHECKING, Any, AsyncGenerator, Coroutine, Literal, cast
 from urllib.parse import quote
 from zlib import adler32
@@ -28,6 +28,9 @@
     from litestar.types import HTTPResponseBodyEvent, PathType, Receive, ResponseCookies, Send
     from litestar.types.file_types import FileInfo, FileSystemProtocol
 
+# brotli not supported in 'mimetypes.encodings_map' until py 3.9.
+encodings_map[".br"] = "br"
+
 
 async def async_file_iterator(
     file_path: PathType, chunk_size: int, adapter: FileSystemAdapter
@@ -125,8 +128,11 @@ def __init__(
                 providing an :class:`os.stat_result`.
         """
         if not media_type:
-            mimetype, _ = guess_type(filename) if filename else (None, None)
+            mimetype, content_encoding = guess_type(filename) if filename else (None, None)
             media_type = mimetype or "application/octet-stream"
+            if content_encoding is not None:
+                headers = headers or {}
+                headers.update({"content-encoding": content_encoding})
 
         self.chunk_size = chunk_size
         self.content_disposition_type = content_disposition_type


Test patch:
diff --git a/tests/static_files/test_file_serving_resolution.py b/tests/static_files/test_file_serving_resolution.py
--- a/tests/static_files/test_file_serving_resolution.py
+++ b/tests/static_files/test_file_serving_resolution.py
@@ -1,7 +1,9 @@
+import gzip
 import mimetypes
 from pathlib import Path
 from typing import TYPE_CHECKING
 
+import brotli
 import pytest
 from fsspec.implementations.local import LocalFileSystem
 
@@ -161,6 +163,25 @@ def test_static_files_response_mimetype(tmpdir: "Path", extension: str) -> None:
         assert response.headers["content-type"].startswith(expected_mime_type)
 
 
+@pytest.mark.parametrize("extension", ["gz", "br"])
+def test_static_files_response_encoding(tmp_path: "Path", extension: str) -> None:
+    fn = f"test.js.{extension}"
+    path = tmp_path / fn
+    if extension == "br":
+        compressed_data = brotli.compress(b"content")
+    elif extension == "gz":
+        compressed_data = gzip.compress(b"content")
+    path.write_bytes(compressed_data)  # pyright: ignore
+    static_files_config = StaticFilesConfig(path="/static", directories=[tmp_path])
+    expected_encoding_type = mimetypes.guess_type(fn)[1]
+
+    with create_test_client([], static_files_config=[static_files_config]) as client:
+        response = client.get(f"/static/{fn}")
+        assert expected_encoding_type
+        assert response.status_code == HTTP_200_OK
+        assert response.headers["content-encoding"].startswith(expected_encoding_type)
+
+
 @pytest.mark.parametrize("send_as_attachment,disposition", [(True, "attachment"), (False, "inline")])
 def test_static_files_content_disposition(tmpdir: "Path", send_as_attachment: bool, disposition: str) -> None:
     path = tmpdir.mkdir("static_part").mkdir("static") / "test.txt"  # type: ignore