Skip to content

Commit 68f8fe4

Browse files
pablogventuradpgeorge
authored andcommitted
requests: Switch to HTTP/1.1 with Content-Length and raw streaming.
Changes: - Send HTTP/1.1 requests (was HTTP/1.0). - Support Content-Length response bodies without buffering in request(). - Preserve .raw as a live BodyStream wrapper; .content remains lazy. Signed-off-by: Pablo Ventura <pablogventura@gmail.com>
1 parent f4cfc3c commit 68f8fe4

4 files changed

Lines changed: 139 additions & 15 deletions

File tree

python-ecosys/requests/README.md

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@ This module provides a lightweight version of the Python
44
[requests](https://requests.readthedocs.io/en/latest/) library.
55

66
It includes support for all HTTP verbs, https, json decoding of responses,
7-
redirects, basic authentication.
7+
redirects, basic authentication, HTTP/1.1 requests, and reading response
8+
bodies with Content-Length via streaming ``.raw`` or lazy ``.content``.
89

910
### Limitations
1011

@@ -14,3 +15,10 @@ redirects, basic authentication.
1415
* Compressed requests/responses are not currently supported.
1516
* File upload is not supported.
1617
* Chunked encoding in responses is not supported.
18+
* HTTP keep-alive connection reuse is not supported (Connection: close by default).
19+
20+
### Follow-up work
21+
22+
* Chunked response bodies.
23+
* TLS certificate verification (see micropython-lib issue #838).
24+
* ``stream=True`` incremental body reads (see issue #777).

python-ecosys/requests/manifest.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
1-
metadata(version="0.11.0", pypi="requests")
1+
metadata(version="1.0.0", pypi="requests")
22

33
package("requests")

python-ecosys/requests/requests/__init__.py

Lines changed: 41 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,37 @@
11
import socket
22

33

4+
class BodyStream:
5+
def __init__(self, sock, remaining):
6+
self._sock = sock
7+
self._remaining = remaining
8+
9+
def read(self, n=-1):
10+
if self._remaining == 0:
11+
return b""
12+
if n < 0 or n > self._remaining:
13+
n = self._remaining
14+
data = self._sock.read(n)
15+
self._remaining -= len(data)
16+
if not data:
17+
raise ValueError("Connection closed before Content-Length satisfied")
18+
return data
19+
20+
def readinto(self, buf):
21+
if self._remaining == 0:
22+
return 0
23+
if len(buf) > self._remaining:
24+
buf = memoryview(buf)[: self._remaining]
25+
got = self._sock.readinto(buf)
26+
self._remaining -= got
27+
if not got:
28+
raise ValueError("Connection closed before Content-Length satisfied")
29+
return got
30+
31+
def close(self):
32+
self._sock.close()
33+
34+
435
class Response:
536
def __init__(self, f):
637
self.raw = f
@@ -104,7 +135,7 @@ def request(
104135
context = tls.SSLContext(tls.PROTOCOL_TLS_CLIENT)
105136
context.verify_mode = tls.CERT_NONE
106137
s = context.wrap_socket(s, server_hostname=host)
107-
s.write(b"%s /%s HTTP/1.0\r\n" % (method, path))
138+
s.write(b"%s /%s HTTP/1.1\r\n" % (method, path))
108139

109140
if "Host" not in headers:
110141
headers["Host"] = host
@@ -161,6 +192,7 @@ def request(
161192
reason = ""
162193
if len(l) > 2:
163194
reason = l[2].rstrip()
195+
remaining = None
164196
while True:
165197
l = s.readline()
166198
if not l or l == b"\r\n":
@@ -179,7 +211,10 @@ def request(
179211
elif parse_headers is True:
180212
l = str(l, "utf-8")
181213
k, v = l.split(":", 1)
182-
resp_d[k] = v.strip()
214+
v = v.strip()
215+
resp_d[k] = v
216+
if k.lower() == "content-length":
217+
remaining = int(v)
183218
else:
184219
parse_headers(l, resp_d)
185220
except OSError:
@@ -195,7 +230,10 @@ def request(
195230
else:
196231
return request(method, redirect, data, json, headers, stream)
197232
else:
198-
resp = Response(s)
233+
if remaining is not None:
234+
resp = Response(BodyStream(s, remaining))
235+
else:
236+
resp = Response(s)
199237
resp.status_code = status
200238
resp.reason = reason
201239
if resp_d is not None:

python-ecosys/requests/test_requests.py

Lines changed: 88 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,9 @@
33

44

55
class Socket:
6-
def __init__(self):
6+
def __init__(self, read_data=b"HTTP/1.1 200 OK\r\n\r\n"):
77
self._write_buffer = io.BytesIO()
8-
self._read_buffer = io.BytesIO(b"HTTP/1.0 200 OK\r\n\r\n")
8+
self._read_buffer = io.BytesIO(read_data)
99

1010
def connect(self, address):
1111
pass
@@ -16,6 +16,15 @@ def write(self, buf):
1616
def readline(self):
1717
return self._read_buffer.readline()
1818

19+
def read(self, size=-1):
20+
return self._read_buffer.read(size)
21+
22+
def readinto(self, buf):
23+
return self._read_buffer.readinto(buf)
24+
25+
def close(self):
26+
pass
27+
1928

2029
class socket:
2130
AF_INET = 2
@@ -43,7 +52,7 @@ def test_simple_get():
4352
response = requests.request("GET", "http://example.com")
4453

4554
assert response.raw._write_buffer.getvalue() == (
46-
b"GET / HTTP/1.0\r\n" + b"Connection: close\r\n" + b"Host: example.com\r\n\r\n"
55+
b"GET / HTTP/1.1\r\n" + b"Connection: close\r\n" + b"Host: example.com\r\n\r\n"
4756
), format_message(response)
4857

4958

@@ -53,7 +62,7 @@ def test_get_auth():
5362
)
5463

5564
assert response.raw._write_buffer.getvalue() == (
56-
b"GET / HTTP/1.0\r\n"
65+
b"GET / HTTP/1.1\r\n"
5766
+ b"Host: example.com\r\n"
5867
+ b"Authorization: Basic dGVzdC11c2VybmFtZTp0ZXN0LXBhc3N3b3Jk\r\n"
5968
+ b"Connection: close\r\n\r\n"
@@ -64,7 +73,7 @@ def test_get_custom_header():
6473
response = requests.request("GET", "http://example.com", headers={"User-Agent": "test-agent"})
6574

6675
assert response.raw._write_buffer.getvalue() == (
67-
b"GET / HTTP/1.0\r\n"
76+
b"GET / HTTP/1.1\r\n"
6877
+ b"User-Agent: test-agent\r\n"
6978
+ b"Host: example.com\r\n"
7079
+ b"Connection: close\r\n\r\n"
@@ -75,7 +84,7 @@ def test_post_json():
7584
response = requests.request("GET", "http://example.com", json="test")
7685

7786
assert response.raw._write_buffer.getvalue() == (
78-
b"GET / HTTP/1.0\r\n"
87+
b"GET / HTTP/1.1\r\n"
7988
+ b"Connection: close\r\n"
8089
+ b"Content-Type: application/json\r\n"
8190
+ b"Host: example.com\r\n"
@@ -91,7 +100,7 @@ def chunks():
91100
response = requests.request("GET", "http://example.com", data=chunks())
92101

93102
assert response.raw._write_buffer.getvalue() == (
94-
b"GET / HTTP/1.0\r\n"
103+
b"GET / HTTP/1.1\r\n"
95104
+ b"Transfer-Encoding: chunked\r\n"
96105
+ b"Host: example.com\r\n"
97106
+ b"Connection: close\r\n\r\n"
@@ -106,7 +115,7 @@ def test_overwrite_get_headers():
106115
)
107116

108117
assert response.raw._write_buffer.getvalue() == (
109-
b"GET / HTTP/1.0\r\n" + b"Connection: keep-alive\r\n" + b"Host: test.com\r\n\r\n"
118+
b"GET / HTTP/1.1\r\n" + b"Connection: keep-alive\r\n" + b"Host: test.com\r\n\r\n"
110119
), format_message(response)
111120

112121

@@ -119,7 +128,7 @@ def test_overwrite_post_json_headers():
119128
)
120129

121130
assert response.raw._write_buffer.getvalue() == (
122-
b"GET / HTTP/1.0\r\n"
131+
b"GET / HTTP/1.1\r\n"
123132
+ b"Connection: close\r\n"
124133
+ b"Content-Length: 10\r\n"
125134
+ b"Content-Type: text/plain\r\n"
@@ -137,7 +146,7 @@ def chunks():
137146
)
138147

139148
assert response.raw._write_buffer.getvalue() == (
140-
b"GET / HTTP/1.0\r\n"
149+
b"GET / HTTP/1.1\r\n"
141150
+ b"Host: example.com\r\n"
142151
+ b"Content-Length: 4\r\n"
143152
+ b"Connection: close\r\n\r\n"
@@ -153,6 +162,70 @@ def test_do_not_modify_headers_argument():
153162
assert do_not_modify_this_dict == {}, do_not_modify_this_dict
154163

155164

165+
def test_content_length_via_content():
166+
socket.socket = lambda *a, **k: Socket(
167+
read_data=b"HTTP/1.1 200 OK\r\nContent-Length: 5\r\n\r\nhello"
168+
)
169+
response = requests.request("GET", "http://example.com")
170+
assert response.content == b"hello"
171+
assert response.headers["Content-Length"] == "5"
172+
socket.socket = lambda *a, **k: Socket()
173+
174+
175+
def test_chunked_response_raises():
176+
socket.socket = lambda *a, **k: Socket(
177+
read_data=b"HTTP/1.1 200 OK\r\nTransfer-Encoding: chunked\r\n\r\n5\r\nhello\r\n0\r\n\r\n"
178+
)
179+
raised = False
180+
try:
181+
requests.request("GET", "http://example.com")
182+
except ValueError as e:
183+
raised = True
184+
if "Unsupported" not in str(e):
185+
raise
186+
if not raised:
187+
raise AssertionError("expected ValueError for chunked response")
188+
socket.socket = lambda *a, **k: Socket()
189+
190+
191+
def test_raw_open_before_content():
192+
socket.socket = lambda *a, **k: Socket(
193+
read_data=b"HTTP/1.1 200 OK\r\nContent-Length: 5\r\n\r\nhello"
194+
)
195+
response = requests.request("GET", "http://example.com")
196+
assert response.raw is not None
197+
assert response.raw.read(1) == b"h"
198+
socket.socket = lambda *a, **k: Socket()
199+
200+
201+
def test_raw_incremental_content_length():
202+
socket.socket = lambda *a, **k: Socket(
203+
read_data=b"HTTP/1.1 200 OK\r\nContent-Length: 10\r\n\r\nabcdefghij"
204+
)
205+
response = requests.request("GET", "http://example.com")
206+
assert response.raw.read(3) == b"abc"
207+
assert response.raw.read(3) == b"def"
208+
assert response.content == b"ghij"
209+
assert response.raw is None
210+
socket.socket = lambda *a, **k: Socket()
211+
212+
213+
def test_raw_readinto_content_length():
214+
socket.socket = lambda *a, **k: Socket(
215+
read_data=b"HTTP/1.1 200 OK\r\nContent-Length: 10\r\n\r\nabcdefghij"
216+
)
217+
response = requests.request("GET", "http://example.com")
218+
buf = bytearray(3)
219+
result = b""
220+
while True:
221+
n = response.raw.readinto(buf)
222+
if n == 0:
223+
break
224+
result += buf if n == 3 else buf[:n]
225+
assert result == b"abcdefghij"
226+
socket.socket = lambda *a, **k: Socket()
227+
228+
156229
test_simple_get()
157230
test_get_auth()
158231
test_get_custom_header()
@@ -162,3 +235,8 @@ def test_do_not_modify_headers_argument():
162235
test_overwrite_post_json_headers()
163236
test_overwrite_post_chunked_data_headers()
164237
test_do_not_modify_headers_argument()
238+
test_content_length_via_content()
239+
test_chunked_response_raises()
240+
test_raw_open_before_content()
241+
test_raw_incremental_content_length()
242+
test_raw_readinto_content_length()

0 commit comments

Comments
 (0)