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

Implement EmptyResponse class #1270

Closed
wants to merge 7 commits into from
Closed
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
23 changes: 23 additions & 0 deletions docs/responses.md
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,29 @@ async def app(scope, receive, send):
await response(scope, receive, send)
```

### EmptyResponse

Supplies an empty message body as the response.

In particular, use an EmptyResponse object for 1xx, 204, 205, 304 responses as it sets or omits a Content-Length header as appropriate.

Signature: `Response(status_code:int, headers: typing.Optional[typing.Dict[str, str]] = None, background: typing.Optional[BackgroundTask] = None)`
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
Signature: `Response(status_code:int, headers: typing.Optional[typing.Dict[str, str]] = None, background: typing.Optional[BackgroundTask] = None)`
Signature: `EmptyResponse(status_code:int, headers: typing.Optional[typing.Dict[str, str]] = None, background: typing.Optional[BackgroundTask] = None)`

Alternatively, just take this line out. We don't have proper, generated API docs so trying to be consistent is difficult.

Copy link
Author

Choose a reason for hiding this comment

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

I fixed the class name. Following the superclass documentation seemed like the right thing to do.


* `status_code` - An integer HTTP status code.
* `headers` - A dictionary of strings.
* `background` - A BackgroundTask to be executed when the response is sent.


```python
from starlette.responses import EmptyResponse


async def app(scope, receive, send):
assert scope['type'] == 'http'
response = EmptyResponse(status_code=204)
await response(scope, receive, send)
```

## Third party middleware

### [SSEResponse(EventSourceResponse)](https://github.com/sysid/sse-starlette)
Expand Down
48 changes: 48 additions & 0 deletions starlette/responses.py
Original file line number Diff line number Diff line change
Expand Up @@ -311,3 +311,51 @@ async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
)
if self.background is not None:
await self.background()


class EmptyResponse(Response):
"""Response to be sent with status code 1xx, 204, 205, 304, or whenever
an empty response is intended."""

def __init__(
self,
status_code: int,
headers: typing.Optional[typing.Dict[str, str]] = None,
background: typing.Optional[BackgroundTask] = None,
) -> None:
super().__init__(
content=b"", status_code=status_code, headers=headers, background=background
)

def init_headers(self, headers: typing.Mapping[str, str] = None) -> None:
byte_headers: typing.Dict[bytes, bytes] = (
{
k.lower().encode("latin-1"): v.encode("latin-1")
for k, v in headers.items()
}
if headers
else {}
)

if self.status_code < 200 or self.status_code == 204:
# Response must not have a content-length header. See
# https://datatracker.ietf.org/doc/html/rfc7230#section-3.3.2
if b"content-length" in byte_headers:
del byte_headers[b"content-length"]
elif self.status_code == 205:
# Response can either have a content-length header or a
# transfer-encoding: chunked header.
# We choose to ensure a content-length header.
# https://datatracker.ietf.org/doc/html/rfc7231#section-6.3.6
byte_headers[b"content-length"] = b"0"
elif self.status_code == 304:
# A 304 Not Modfied response may contain a content-length header
# whose value is the length of
# message that would have been sent in a 200 OK response.
# So we leave the headers as is.
# https://datatracker.ietf.org/doc/html/rfc7230#section-3.3.2
pass
else:
byte_headers[b"content-length"] = b"0"

self.raw_headers = [(k, v) for k, v in byte_headers.items()]
32 changes: 32 additions & 0 deletions tests/test_responses.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
from starlette.background import BackgroundTask
from starlette.requests import Request
from starlette.responses import (
EmptyResponse,
FileResponse,
JSONResponse,
RedirectResponse,
Expand Down Expand Up @@ -309,3 +310,34 @@ def test_head_method(test_client_factory):
client = test_client_factory(app)
response = client.head("/")
assert response.text == ""


def test_empty_response_100():
response = EmptyResponse(status_code=100)
assert "content-length" not in response.headers


def test_empty_response_200():
response = EmptyResponse(status_code=200)
assert response.headers["content-length"] == "0"


def test_empty_response_204():
response = EmptyResponse(status_code=204)
assert "content-length" not in response.headers


def test_empty_response_204_removing_header():
response = EmptyResponse(status_code=204, headers={"content-length": "0"})
assert "content-length" not in response.headers


def test_empty_response_205():
response = EmptyResponse(status_code=205)
assert response.headers["content-length"] == "0"


def test_empty_response_304():
headers = {"content-length": "43"}
response = EmptyResponse(status_code=304, headers=headers)
assert response.headers["content-length"] == "43"