Skip to content

Commit

Permalink
Add gzip fallback (#13)
Browse files Browse the repository at this point in the history
* Add gzip fallback

* Add tests

* Update README
  • Loading branch information
kylebarron authored Sep 30, 2020
1 parent dba50e0 commit 919e73f
Show file tree
Hide file tree
Showing 3 changed files with 52 additions and 3 deletions.
13 changes: 10 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
# brotli-middleware
# brotli-asgi

`BrotliMiddleware` adds [Brotli](https://github.com/google/brotli) response compression to ASGI applications (Starlette, FastAPI, Quart, etc.). It provides faster and more dense compression than GZip, and can be used as a drop in replacement for the `GZipMiddleware` shipped with Starlette.

**Installation**

```bash
pip install brotli-middleware
pip install brotli-asgi
```

## Examples
Expand Down Expand Up @@ -49,7 +49,13 @@ def home() -> dict:

```python
app.add_middleware(
BrotliMiddleware, quality=4, mode="text", lgwin=22, lgblock=0, minimum_size=400,
BrotliMiddleware,
quality=4,
mode="text",
lgwin=22,
lgblock=0,
minimum_size=400,
gzip_fallback=True
)
```

Expand All @@ -60,6 +66,7 @@ app.add_middleware(
- _(Optional)_ `lgwin`: Base 2 logarithm of the sliding window size. Range is 10 to 24.
- _(Optional)_ `lgblock`: Base 2 logarithm of the maximum input block size. Range is 16 to 24. If set to 0, the value will be set based on the quality.
- _(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.

## Performance

Expand Down
8 changes: 8 additions & 0 deletions brotli_asgi/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

from brotli import MODE_FONT, MODE_GENERIC, MODE_TEXT, Compressor # type: ignore
from starlette.datastructures import Headers, MutableHeaders
from starlette.middleware.gzip import GZipResponder
from starlette.types import ASGIApp, Message, Receive, Scope, Send


Expand All @@ -29,6 +30,7 @@ def __init__(
lgwin: int = 22,
lgblock: int = 0,
minimum_size: int = 400,
gzip_fallback: bool = True,
) -> None:
"""
Arguments.
Expand All @@ -45,13 +47,15 @@ def __init__(
Range is 16 to 24. If set to 0, the value will be set based on the
quality.
minimum_size: Only compress responses that are bigger than this value in bytes.
gzip_fallback: If True, uses gzip encoding if br is not in the Accept-Encoding header.
"""
self.app = app
self.quality = quality
self.mode = getattr(Mode, mode)
self.minimum_size = minimum_size
self.lgwin = lgwin
self.lgblock = lgblock
self.gzip_fallback = gzip_fallback

async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
if scope["type"] == "http":
Expand All @@ -67,6 +71,10 @@ async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
)
await responder(scope, receive, send)
return
if self.gzip_fallback and "gzip" in headers.get("Accept-Encoding", ""):
responder = GZipResponder(self.app, self.minimum_size)
await responder(scope, receive, send)
return
await self.app(scope, receive, send)


Expand Down
34 changes: 34 additions & 0 deletions tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -100,3 +100,37 @@ def homepage(request):
client = TestClient(app)
response = client.get("/", headers={"accept-encoding": "br"})
assert response.status_code == 200


def test_gzip_fallback():
app = Starlette()

app.add_middleware(BrotliMiddleware, gzip_fallback=True)

@app.route("/")
def homepage(request):
return PlainTextResponse("x" * 4000, status_code=200)

client = TestClient(app)
response = client.get("/", headers={"accept-encoding": "gzip"})
assert response.status_code == 200
assert response.text == "x" * 4000
assert response.headers["Content-Encoding"] == "gzip"
assert int(response.headers["Content-Length"]) < 4000


def test_gzip_fallback_false():
app = Starlette()

app.add_middleware(BrotliMiddleware, gzip_fallback=False)

@app.route("/")
def homepage(request):
return PlainTextResponse("x" * 4000, status_code=200)

client = TestClient(app)
response = client.get("/", headers={"accept-encoding": "gzip"})
assert response.status_code == 200
assert response.text == "x" * 4000
assert "Content-Encoding" not in response.headers
assert int(response.headers["Content-Length"]) == 4000

0 comments on commit 919e73f

Please sign in to comment.