Skip to content

Commit

Permalink
Add support for ASGI pathsend extension
Browse files Browse the repository at this point in the history
  • Loading branch information
gi0baro committed Aug 22, 2024
1 parent c78c9aa commit ddfc5c0
Show file tree
Hide file tree
Showing 5 changed files with 91 additions and 2 deletions.
1 change: 1 addition & 0 deletions docs/middleware.md
Original file line number Diff line number Diff line change
Expand Up @@ -264,6 +264,7 @@ around explicitly, rather than mutating the middleware instance.
Currently, the `BaseHTTPMiddleware` has some known limitations:

- Using `BaseHTTPMiddleware` will prevent changes to [`contextlib.ContextVar`](https://docs.python.org/3/library/contextvars.html#contextvars.ContextVar)s from propagating upwards. That is, if you set a value for a `ContextVar` in your endpoint and try to read it from a middleware you will find that the value is not the same value you set in your endpoint (see [this test](https://github.com/encode/starlette/blob/621abc747a6604825190b93467918a0ec6456a24/tests/middleware/test_base.py#L192-L223) for an example of this behavior).
- Using `BaseHTTPMiddleware` will prevent [ASGI pathsend extension](https://asgi.readthedocs.io/en/latest/extensions.html#path-send) to work properly. Thus, if you run your Starlette application with a server implementing this extension, routes returning [FileResponse](responses.md#fileresponse) should avoid the usage of this middleware.

To overcome these limitations, use [pure ASGI middleware](#pure-asgi-middleware), as shown below.

Expand Down
5 changes: 4 additions & 1 deletion starlette/middleware/gzip.py
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,6 @@ async def send_with_gzip(self, message: Message) -> None:

await self.send(self.initial_message)
await self.send(message)

elif message_type == "http.response.body":
# Remaining body in streaming GZip response.
body = message.get("body", b"")
Expand All @@ -108,6 +107,10 @@ async def send_with_gzip(self, message: Message) -> None:
self.gzip_buffer.truncate()

await self.send(message)
elif message_type == "http.response.pathsend":
# Don't apply GZip to pathsend responses
await self.send(self.initial_message)
await self.send(message)


async def unattached_send(message: Message) -> typing.NoReturn:
Expand Down
2 changes: 2 additions & 0 deletions starlette/responses.py
Original file line number Diff line number Diff line change
Expand Up @@ -339,6 +339,8 @@ async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
)
if scope["method"].upper() == "HEAD":
await send({"type": "http.response.body", "body": b"", "more_body": False})
elif "http.response.pathsend" in scope["extensions"]:
await send({"type": "http.response.pathsend", "path": str(self.path)})
else:
async with await anyio.open_file(self.path, mode="rb") as file:
more_body = True
Expand Down
53 changes: 52 additions & 1 deletion tests/middleware/test_gzip.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,21 @@
from __future__ import annotations

from pathlib import Path

import pytest

from starlette.applications import Starlette
from starlette.middleware import Middleware
from starlette.middleware.gzip import GZipMiddleware
from starlette.requests import Request
from starlette.responses import ContentStream, PlainTextResponse, StreamingResponse
from starlette.responses import (
ContentStream,
FileResponse,
PlainTextResponse,
StreamingResponse,
)
from starlette.routing import Route
from starlette.types import Message
from tests.types import TestClientFactory


Expand Down Expand Up @@ -106,3 +118,42 @@ async def generator(bytes: bytes, count: int) -> ContentStream:
assert response.text == "x" * 4000
assert response.headers["Content-Encoding"] == "text"
assert "Content-Length" not in response.headers


@pytest.mark.anyio
async def test_gzip_ignored_for_pathsend_responses(tmpdir: Path) -> None:
path = tmpdir / "example.txt"
with path.open("w") as file:
file.write("<file content>")

events: list[Message] = []

async def endpoint_with_pathsend(request: Request) -> FileResponse:
_ = await request.body()
return FileResponse(path)

app = Starlette(
routes=[Route("/", endpoint=endpoint_with_pathsend)],
middleware=[Middleware(GZipMiddleware)],
)

scope = {
"type": "http",
"version": "3",
"method": "GET",
"path": "/",
"headers": [(b"accept-encoding", b"gzip, text")],
"extensions": {"http.response.pathsend": {}},
}

async def receive() -> Message:
return {"type": "http.request", "body": b"", "more_body": False}

async def send(message: Message) -> None:
events.append(message)

await app(scope, receive, send)

assert len(events) == 2
assert events[0]["type"] == "http.response.start"
assert events[1]["type"] == "http.response.pathsend"
32 changes: 32 additions & 0 deletions tests/test_responses.py
Original file line number Diff line number Diff line change
Expand Up @@ -356,6 +356,38 @@ def test_file_response_with_method_warns(tmp_path: Path) -> None:
FileResponse(path=tmp_path, filename="example.png", method="GET")


@pytest.mark.anyio
async def test_file_response_with_pathsend(tmpdir: Path) -> None:
path = tmpdir / "xyz"
content = b"<file content>" * 1000
with open(path, "wb") as file:
file.write(content)

app = FileResponse(path=path, filename="example.png")

async def receive() -> Message: # type: ignore[empty-body]
... # pragma: no cover

async def send(message: Message) -> None:
if message["type"] == "http.response.start":
assert message["status"] == status.HTTP_200_OK
headers = Headers(raw=message["headers"])
assert headers["content-type"] == "image/png"
assert "content-length" in headers
assert "content-disposition" in headers
assert "last-modified" in headers
assert "etag" in headers
elif message["type"] == "http.response.pathsend":
assert message["path"] == str(path)

# Since the TestClient doesn't support `pathsend`, we need to test this directly.
await app(
{"type": "http", "method": "get", "extensions": {"http.response.pathsend": {}}},
receive,
send,
)


def test_set_cookie(
test_client_factory: TestClientFactory, monkeypatch: pytest.MonkeyPatch
) -> None:
Expand Down

0 comments on commit ddfc5c0

Please sign in to comment.