Skip to content
5 changes: 2 additions & 3 deletions python-ecosys/requests/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@ This module provides a lightweight version of the Python

It includes support for all HTTP verbs, https, json decoding of responses,
redirects, basic authentication, HTTP/1.1 requests, and reading response
bodies with Content-Length via streaming ``.raw`` or lazy ``.content``.
bodies with Content-Length or Transfer-Encoding: chunked via streaming
``.raw`` or lazy ``.content``.

### Limitations

Expand All @@ -14,11 +15,9 @@ bodies with Content-Length via streaming ``.raw`` or lazy ``.content``.
multipart-form encoding of post data (this can be done manually).
* Compressed requests/responses are not currently supported.
* File upload is not supported.
* Chunked encoding in responses is not supported.
* HTTP keep-alive connection reuse is not supported (Connection: close by default).

### Follow-up work

* Chunked response bodies.
* TLS certificate verification (see micropython-lib issue #838).
* ``stream=True`` incremental body reads (see issue #777).
51 changes: 34 additions & 17 deletions python-ecosys/requests/requests/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,30 +2,44 @@


class BodyStream:
def __init__(self, sock, remaining):
def __init__(self, sock, remaining=0):
self._sock = sock
self._chunked = remaining < 0
self._remaining = remaining

def read(self, n=-1):
if self._remaining == 0:
return b""
if n < 0 or n > self._remaining:
n = self._remaining
data = self._sock.read(n)
self._remaining -= len(data)
if not data:
raise ValueError("Connection closed before Content-Length satisfied")
return data
buf = bytearray(n if n >= 0 else 256)
if n >= 0:
got = self.readinto(buf)
return buf[:got] if got else b""
result = b""
while 1:
got = self.readinto(buf)
if not got:
return result
result += buf[:got]

def readinto(self, buf):
if self._remaining == 0:
return 0
s = self._sock
if self._remaining <= 0:
if self._remaining == 0:
return 0
self._remaining = int(s.readline().split(b";")[0], 16)
if self._remaining == 0:
while 1:
l = s.readline()
if not l or l == b"\r\n":
break
return 0
if len(buf) > self._remaining:
buf = memoryview(buf)[: self._remaining]
got = self._sock.readinto(buf)
self._remaining -= got
got = s.readinto(buf)
if not got:
raise ValueError("Connection closed before Content-Length satisfied")
raise ValueError("Connection closed before body complete")
self._remaining -= got
if self._remaining == 0 and self._chunked:
s.readline()
self._remaining = -1
return got

def close(self):
Expand Down Expand Up @@ -196,14 +210,15 @@ def request(
if len(l) > 2:
reason = l[2].rstrip()
remaining = None
chunked = False
while True:
l = s.readline()
if not l or l == b"\r\n":
break
# print(l)
if l.startswith(b"Transfer-Encoding:"):
if b"chunked" in l:
raise ValueError("Unsupported " + str(l, "utf-8"))
chunked = True
elif l.startswith(b"Location:") and not 200 <= status <= 299:
if status in [301, 302, 303, 307, 308]:
redirect = str(l[10:-2], "utf-8")
Expand Down Expand Up @@ -235,7 +250,9 @@ def request(
else:
return request(method, redirect, data, json, headers, stream)
else:
if remaining is not None:
if chunked:
resp = Response(BodyStream(s, -1))
elif remaining is not None:
resp = Response(BodyStream(s, remaining))
else:
resp = Response(s)
Expand Down
34 changes: 22 additions & 12 deletions python-ecosys/requests/test_requests.py
Original file line number Diff line number Diff line change
Expand Up @@ -221,19 +221,28 @@ def test_content_length_via_content():
socket.socket = lambda *a, **k: Socket()


def test_chunked_response_raises():
def test_chunked_response_via_content():
socket.socket = lambda *a, **k: Socket(
read_data=b"HTTP/1.1 200 OK\r\nTransfer-Encoding: chunked\r\n\r\n5\r\nhello\r\n0\r\n\r\n"
read_data=b"HTTP/1.1 200 OK\r\nTransfer-Encoding: chunked\r\n\r\n5\r\nhello\r\n6\r\n world\r\n0\r\n\r\n"
)
raised = False
try:
requests.request("GET", "http://example.com")
except ValueError as e:
raised = True
if "Unsupported" not in str(e):
raise
if not raised:
raise AssertionError("expected ValueError for chunked response")
response = requests.request("GET", "http://example.com")
assert response.content == b"hello world"
socket.socket = lambda *a, **k: Socket()


def test_chunked_response_readinto():
socket.socket = lambda *a, **k: Socket(
read_data=b"HTTP/1.1 200 OK\r\nTransfer-Encoding: chunked\r\n\r\n3\r\nabc\r\n4\r\ndefg\r\n0\r\n\r\n"
)
response = requests.request("GET", "http://example.com")
buf = bytearray(2)
result = b""
while True:
n = response.raw.readinto(buf)
if n == 0:
break
result += buf if n == 2 else buf[:n]
assert result == b"abcdefg"
socket.socket = lambda *a, **k: Socket()


Expand Down Expand Up @@ -325,7 +334,8 @@ def test_redirect_relative():
test_overwrite_post_chunked_data_headers()
test_do_not_modify_headers_argument()
test_content_length_via_content()
test_chunked_response_raises()
test_chunked_response_via_content()
test_chunked_response_readinto()
test_raw_open_before_content()
test_raw_incremental_content_length()
test_raw_readinto_content_length()
Expand Down
Loading