53
53
__version__ = "0.0.0-auto.0"
54
54
__repo__ = "https://github.com/adafruit/Adafruit_CircuitPython_Requests.git"
55
55
56
+ import errno
56
57
57
58
class _RawResponse :
58
59
def __init__ (self , response ):
@@ -72,6 +73,8 @@ def readinto(self, buf):
72
73
into buf."""
73
74
return self ._response ._readinto (buf ) # pylint: disable=protected-access
74
75
76
+ class _SendFailed (Exception ):
77
+ """Custom exception to abort sending a request."""
75
78
76
79
class Response :
77
80
"""The response from a request, contains all the headers/content"""
@@ -94,11 +97,13 @@ def __init__(self, sock, session=None):
94
97
self ._chunked = False
95
98
96
99
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" )
99
100
100
101
http = self ._readto (b" " )
101
102
if not http :
103
+ if session :
104
+ session ._close_socket (self .socket )
105
+ else :
106
+ self .socket .close ()
102
107
raise RuntimeError ("Unable to read HTTP response." )
103
108
self .status_code = int (bytes (self ._readto (b" " )))
104
109
self .reason = self ._readto (b"\r \n " )
@@ -414,30 +419,39 @@ def _get_socket(self, host, port, proto, *, timeout=1):
414
419
addr_info = self ._socket_pool .getaddrinfo (
415
420
host , port , 0 , self ._socket_pool .SOCK_STREAM
416
421
)[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 ]
437
438
if proto == "https:" :
438
439
sock = self ._ssl_context .wrap_socket (sock , server_hostname = host )
440
+ connect_host = host
439
441
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
+
441
455
self ._open_sockets [key ] = sock
442
456
self ._socket_free [sock ] = False
443
457
return sock
@@ -446,11 +460,15 @@ def _get_socket(self, host, port, proto, *, timeout=1):
446
460
def _send (socket , data ):
447
461
total_sent = 0
448
462
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
450
468
if sent is None :
451
469
sent = len (data )
452
470
if sent == 0 :
453
- raise RuntimeError ( "Connection closed" )
471
+ raise _SendFailed ( )
454
472
total_sent += sent
455
473
456
474
def _send_request (self , socket , host , method , path , headers , data , json ):
@@ -532,12 +550,19 @@ def request(
532
550
self ._last_response .close ()
533
551
self ._last_response = None
534
552
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
541
566
542
567
resp = Response (socket , self ) # our response
543
568
if "location" in resp .headers and 300 <= resp .status_code <= 399 :
@@ -588,11 +613,9 @@ def __init__(self, socket, tls_mode):
588
613
def connect (self , address ):
589
614
"""connect wrapper to add non-standard mode parameter"""
590
615
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 )
596
619
597
620
class _FakeSSLContext :
598
621
def __init__ (self , iface ):
0 commit comments