Skip to content

Commit e0f6d5e

Browse files
committed
Better handle errors by retrying
* Handle send failures * Handle send exception from ESP32SPI * Handle failed response read * Handle connect() failures * Handle runtime error from ESP32SPI on connect
1 parent 7d0e3ba commit e0f6d5e

File tree

1 file changed

+59
-36
lines changed

1 file changed

+59
-36
lines changed

adafruit_requests.py

100755100644
Lines changed: 59 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@
5353
__version__ = "0.0.0-auto.0"
5454
__repo__ = "https://github.com/adafruit/Adafruit_CircuitPython_Requests.git"
5555

56+
import errno
5657

5758
class _RawResponse:
5859
def __init__(self, response):
@@ -72,6 +73,8 @@ def readinto(self, buf):
7273
into buf."""
7374
return self._response._readinto(buf) # pylint: disable=protected-access
7475

76+
class _SendFailed(Exception):
77+
"""Custom exception to abort sending a request."""
7578

7679
class Response:
7780
"""The response from a request, contains all the headers/content"""
@@ -94,11 +97,13 @@ def __init__(self, sock, session=None):
9497
self._chunked = False
9598

9699
self._backwards_compatible = not hasattr(sock, "recv_into")
97-
if self._backwards_compatible:
98-
print("Socket missing recv_into. Using more memory to be compatible")
99100

100101
http = self._readto(b" ")
101102
if not http:
103+
if session:
104+
session._close_socket(self.socket)
105+
else:
106+
self.socket.close()
102107
raise RuntimeError("Unable to read HTTP response.")
103108
self.status_code = int(bytes(self._readto(b" ")))
104109
self.reason = self._readto(b"\r\n")
@@ -414,30 +419,39 @@ def _get_socket(self, host, port, proto, *, timeout=1):
414419
addr_info = self._socket_pool.getaddrinfo(
415420
host, port, 0, self._socket_pool.SOCK_STREAM
416421
)[0]
417-
sock = self._socket_pool.socket(addr_info[0], addr_info[1], addr_info[2])
418-
connect_host = addr_info[-1][0]
419-
if proto == "https:":
420-
sock = self._ssl_context.wrap_socket(sock, server_hostname=host)
421-
connect_host = host
422-
sock.settimeout(timeout) # socket read timeout
423-
ok = True
424-
try:
425-
ok = sock.connect((connect_host, port))
426-
except MemoryError:
427-
if not any(self._socket_free.items()):
428-
raise
429-
ok = False
430-
431-
# We couldn't connect due to memory so clean up the open sockets.
432-
if not ok:
433-
self._free_sockets()
434-
# Recreate the socket because the ESP-IDF won't retry the connection if it failed once.
435-
sock = None # Clear first so the first socket can be cleaned up.
436-
sock = self._socket_pool.socket(addr_info[0], addr_info[1], addr_info[2])
422+
retry_count = 0
423+
sock = None
424+
while retry_count < 5 and sock is None:
425+
if retry_count > 0:
426+
if any(self._socket_free.items()):
427+
self._free_sockets()
428+
else:
429+
raise RuntimeError("Out of sockets")
430+
retry_count += 1
431+
432+
try:
433+
sock = self._socket_pool.socket(addr_info[0], addr_info[1], addr_info[2])
434+
except OSError:
435+
continue
436+
437+
connect_host = addr_info[-1][0]
437438
if proto == "https:":
438439
sock = self._ssl_context.wrap_socket(sock, server_hostname=host)
440+
connect_host = host
439441
sock.settimeout(timeout) # socket read timeout
440-
sock.connect((connect_host, port))
442+
443+
try:
444+
sock.connect((connect_host, port))
445+
except MemoryError:
446+
sock.close()
447+
sock = None
448+
except OSError as e:
449+
sock.close()
450+
sock = None
451+
452+
if sock is None:
453+
raise RuntimeError("Repeated socket failures")
454+
441455
self._open_sockets[key] = sock
442456
self._socket_free[sock] = False
443457
return sock
@@ -446,11 +460,15 @@ def _get_socket(self, host, port, proto, *, timeout=1):
446460
def _send(socket, data):
447461
total_sent = 0
448462
while total_sent < len(data):
449-
sent = socket.send(data[total_sent:])
463+
# ESP32SPI sockets raise a RuntimeError when unable to send.
464+
try:
465+
sent = socket.send(data[total_sent:])
466+
except RuntimeError:
467+
sent = 0
450468
if sent is None:
451469
sent = len(data)
452470
if sent == 0:
453-
raise RuntimeError("Connection closed")
471+
raise _SendFailed()
454472
total_sent += sent
455473

456474
def _send_request(self, socket, host, method, path, headers, data, json):
@@ -532,12 +550,19 @@ def request(
532550
self._last_response.close()
533551
self._last_response = None
534552

535-
socket = self._get_socket(host, port, proto, timeout=timeout)
536-
try:
537-
self._send_request(socket, host, method, path, headers, data, json)
538-
except:
539-
self._close_socket(socket)
540-
raise
553+
# We may fail to send the request if the socket we got is closed already. So, try a second
554+
# time in that case.
555+
retry_count = 0
556+
while retry_count < 2:
557+
retry_count += 1
558+
socket = self._get_socket(host, port, proto, timeout=timeout)
559+
try:
560+
self._send_request(socket, host, method, path, headers, data, json)
561+
break
562+
except _SendFailed:
563+
self._close_socket(socket)
564+
if retry_count > 1:
565+
raise
541566

542567
resp = Response(socket, self) # our response
543568
if "location" in resp.headers and 300 <= resp.status_code <= 399:
@@ -588,11 +613,9 @@ def __init__(self, socket, tls_mode):
588613
def connect(self, address):
589614
"""connect wrapper to add non-standard mode parameter"""
590615
try:
591-
self._socket.connect(address, self._mode)
592-
return True
593-
except RuntimeError:
594-
return False
595-
616+
return self._socket.connect(address, self._mode)
617+
except RuntimeError as e:
618+
raise OSError(errno.ENOMEM)
596619

597620
class _FakeSSLContext:
598621
def __init__(self, iface):

0 commit comments

Comments
 (0)