5353__version__ = "0.0.0-auto.0"
5454__repo__ = "https://github.com/adafruit/Adafruit_CircuitPython_Requests.git"
5555
56+ import errno
5657
5758class _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
7679class 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
597620class _FakeSSLContext :
598621 def __init__ (self , iface ):
0 commit comments