RedirectBodyUnavailable,
RedirectLoop,
ResponseClosed,
+ ResponseNotRead,
StreamConsumed,
Timeout,
TooManyRedirects,
reason_phrase=reason_phrase,
protocol="HTTP/1.1",
headers=headers,
- body=body,
+ content=body,
on_close=self.response_closed,
request=request,
)
status_code=status_code,
protocol="HTTP/2",
headers=headers,
- body=body,
+ content=body,
on_close=on_close,
request=request,
)
"""
+class ResponseNotRead(Exception):
+ """
+ Attempted to access response content, without having called `read()`
+ after a streaming response.
+ """
+
+
class ResponseClosed(Exception):
"""
Attempted to read or stream response content, but the request has been
"""
+# Other cases...
+
+
class InvalidURL(Exception):
"""
URL was missing a hostname, or was not one of HTTP/HTTPS.
IdentityDecoder,
MultiDecoder,
)
-from .exceptions import ResponseClosed, StreamConsumed
+from .exceptions import ResponseClosed, ResponseNotRead, StreamConsumed
from .status_codes import codes
from .utils import get_reason_phrase, normalize_header_key, normalize_header_value
except KeyError:
return default
- def getlist(self, key: str, default: typing.Any = None, split_commas = None) -> typing.List[str]:
+ def getlist(
+ self, key: str, split_commas: bool=False
+ ) -> typing.List[str]:
"""
Return multiple header values.
"""
get_header_key = key.lower().encode(self.encoding)
if split_commas is None:
- split_commas = get_header_key != b'set-cookie'
+ split_commas = get_header_key != b"set-cookie"
values = [
item_value.decode(self.encoding)
if item_key == get_header_key
]
- if not values:
- return [] if default is None else default
-
if not split_commas:
return values
yield self.body
def prepare(self) -> None:
+ """
+ Adds in any default headers. When using the `Client`, this will
+ end up being called into by the `prepare_request()` stage.
+
+ You can omit this behavior by calling `Client.send()` with an
+ explicitly built `Request` instance.
+ """
auto_headers = [] # type: typing.List[typing.Tuple[bytes, bytes]]
has_host = "host" in self.headers
reason_phrase: str = None,
protocol: str = None,
headers: HeaderTypes = None,
- body: BodyTypes = b"",
+ content: BodyTypes = b"",
on_close: typing.Callable = None,
request: Request = None,
history: typing.List["Response"] = None,
):
self.status_code = status_code
- if reason_phrase is None:
- self.reason_phrase = get_reason_phrase(status_code)
- else:
- self.reason_phrase = reason_phrase
+ self.reason_phrase = reason_phrase or get_reason_phrase(status_code)
self.protocol = protocol
self.headers = Headers(headers)
- self.on_close = on_close
- self.is_closed = False
- self.is_streamed = False
- if isinstance(body, bytes):
+ if isinstance(content, bytes):
self.is_closed = True
- self.body = self.decoder.decode(body) + self.decoder.flush()
+ self.is_stream_consumed = True
+ self._raw_content = content
else:
- self.body_aiter = body
+ self.is_closed = False
+ self.is_stream_consumed = False
+ self._raw_stream = content
+ self.on_close = on_close
self.request = request
self.history = [] if history is None else list(history)
self.next = None # typing.Optional[typing.Callable]
"""
return None if self.request is None else self.request.url
+ @property
+ def content(self) -> bytes:
+ if not hasattr(self, "_content"):
+ if hasattr(self, "_raw_content"):
+ self._content = (
+ self.decoder.decode(self._raw_content) + self.decoder.flush()
+ )
+ else:
+ raise ResponseNotRead()
+ return self._content
+
@property
def decoder(self) -> Decoder:
"""
"""
if not hasattr(self, "_decoder"):
decoders = [] # type: typing.List[Decoder]
- values = self.headers.getlist("content-encoding", ["identity"])
+ values = self.headers.getlist("content-encoding", split_commas=True)
for value in values:
value = value.strip().lower()
decoder_cls = SUPPORTED_DECODERS[value]
"""
Read and return the response content.
"""
- if not hasattr(self, "body"):
- body = b""
+ if not hasattr(self, "_content"):
+ content = b""
async for part in self.stream():
- body += part
- self.body = body
- return self.body
+ content += part
+ self._content = content
+ return self._content
async def stream(self) -> typing.AsyncIterator[bytes]:
"""
A byte-iterator over the decoded response content.
This allows us to handle gzip, deflate, and brotli encoded responses.
"""
- if hasattr(self, "body"):
- yield self.body
+ if hasattr(self, "_content"):
+ yield self._content
else:
async for chunk in self.raw():
yield self.decoder.decode(chunk)
"""
A byte-iterator over the raw response content.
"""
- if self.is_streamed:
- raise StreamConsumed()
- if self.is_closed:
- raise ResponseClosed()
- self.is_streamed = True
- async for part in self.body_aiter:
- yield part
- await self.close()
+ if hasattr(self, "_raw_content"):
+ yield self._raw_content
+ else:
+ if self.is_stream_consumed:
+ raise StreamConsumed()
+ if self.is_closed:
+ raise ResponseClosed()
+
+ self.is_stream_consumed = True
+ async for part in self._raw_stream:
+ yield part
+ await self.close()
async def close(self) -> None:
"""
return self._response.headers
@property
- def body(self) -> bytes:
- return self._response.body
+ def content(self) -> bytes:
+ return self._response.content
def read(self) -> bytes:
return self._loop.run_until_complete(self._response.read())
elif request.url.path == "/cross_domain_target":
headers = dict(request.headers.items())
body = json.dumps({"headers": headers}).encode()
- return Response(codes.ok, body=body, request=request)
+ return Response(codes.ok, content=body, request=request)
elif request.url.path == "/redirect_body":
body = await request.read()
elif request.url.path == "/redirect_body_target":
body = await request.read()
body = json.dumps({"body": body.decode()}).encode()
- return Response(codes.ok, body=body, request=request)
+ return Response(codes.ok, content=body, request=request)
- return Response(codes.ok, body=b"Hello, world!", request=request)
+ return Response(codes.ok, content=b"Hello, world!", request=request)
@pytest.mark.asyncio
url = "https://example.com/cross_domain"
headers = {"Authorization": "abc"}
response = await client.request("GET", url, headers=headers)
- data = json.loads(response.body.decode())
+ data = json.loads(response.content.decode())
assert response.url == URL("https://example.org/cross_domain_target")
assert data == {"headers": {}}
url = "https://example.org/cross_domain"
headers = {"Authorization": "abc"}
response = await client.request("GET", url, headers=headers)
- data = json.loads(response.body.decode())
+ data = json.loads(response.content.decode())
assert response.url == URL("https://example.org/cross_domain_target")
assert data == {"headers": {"authorization": "abc"}}
url = "https://example.org/redirect_body"
body = b"Example request body"
response = await client.request("POST", url, body=body)
- data = json.loads(response.body.decode())
+ data = json.loads(response.content.decode())
assert response.url == URL("https://example.org/redirect_body_target")
assert data == {"body": "Example request body"}
http = httpcore.HTTPConnection(origin="http://127.0.0.1:8000/")
response = await http.request("GET", "http://127.0.0.1:8000/")
assert response.status_code == 200
- assert response.body == b"Hello, world!"
+ assert response.content == b"Hello, world!"
@pytest.mark.asyncio
async with httpcore.HTTP2Connection(reader=server, writer=server) as conn:
response = await conn.request("GET", "http://example.org")
assert response.status_code == 200
- assert json.loads(response.body) == {"method": "GET", "path": "/", "body": ""}
+ assert json.loads(response.content) == {"method": "GET", "path": "/", "body": ""}
@pytest.mark.asyncio
async with httpcore.HTTP2Connection(reader=server, writer=server) as conn:
response = await conn.request("POST", "http://example.org", body=b"<data>")
assert response.status_code == 200
- assert json.loads(response.body) == {
+ assert json.loads(response.content) == {
"method": "POST",
"path": "/",
"body": "<data>",
response_3 = await conn.request("GET", "http://example.org/3")
assert response_1.status_code == 200
- assert json.loads(response_1.body) == {"method": "GET", "path": "/1", "body": ""}
+ assert json.loads(response_1.content) == {"method": "GET", "path": "/1", "body": ""}
assert response_2.status_code == 200
- assert json.loads(response_2.body) == {"method": "GET", "path": "/2", "body": ""}
+ assert json.loads(response_2.content) == {"method": "GET", "path": "/2", "body": ""}
assert response_3.status_code == 200
- assert json.loads(response_3.body) == {"method": "GET", "path": "/3", "body": ""}
+ assert json.loads(response_3.content) == {"method": "GET", "path": "/3", "body": ""}
"""
Most headers should split by commas for `getlist`, except 'Set-Cookie'.
"""
- h = httpcore.Headers([('set-cookie', 'a, b'), ('set-cookie', 'c')])
- h.getlist('Set-Cookie') == ['a, b', 'b']
+ h = httpcore.Headers([("set-cookie", "a, b"), ("set-cookie", "c")])
+ h.getlist("Set-Cookie") == ["a, b", "b"]
- h = httpcore.Headers([('vary', 'a, b'), ('vary', 'c')])
- h.getlist('Vary') == ['a', 'b', 'c']
+ h = httpcore.Headers([("vary", "a, b"), ("vary", "c")])
+ h.getlist("Vary") == ["a", "b", "c"]
def test_response():
- response = httpcore.Response(200, body=b"Hello, world!")
+ response = httpcore.Response(200, content=b"Hello, world!")
assert response.status_code == 200
assert response.reason_phrase == "OK"
- assert response.body == b"Hello, world!"
+ assert response.content == b"Hello, world!"
assert response.is_closed
@pytest.mark.asyncio
async def test_read_response():
- response = httpcore.Response(200, body=b"Hello, world!")
+ response = httpcore.Response(200, content=b"Hello, world!")
assert response.status_code == 200
- assert response.body == b"Hello, world!"
+ assert response.content == b"Hello, world!"
assert response.is_closed
- body = await response.read()
+ content = await response.read()
- assert body == b"Hello, world!"
- assert response.body == b"Hello, world!"
+ assert content == b"Hello, world!"
+ assert response.content == b"Hello, world!"
assert response.is_closed
@pytest.mark.asyncio
async def test_streaming_response():
- response = httpcore.Response(200, body=streaming_body())
+ response = httpcore.Response(200, content=streaming_body())
assert response.status_code == 200
- assert not hasattr(response, "body")
assert not response.is_closed
- body = await response.read()
+ content = await response.read()
- assert body == b"Hello, world!"
- assert response.body == b"Hello, world!"
+ assert content == b"Hello, world!"
+ assert response.content == b"Hello, world!"
assert response.is_closed
@pytest.mark.asyncio
async def test_cannot_read_after_stream_consumed():
- response = httpcore.Response(200, body=streaming_body())
+ response = httpcore.Response(200, content=streaming_body())
- body = b""
+ content = b""
async for part in response.stream():
- body += part
+ content += part
with pytest.raises(httpcore.StreamConsumed):
await response.read()
@pytest.mark.asyncio
async def test_cannot_read_after_response_closed():
- response = httpcore.Response(200, body=streaming_body())
+ response = httpcore.Response(200, content=streaming_body())
await response.close()
async with httpcore.Client() as client:
response = await client.get(url)
assert response.status_code == 200
- assert response.body == b"Hello, world!"
+ assert response.content == b"Hello, world!"
@pytest.mark.asyncio
async with httpcore.Client() as client:
response = await client.request("GET", "http://127.0.0.1:8000/", stream=True)
assert response.status_code == 200
- assert not hasattr(response, "body")
body = await response.read()
assert body == b"Hello, world!"
+ assert response.content == b"Hello, world!"
+
+
+@pytest.mark.asyncio
+async def test_access_content_stream_response(server):
+ async with httpcore.Client() as client:
+ response = await client.request("GET", "http://127.0.0.1:8000/", stream=True)
+ assert response.status_code == 200
+ with pytest.raises(httpcore.ResponseNotRead):
+ response.content
@pytest.mark.asyncio
compressed_body = compressor.compress(body) + compressor.flush()
headers = [(b"Content-Encoding", b"deflate")]
- response = httpcore.Response(200, headers=headers, body=compressed_body)
- assert response.body == body
+ response = httpcore.Response(200, headers=headers, content=compressed_body)
+ assert response.content == body
def test_gzip():
compressed_body = compressor.compress(body) + compressor.flush()
headers = [(b"Content-Encoding", b"gzip")]
- response = httpcore.Response(200, headers=headers, body=compressed_body)
- assert response.body == body
+ response = httpcore.Response(200, headers=headers, content=compressed_body)
+ assert response.content == body
def test_brotli():
compressed_body = brotli.compress(body)
headers = [(b"Content-Encoding", b"br")]
- response = httpcore.Response(200, headers=headers, body=compressed_body)
- assert response.body == body
+ response = httpcore.Response(200, headers=headers, content=compressed_body)
+ assert response.content == body
def test_multi():
)
headers = [(b"Content-Encoding", b"deflate, gzip")]
- response = httpcore.Response(200, headers=headers, body=compressed_body)
- assert response.body == body
+ response = httpcore.Response(200, headers=headers, content=compressed_body)
+ assert response.content == body
def test_multi_with_identity():
compressed_body = brotli.compress(body)
headers = [(b"Content-Encoding", b"br, identity")]
- response = httpcore.Response(200, headers=headers, body=compressed_body)
- assert response.body == body
+ response = httpcore.Response(200, headers=headers, content=compressed_body)
+ assert response.content == body
headers = [(b"Content-Encoding", b"identity, br")]
- response = httpcore.Response(200, headers=headers, body=compressed_body)
- assert response.body == body
+ response = httpcore.Response(200, headers=headers, content=compressed_body)
+ assert response.content == body
@pytest.mark.asyncio
yield compressor.flush()
headers = [(b"Content-Encoding", b"gzip")]
- response = httpcore.Response(200, headers=headers, body=compress(body))
+ response = httpcore.Response(200, headers=headers, content=compress(body))
assert not hasattr(response, "body")
assert await response.read() == body
body = b"test 123"
compressed_body = brotli.compress(body)[3:]
with pytest.raises(httpcore.exceptions.DecodingError):
- response = httpcore.Response(200, headers=headers, body=compressed_body)
+ response = httpcore.Response(200, headers=headers, content=compressed_body)
+ response.content
with httpcore.SyncConnectionPool() as http:
response = http.request("GET", "http://127.0.0.1:8000/")
assert response.status_code == 200
- assert response.body == b"Hello, world!"
+ assert response.content == b"Hello, world!"
@threadpool
with httpcore.SyncConnectionPool() as http:
response = http.request("GET", "http://127.0.0.1:8000/", stream=True)
assert response.status_code == 200
- assert not hasattr(response, "body")
- body = response.read()
- assert body == b"Hello, world!"
+ content = response.read()
+ assert content == b"Hello, world!"
@threadpool
with httpcore.SyncConnectionPool() as http:
response = http.request("GET", "http://127.0.0.1:8000/", stream=True)
assert response.status_code == 200
- body = b""
+ content = b""
for chunk in response.stream():
- body += chunk
- assert body == b"Hello, world!"
+ content += chunk
+ assert content == b"Hello, world!"