Skip to content

Commit 178dbc3

Browse files
authored
Merge pull request #149 from justmobilize/fix-duplicate-header-issue
Fix duplicate header issue
2 parents 4754bf3 + 5fa5aa2 commit 178dbc3

7 files changed

+225
-55
lines changed

adafruit_requests.py

+51-30
Original file line numberDiff line numberDiff line change
@@ -551,6 +551,16 @@ def _send(socket: SocketType, data: bytes):
551551
raise OSError(errno.EIO)
552552
total_sent += sent
553553

554+
def _send_as_bytes(self, socket: SocketType, data: str):
555+
return self._send(socket, bytes(data, "utf-8"))
556+
557+
def _send_header(self, socket, header, value):
558+
self._send_as_bytes(socket, header)
559+
self._send(socket, b": ")
560+
self._send_as_bytes(socket, value)
561+
self._send(socket, b"\r\n")
562+
563+
# pylint: disable=too-many-arguments
554564
def _send_request(
555565
self,
556566
socket: SocketType,
@@ -561,40 +571,51 @@ def _send_request(
561571
data: Any,
562572
json: Any,
563573
):
564-
# pylint: disable=too-many-arguments
565-
self._send(socket, bytes(method, "utf-8"))
566-
self._send(socket, b" /")
567-
self._send(socket, bytes(path, "utf-8"))
568-
self._send(socket, b" HTTP/1.1\r\n")
569-
if "Host" not in headers:
570-
self._send(socket, b"Host: ")
571-
self._send(socket, bytes(host, "utf-8"))
572-
self._send(socket, b"\r\n")
573-
if "User-Agent" not in headers:
574-
self._send(socket, b"User-Agent: Adafruit CircuitPython\r\n")
575-
# Iterate over keys to avoid tuple alloc
576-
for k in headers:
577-
self._send(socket, k.encode())
578-
self._send(socket, b": ")
579-
self._send(socket, headers[k].encode())
580-
self._send(socket, b"\r\n")
574+
# Convert data
575+
content_type_header = None
576+
577+
# If json is sent, set content type header and convert to string
581578
if json is not None:
582579
assert data is None
580+
content_type_header = "application/json"
583581
data = json_module.dumps(json)
584-
self._send(socket, b"Content-Type: application/json\r\n")
585-
if data:
586-
if isinstance(data, dict):
587-
self._send(
588-
socket, b"Content-Type: application/x-www-form-urlencoded\r\n"
589-
)
590-
_post_data = ""
591-
for k in data:
592-
_post_data = "{}&{}={}".format(_post_data, k, data[k])
593-
data = _post_data[1:]
594-
if isinstance(data, str):
595-
data = bytes(data, "utf-8")
596-
self._send(socket, b"Content-Length: %d\r\n" % len(data))
582+
583+
# If data is sent and it's a dict, set content type header and convert to string
584+
if data and isinstance(data, dict):
585+
content_type_header = "application/x-www-form-urlencoded"
586+
_post_data = ""
587+
for k in data:
588+
_post_data = "{}&{}={}".format(_post_data, k, data[k])
589+
# remove first "&" from concatenation
590+
data = _post_data[1:]
591+
592+
# Convert str data to bytes
593+
if data and isinstance(data, str):
594+
data = bytes(data, "utf-8")
595+
596+
self._send_as_bytes(socket, method)
597+
self._send(socket, b" /")
598+
self._send_as_bytes(socket, path)
599+
self._send(socket, b" HTTP/1.1\r\n")
600+
601+
# create lower-case supplied header list
602+
supplied_headers = {header.lower() for header in headers}
603+
604+
# Send headers
605+
if not "host" in supplied_headers:
606+
self._send_header(socket, "Host", host)
607+
if not "user-agent" in supplied_headers:
608+
self._send_header(socket, "User-Agent", "Adafruit CircuitPython")
609+
if content_type_header and not "content-type" in supplied_headers:
610+
self._send_header(socket, "Content-Type", content_type_header)
611+
if data and not "content-length" in supplied_headers:
612+
self._send_header(socket, "Content-Length", str(len(data)))
613+
# Iterate over keys to avoid tuple alloc
614+
for header in headers:
615+
self._send_header(socket, header, headers[header])
597616
self._send(socket, b"\r\n")
617+
618+
# Send data
598619
if data:
599620
self._send(socket, bytes(data))
600621

tests/chunk_test.py

+6-3
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,8 @@ def do_test_get_text(
6363
)
6464
sock.send.assert_has_calls(
6565
[
66-
mock.call(b"Host: "),
66+
mock.call(b"Host"),
67+
mock.call(b": "),
6768
mock.call(b"wifitest.adafruit.com"),
6869
]
6970
)
@@ -105,7 +106,8 @@ def do_test_close_flush(
105106
)
106107
sock.send.assert_has_calls(
107108
[
108-
mock.call(b"Host: "),
109+
mock.call(b"Host"),
110+
mock.call(b": "),
109111
mock.call(b"wifitest.adafruit.com"),
110112
]
111113
)
@@ -146,7 +148,8 @@ def do_test_get_text_extra_space(
146148
)
147149
sock.send.assert_has_calls(
148150
[
149-
mock.call(b"Host: "),
151+
mock.call(b"Host"),
152+
mock.call(b": "),
150153
mock.call(b"wifitest.adafruit.com"),
151154
]
152155
)

tests/concurrent_test.py

+4-2
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,8 @@ def test_second_connect_fails_memoryerror(): # pylint: disable=invalid-name
4040

4141
sock.send.assert_has_calls(
4242
[
43-
mock.call(b"Host: "),
43+
mock.call(b"Host"),
44+
mock.call(b": "),
4445
mock.call(b"wifitest.adafruit.com"),
4546
mock.call(b"\r\n"),
4647
]
@@ -82,7 +83,8 @@ def test_second_connect_fails_oserror(): # pylint: disable=invalid-name
8283

8384
sock.send.assert_has_calls(
8485
[
85-
mock.call(b"Host: "),
86+
mock.call(b"Host"),
87+
mock.call(b": "),
8688
mock.call(b"wifitest.adafruit.com"),
8789
mock.call(b"\r\n"),
8890
]

tests/header_test.py

+120-4
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,75 @@
1212
RESPONSE_HEADERS = b"HTTP/1.0 200 OK\r\nContent-Length: 0\r\n\r\n"
1313

1414

15-
def test_json():
15+
def test_host():
16+
pool = mocket.MocketPool()
17+
pool.getaddrinfo.return_value = ((None, None, None, None, (IP, 80)),)
18+
sock = mocket.Mocket(RESPONSE_HEADERS)
19+
pool.socket.return_value = sock
20+
sent = []
21+
22+
def _send(data):
23+
sent.append(data) # pylint: disable=no-member
24+
return len(data)
25+
26+
sock.send.side_effect = _send
27+
28+
requests_session = adafruit_requests.Session(pool)
29+
headers = {}
30+
requests_session.get("http://" + HOST + "/get", headers=headers)
31+
32+
sock.connect.assert_called_once_with((IP, 80))
33+
sent = b"".join(sent)
34+
assert b"Host: httpbin.org\r\n" in sent
35+
36+
37+
def test_host_replace():
38+
pool = mocket.MocketPool()
39+
pool.getaddrinfo.return_value = ((None, None, None, None, (IP, 80)),)
40+
sock = mocket.Mocket(RESPONSE_HEADERS)
41+
pool.socket.return_value = sock
42+
sent = []
43+
44+
def _send(data):
45+
sent.append(data) # pylint: disable=no-member
46+
return len(data)
47+
48+
sock.send.side_effect = _send
49+
50+
requests_session = adafruit_requests.Session(pool)
51+
headers = {"host": IP}
52+
requests_session.get("http://" + HOST + "/get", headers=headers)
53+
54+
sock.connect.assert_called_once_with((IP, 80))
55+
sent = b"".join(sent)
56+
assert b"host: 1.2.3.4\r\n" in sent
57+
assert b"Host: httpbin.org\r\n" not in sent
58+
assert sent.lower().count(b"host:") == 1
59+
60+
61+
def test_user_agent():
62+
pool = mocket.MocketPool()
63+
pool.getaddrinfo.return_value = ((None, None, None, None, (IP, 80)),)
64+
sock = mocket.Mocket(RESPONSE_HEADERS)
65+
pool.socket.return_value = sock
66+
sent = []
67+
68+
def _send(data):
69+
sent.append(data) # pylint: disable=no-member
70+
return len(data)
71+
72+
sock.send.side_effect = _send
73+
74+
requests_session = adafruit_requests.Session(pool)
75+
headers = {}
76+
requests_session.get("http://" + HOST + "/get", headers=headers)
77+
78+
sock.connect.assert_called_once_with((IP, 80))
79+
sent = b"".join(sent)
80+
assert b"User-Agent: Adafruit CircuitPython\r\n" in sent
81+
82+
83+
def test_user_agent_replace():
1684
pool = mocket.MocketPool()
1785
pool.getaddrinfo.return_value = ((None, None, None, None, (IP, 80)),)
1886
sock = mocket.Mocket(RESPONSE_HEADERS)
@@ -30,7 +98,55 @@ def _send(data):
3098
requests_session.get("http://" + HOST + "/get", headers=headers)
3199

32100
sock.connect.assert_called_once_with((IP, 80))
33-
sent = b"".join(sent).lower()
101+
sent = b"".join(sent)
34102
assert b"user-agent: blinka/1.0.0\r\n" in sent
35-
# The current implementation sends two user agents. Fix it, and uncomment below.
36-
# assert sent.count(b"user-agent:") == 1
103+
assert b"User-Agent: Adafruit CircuitPython\r\n" not in sent
104+
assert sent.lower().count(b"user-agent:") == 1
105+
106+
107+
def test_content_type():
108+
pool = mocket.MocketPool()
109+
pool.getaddrinfo.return_value = ((None, None, None, None, (IP, 80)),)
110+
sock = mocket.Mocket(RESPONSE_HEADERS)
111+
pool.socket.return_value = sock
112+
sent = []
113+
114+
def _send(data):
115+
sent.append(data) # pylint: disable=no-member
116+
return len(data)
117+
118+
sock.send.side_effect = _send
119+
120+
requests_session = adafruit_requests.Session(pool)
121+
headers = {}
122+
data = {"test": True}
123+
requests_session.post("http://" + HOST + "/get", data=data, headers=headers)
124+
125+
sock.connect.assert_called_once_with((IP, 80))
126+
sent = b"".join(sent)
127+
assert b"Content-Type: application/x-www-form-urlencoded\r\n" in sent
128+
129+
130+
def test_content_type_replace():
131+
pool = mocket.MocketPool()
132+
pool.getaddrinfo.return_value = ((None, None, None, None, (IP, 80)),)
133+
sock = mocket.Mocket(RESPONSE_HEADERS)
134+
pool.socket.return_value = sock
135+
sent = []
136+
137+
def _send(data):
138+
sent.append(data) # pylint: disable=no-member
139+
return len(data)
140+
141+
sock.send.side_effect = _send
142+
143+
requests_session = adafruit_requests.Session(pool)
144+
headers = {"content-type": "application/test"}
145+
data = {"test": True}
146+
requests_session.post("http://" + HOST + "/get", data=data, headers=headers)
147+
148+
sock.connect.assert_called_once_with((IP, 80))
149+
sent = b"".join(sent)
150+
assert b"content-type: application/test\r\n" in sent
151+
assert b"Content-Type: application/x-www-form-urlencoded\r\n" not in sent
152+
assert sent.lower().count(b"content-type:") == 1

tests/post_test.py

+22-5
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,8 @@ def test_method():
3838
)
3939
sock.send.assert_has_calls(
4040
[
41-
mock.call(b"Host: "),
41+
mock.call(b"Host"),
42+
mock.call(b": "),
4243
mock.call(b"httpbin.org"),
4344
]
4445
)
@@ -64,10 +65,18 @@ def test_form():
6465
pool.socket.return_value = sock
6566

6667
requests_session = adafruit_requests.Session(pool)
67-
data = {"Date": "July 25, 2019"}
68+
data = {"Date": "July 25, 2019", "Time": "12:00"}
6869
requests_session.post("http://" + HOST + "/post", data=data)
6970
sock.connect.assert_called_once_with((IP, 80))
70-
sock.send.assert_called_with(b"Date=July 25, 2019")
71+
sock.send.assert_has_calls(
72+
[
73+
mock.call(b"Content-Type"),
74+
mock.call(b": "),
75+
mock.call(b"application/x-www-form-urlencoded"),
76+
mock.call(b"\r\n"),
77+
]
78+
)
79+
sock.send.assert_called_with(b"Date=July 25, 2019&Time=12:00")
7180

7281

7382
def test_json():
@@ -77,7 +86,15 @@ def test_json():
7786
pool.socket.return_value = sock
7887

7988
requests_session = adafruit_requests.Session(pool)
80-
json_data = {"Date": "July 25, 2019"}
89+
json_data = {"Date": "July 25, 2019", "Time": "12:00"}
8190
requests_session.post("http://" + HOST + "/post", json=json_data)
8291
sock.connect.assert_called_once_with((IP, 80))
83-
sock.send.assert_called_with(b'{"Date": "July 25, 2019"}')
92+
sock.send.assert_has_calls(
93+
[
94+
mock.call(b"Content-Type"),
95+
mock.call(b": "),
96+
mock.call(b"application/json"),
97+
mock.call(b"\r\n"),
98+
]
99+
)
100+
sock.send.assert_called_with(b'{"Date": "July 25, 2019", "Time": "12:00"}')

tests/protocol_test.py

+6-3
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,8 @@ def test_get_https_text():
4949
)
5050
sock.send.assert_has_calls(
5151
[
52-
mock.call(b"Host: "),
52+
mock.call(b"Host"),
53+
mock.call(b": "),
5354
mock.call(b"wifitest.adafruit.com"),
5455
]
5556
)
@@ -80,7 +81,8 @@ def test_get_http_text():
8081
)
8182
sock.send.assert_has_calls(
8283
[
83-
mock.call(b"Host: "),
84+
mock.call(b"Host"),
85+
mock.call(b": "),
8486
mock.call(b"wifitest.adafruit.com"),
8587
]
8688
)
@@ -109,7 +111,8 @@ def test_get_close():
109111
)
110112
sock.send.assert_has_calls(
111113
[
112-
mock.call(b"Host: "),
114+
mock.call(b"Host"),
115+
mock.call(b": "),
113116
mock.call(b"wifitest.adafruit.com"),
114117
]
115118
)

0 commit comments

Comments
 (0)