Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions cachecontrol/adapter.py
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,10 @@ def _update_chunk_length(
super_update_chunk_length(self)
if self.chunk_left == 0:
self._fp._close() # type: ignore[union-attr]
elif self.chunk_left is not None:
self._fp._set_chunk_bytes_remaining( # type: ignore[union-attr]
self.chunk_left
)

response._update_chunk_length = functools.partial( # type: ignore[method-assign]
_update_chunk_length, weakref.ref(response)
Expand Down
9 changes: 6 additions & 3 deletions cachecontrol/filewrapper.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ def __init__(
self.__buf = NamedTemporaryFile("rb+", delete=True)
self.__fp = fp
self.__callback = callback
self.__chunk_bytes_remaining = 0

def __getattr__(self, name: str) -> Any:
# The vagaries of garbage collection means that self.__fp is
Expand Down Expand Up @@ -107,14 +108,16 @@ def read(self, amt: int | None = None) -> bytes:

return data

def _set_chunk_bytes_remaining(self, chunk_bytes_remaining: int) -> None:
self.__chunk_bytes_remaining = chunk_bytes_remaining

def _safe_read(self, amt: int) -> bytes:
data: bytes = self.__fp._safe_read(amt) # type: ignore[attr-defined]
if amt == 2 and data == b"\r\n":
# urllib executes this read to toss the CRLF at the end
# of the chunk.
if self.__chunk_bytes_remaining == 0 and amt == 2:
return data

self.__buf.write(data)
self.__chunk_bytes_remaining -= len(data)
if self.__is_fp_closed():
self._close()

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

import cherrypy
import pytest
from cheroot.server import HTTPRequest


class SimpleApp:
Expand Down Expand Up @@ -101,6 +102,14 @@ def stream(self, env, start_response):
for i in range(10):
yield pformat(i).encode("utf8")

def stream_with_crlf(self, env, start_response):
headers = [("Content-Type", "text/plain"), ("Cache-Control", "max-age=5000")]
start_response("200 OK", headers)

yield b"AA"
yield b"\r\n"
yield b"BB"

def fixed_length(self, env, start_response):
body = b"0123456789"
headers = [
Expand Down Expand Up @@ -147,6 +156,21 @@ def url(server):
return "http://%s:%s/" % server.bind_addr


@pytest.fixture()
def malformed_chunk_delimiters(monkeypatch):
"""Make the test server send XX instead of each chunk's trailing CRLF."""
write = HTTPRequest.write

def write_malformed_chunk(request, chunk):
if request.chunked_write and chunk:
data = f"{len(chunk):x}\r\n".encode() + chunk + b"XX"
request.conn.wfile.write(data)
else:
write(request, chunk)

monkeypatch.setattr(HTTPRequest, "write", write_malformed_chunk)


def get_free_port():
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.bind(("", 0))
Expand Down
20 changes: 19 additions & 1 deletion tests/test_chunked_response.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,14 +43,32 @@ def test_stream_is_cached(self, url, sess):
content_1 = resp_1.content

resp_2 = sess.get(url + "stream")
content_2 = resp_1.content
content_2 = resp_2.content

assert not resp_1.from_cache
assert resp_2.from_cache
assert content_1 == content_2

def test_stream_with_crlf_chunk_is_cached_without_corruption(self, url, sess):
resp_1 = sess.get(url + "stream_with_crlf")
resp_2 = sess.get(url + "stream_with_crlf")

assert resp_1.content == b"AA\r\nBB"
assert resp_2.from_cache
assert resp_2.content == resp_1.content

def test_stream_is_not_cached_when_content_is_not_read(self, url, sess):
sess.get(url + "stream", stream=True)
resp = sess.get(url + "stream", stream=True)

assert not resp.from_cache

def test_stream_with_malformed_delimiters_is_cached_without_corruption(
self, url, sess, malformed_chunk_delimiters
):
resp_1 = sess.get(url + "stream_with_crlf")
resp_2 = sess.get(url + "stream_with_crlf")

assert resp_1.content == b"AA\r\nBB"
assert resp_2.from_cache
assert resp_2.content == resp_1.content