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 all commits
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
54 changes: 52 additions & 2 deletions starlette/responses.py
Original file line number Diff line number Diff line change
Expand Up @@ -129,14 +129,64 @@ def delete_cookie(self, key: str, path: str = "/", domain: str = None) -> None:
self.set_cookie(key, expires=0, max_age=0, path=path, domain=domain)

async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
if self.status_code < 200:
# Response is terminated after the status line. So no headers and no body.
# https://datatracker.ietf.org/doc/html/rfc7231#section-6.2
raw_headers = []
body = b""
elif self.status_code == 204:
# Response must not have a content-length header. See
# https://datatracker.ietf.org/doc/html/rfc7230#section-3.3.2
# http spec does not appear to say whether or not there can be
# a content-type header. Some clients
# will attempt to parse the message body if there is a content-type
# header, so we ensure that there isn't one in
# the response.
Comment on lines +140 to +144
Copy link
Sponsor Member

Choose a reason for hiding this comment

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

Why? Is it obvious?

Shouldn't we focus on what is defined?

raw_headers = [
header
for header in self.raw_headers
if header[0] not in (b"content-length", b"content-type")
]
body = b""
elif self.status_code == 205:
# Response must not include a body.
# Response can either have a content-length: 0 header or a
# transfer-encoding: chunked header.
# We check for a transfer-encoding header. If not found,
# we ensure the presence of the content-length header.
# https://datatracker.ietf.org/doc/html/rfc7231#section-6.3.6
raw_headers = [
header
for header in self.raw_headers
if header[0] not in (b"content-length", b"content-type")
]
for header in self.raw_headers:
if header[0] == b"transfer-encoding":
break
else:
raw_headers.append((b"content-length", b"0"))
body = b""
elif self.status_code == 304:
# A 304 Not Modfied response may contain a transfer-encoding header,
# or 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.1
# https://datatracker.ietf.org/doc/html/rfc7230#section-3.3.2
raw_headers = self.raw_headers
body = b""
else:
raw_headers = self.raw_headers
body = self.body

await send(
{
"type": "http.response.start",
"status": self.status_code,
"headers": self.raw_headers,
"headers": raw_headers,
}
)
await send({"type": "http.response.body", "body": self.body})
await send({"type": "http.response.body", "body": body})

if self.background is not None:
await self.background()
Expand Down
125 changes: 125 additions & 0 deletions tests/test_responses.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import itertools
import os

import anyio
Expand Down Expand Up @@ -309,3 +310,127 @@ def test_head_method(test_client_factory):
client = test_client_factory(app)
response = client.head("/")
assert response.text == ""


@pytest.mark.anyio
@pytest.mark.parametrize(
"response_cls, status_code",
list(itertools.product([Response, JSONResponse], [100, 101, 102])),
)
async def test_response_1xx(response_cls, status_code):
scope = {}

async def receive():
return {} # pragma: no cover

async def send(message: dict):
if message["type"] == "http.response.start":
# also ensures that self.raw_headers is not None
assert len(message["headers"]) == 0
elif message["type"] == "http.response.body":
# per ASGI, if body key is missing, default is False
assert "body" not in message or message["body"] == b""
assert "more_body" not in message or message["more_body"] is False
else:
pass # pragma: no cover

response = response_cls(status_code=status_code)
await response.__call__(scope, receive, send)


@pytest.mark.anyio
@pytest.mark.parametrize(
"response_cls, content",
itertools.product([Response, JSONResponse], [None, "test"]),
)
async def test_response_204(response_cls, content):
scope = {}

async def receive():
return {} # pragma: no cover

async def send(message: dict):
if message["type"] == "http.response.start":
header_map = dict(message["headers"])
assert b"content-length" not in header_map
assert b"content-type" not in header_map
elif message["type"] == "http.response.body":
# per ASGI, if body key is missing, default is False
assert "body" not in message or message["body"] == b""
assert "more_body" not in message or message["more_body"] is False
else:
pass # pragma: no cover

response = response_cls(status_code=204, content=content)
await response.__call__(scope, receive, send)


@pytest.mark.anyio
@pytest.mark.parametrize("response_cls", [Response, JSONResponse])
async def test_response_205_with_te_header(response_cls):
scope = {}

async def receive():
return {} # pragma: no cover

async def send(message: dict):
if message["type"] == "http.response.start":
header_map = dict(message["headers"])
assert header_map[b"transfer-encoding"] == b"chunked"
assert b"content-length" not in header_map
assert b"content-type" not in header_map
elif message["type"] == "http.response.body":
# per ASGI, if body key is missing, default is False
assert "body" not in message or message["body"] == b""
assert "more_body" not in message or message["more_body"] is False
else:
pass # pragma: no cover

response = response_cls(status_code=205, headers={"transfer-encoding": "chunked"})
await response.__call__(scope, receive, send)


@pytest.mark.anyio
@pytest.mark.parametrize("response_cls", [Response, JSONResponse])
async def test_response_205_with_cl_header(response_cls):
scope = {}

async def receive():
return {} # pragma: no cover

async def send(message: dict):
if message["type"] == "http.response.start":
header_map = dict(message["headers"])
assert header_map[b"content-length"] == b"0"
assert b"content-type" not in header_map
elif message["type"] == "http.response.body":
# per ASGI, if body key is missing, default is False
assert "body" not in message or message["body"] == b""
assert "more_body" not in message or message["more_body"] is False
else:
pass # pragma: no cover

response = response_cls(status_code=205)
await response.__call__(scope, receive, send)


@pytest.mark.anyio
@pytest.mark.parametrize("response_cls", [Response, JSONResponse])
async def test_response_304(response_cls):
scope = {}

async def receive():
return {} # pragma: no cover

async def send(message: dict):
if message["type"] == "http.response.start":
pass
elif message["type"] == "http.response.body":
# per ASGI, 'body', 'more_body' are optional.
assert "body" not in message or message["body"] == b""
assert "more_body" not in message or message["more_body"] is False
else:
pass # pragma: no cover

response = response_cls(status_code=304)
await response.__call__(scope, receive, send)