Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 37 additions & 8 deletions starlette/middleware/gzip.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,28 @@
from starlette.datastructures import Headers, MutableHeaders
from starlette.types import ASGIApp, Message, Receive, Scope, Send

DEFAULT_EXCLUDED_CONTENT_TYPES = ("text/event-stream",)
DEFAULT_EXCLUDED_CONTENT_TYPES = (
"text/event-stream",
"application/zip",
"application/gzip",
"application/x-gzip",
)
Comment on lines +8 to +13
Copy link

Copilot AI Jan 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Adding new content types to DEFAULT_EXCLUDED_CONTENT_TYPES is a breaking change. While it's sensible to exclude already-compressed formats (application/zip, application/gzip, application/x-gzip), this changes the default behavior for existing users. Consider documenting this as a breaking change in the release notes and ensuring it's mentioned in the PR description or migration guide.

Copilot uses AI. Check for mistakes.


class GZipMiddleware:
def __init__(self, app: ASGIApp, minimum_size: int = 500, compresslevel: int = 9) -> None:
def __init__(
self,
app: ASGIApp,
minimum_size: int = 500,
compresslevel: int = 9,
*,
excluded_content_types: tuple[str, ...] = DEFAULT_EXCLUDED_CONTENT_TYPES,
additional_excluded_content_types: tuple[str, ...] = (),
Comment on lines +23 to +24
Copy link

Copilot AI Jan 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new excluded_content_types and additional_excluded_content_types parameters lack test coverage. Consider adding tests that verify:

  1. Custom content types can be excluded from compression using excluded_content_types
  2. Additional content types can be excluded using additional_excluded_content_types
  3. The default excluded content types (application/zip, application/gzip, application/x-gzip) are properly excluded from compression

Copilot uses AI. Check for mistakes.
Comment on lines +23 to +24
Copy link

Copilot AI Jan 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The documentation in docs/middleware.md should be updated to describe the new excluded_content_types and additional_excluded_content_types parameters. The current documentation at lines 240-244 mentions the default exclusion behavior but doesn't document that these can now be customized.

Copilot uses AI. Check for mistakes.
) -> None:
self.app = app
self.minimum_size = minimum_size
self.compresslevel = compresslevel
self.excluded_content_types = excluded_content_types + additional_excluded_content_types

async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
if scope["type"] != "http": # pragma: no cover
Expand All @@ -22,19 +36,27 @@ async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
headers = Headers(scope=scope)
responder: ASGIApp
if "gzip" in headers.get("Accept-Encoding", ""):
responder = GZipResponder(self.app, self.minimum_size, compresslevel=self.compresslevel)
responder = GZipResponder(
self.app,
self.minimum_size,
excluded_content_types=self.excluded_content_types,
compresslevel=self.compresslevel,
)
else:
responder = IdentityResponder(self.app, self.minimum_size)
responder = IdentityResponder(
self.app, self.minimum_size, excluded_content_types=self.excluded_content_types
)

await responder(scope, receive, send)


class IdentityResponder:
content_encoding: str

def __init__(self, app: ASGIApp, minimum_size: int) -> None:
def __init__(self, app: ASGIApp, minimum_size: int, *, excluded_content_types: tuple[str, ...]) -> None:
self.app = app
self.minimum_size = minimum_size
self.excluded_content_types = excluded_content_types
self.send: Send = unattached_send
self.initial_message: Message = {}
self.started = False
Expand All @@ -53,7 +75,7 @@ async def send_with_compression(self, message: Message) -> None:
self.initial_message = message
headers = Headers(raw=self.initial_message["headers"])
self.content_encoding_set = "content-encoding" in headers
self.content_type_is_excluded = headers.get("content-type", "").startswith(DEFAULT_EXCLUDED_CONTENT_TYPES)
self.content_type_is_excluded = headers.get("content-type", "").startswith(self.excluded_content_types)
elif message_type == "http.response.body" and (self.content_encoding_set or self.content_type_is_excluded):
if not self.started:
self.started = True
Expand Down Expand Up @@ -119,8 +141,15 @@ def apply_compression(self, body: bytes, *, more_body: bool) -> bytes:
class GZipResponder(IdentityResponder):
content_encoding = "gzip"

def __init__(self, app: ASGIApp, minimum_size: int, compresslevel: int = 9) -> None:
super().__init__(app, minimum_size)
def __init__(
self,
app: ASGIApp,
minimum_size: int,
compresslevel: int = 9,
*,
excluded_content_types: tuple[str, ...],
) -> None:
super().__init__(app, minimum_size, excluded_content_types=excluded_content_types)

self.gzip_buffer = io.BytesIO()
self.gzip_file = gzip.GzipFile(mode="wb", fileobj=self.gzip_buffer, compresslevel=compresslevel)
Expand Down
Loading