Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Avoid double encode #24

Merged
merged 1 commit into from
Nov 25, 2022
Merged
Show file tree
Hide file tree
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
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,5 @@ dist
*.egg-info
.mypy_cache
.pytest_cache
.vscode
.vscode
build
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,11 @@ app.add_middleware(
- _(Optional)_ `minimum_size`: Only compress responses that are bigger than this value in bytes.
- _(Optional)_ `gzip_fallback`: If `True`, uses gzip encoding if `br` is not in the Accept-Encoding header.

**Notes**:

- It won't apply Brotli compression on responses that already have a Content-Encoding set, to prevent them from being encoded twice.
- If the GZip fallback is applied, and the response already had a Content-Encoding set, the double-encoding-prevention will only happen if you're using Starlette `>=0.22.0` (see [the PR](https://github.com/encode/starlette/pull/1901)).

## Performance

To better understand the benefits of Brotli over GZip, see, [Gzip vs. Brotli: Comparing Compression Techniques](https://www.coralnodes.com/gzip-vs-brotli/), where detailed information and benchmarks are provided.
Expand Down
8 changes: 8 additions & 0 deletions brotli_asgi/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,7 @@ def __init__(
self.send = unattached_send # type: Send
self.initial_message = {} # type: Message
self.started = False
self.content_encoding_set = False
self.br_file = Compressor(
quality=self.quality, mode=self.mode, lgwin=self.lgwin, lgblock=self.lgblock
)
Expand All @@ -131,6 +132,13 @@ async def send_with_brotli(self, message: Message) -> NoReturn:
# Don't send the initial message until we've determined how to
# modify the outgoing headers correctly.
self.initial_message = message
headers = Headers(raw=self.initial_message["headers"])
self.content_encoding_set = "content-encoding" in headers
elif message_type == "http.response.body" and self.content_encoding_set:
if not self.started:
self.started = True
await self.send(self.initial_message)
await self.send(message)
elif message_type == "http.response.body" and not self.started:
self.started = True
body = message.get("body", b"")
Expand Down
2 changes: 1 addition & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
Brotli==1.0.9
starlette==0.21.0
starlette==0.22.0
6 changes: 3 additions & 3 deletions setup.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
"""A compression AGSI middleware using brotli.

Built using starlette under the hood, it can be used as a drop in replacement to
Built using starlette under the hood, it can be used as a drop-in replacement to
GZipMiddleware for Starlette or FastAPI.
"""

from setuptools import setup # type: ignore

extras = {
'test_brotli': ['requests==2.23.0', 'mypy==0.770'],
'test_brotlipy': ['requests==2.23.0', 'mypy==0.770', 'brotlipy==0.7.0']
'test_brotli': ['requests>=2.23.0', 'mypy>=0.770'],
'test_brotlipy': ['requests>=2.23.0', 'mypy>=0.770', 'brotlipy>=0.7.0']
}

setup(
Expand Down
36 changes: 31 additions & 5 deletions tests.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
"""Main test for brotli middleware.
"""Main tests for brotli middleware.

This tests are the same as the ones from starlette.tests.middleware.test_gzip but using
brotli insted.
Some of these tests are the same as the ones from starlette.tests.middleware.test_gzip
but using brotli instead.
"""
import gzip
import io

from starlette.applications import Starlette
from starlette.responses import PlainTextResponse, StreamingResponse, JSONResponse
from starlette.responses import PlainTextResponse, StreamingResponse, JSONResponse, Response
from starlette.testclient import TestClient
from starlette.middleware import Middleware

Expand All @@ -25,7 +28,6 @@ def homepage(request):
assert response.status_code == 200
assert response.text == "x" * 4000
assert response.headers["Content-Encoding"] == "br"
print(len(response.headers["Content-Length"]))
assert int(response.headers["Content-Length"]) < 4000


Expand Down Expand Up @@ -159,3 +161,27 @@ def homepage(request):
assert response.text == "x" * 4000
assert "Content-Encoding" not in response.headers
assert int(response.headers["Content-Length"]) == 4000


def test_brotli_avoids_double_encoding():
# See https://github.com/encode/starlette/pull/1901

app = Starlette()

app.add_middleware(BrotliMiddleware, minimum_size=1)

@app.route("/")
def homepage(request):
gzip_buffer = io.BytesIO()
gzip_file = gzip.GzipFile(mode="wb", fileobj=gzip_buffer)
gzip_file.write(b"hello world" * 200)
gzip_file.close()
body = gzip_buffer.getvalue()
return Response(body, headers={"content-encoding": "gzip", "x-gzipped-content-length-in-test": str(len(body))})

client = TestClient(app)
response = client.get("/", headers={"accept-encoding": "br"})
assert response.status_code == 200
assert response.text == "hello world" * 200
assert response.headers["Content-Encoding"] == "gzip"
assert response.headers["Content-Length"] == response.headers["x-gzipped-content-length-in-test"]