22
33# prefer slower Python-based io module
44import _pyio as io
5+ import errno
56import socket
7+ import sys
68
9+ from OpenSSL .SSL import SysCallError
10+
11+
12+ # Define a variable to hold the platform-specific "not a socket" error.
13+ _not_a_socket_err = None
14+
15+ if sys .platform == 'win32' :
16+ # On Windows, try to get the named constant from the socket module.
17+ # If that fails, fall back to the known numeric value.
18+ try :
19+ _not_a_socket_err = socket .WSAENOTSOCK
20+ except AttributeError :
21+ _not_a_socket_err = 10038
22+ else :
23+ # On other platforms, the relevant error is EBADF (Bad file descriptor),
24+ # which is already in your list of handled errors.
25+ pass
26+
27+ # Expose the error constant for use in the module's public API if needed.
28+ WSAENOTSOCK = _not_a_socket_err
729
830# Write only 16K at a time to sockets
931SOCK_WRITE_BLOCKSIZE = 16384
@@ -30,10 +52,59 @@ def _flush_unlocked(self):
3052 # ssl sockets only except 'bytes', not bytearrays
3153 # so perhaps we should conditionally wrap this for perf?
3254 n = self .raw .write (bytes (self ._write_buf ))
33- except io .BlockingIOError as e :
34- n = e .characters_written
55+ except (io .BlockingIOError , OSError , SysCallError ) as e :
56+ # Check for a different error attribute depending
57+ # on the exception type
58+ if isinstance (e , io .BlockingIOError ):
59+ n = e .characters_written
60+ else :
61+ error_code = (
62+ e .errno if isinstance (e , OSError ) else e .args [0 ]
63+ )
64+ if error_code in {
65+ errno .EBADF ,
66+ errno .ENOTCONN ,
67+ errno .EPIPE ,
68+ WSAENOTSOCK , # Windows-specific error
69+ }:
70+ # The socket is gone, so just ignore this error.
71+ return
72+ raise
73+ else :
74+ # The 'try' block completed without an exception
75+ if n is None :
76+ # This could happen with non-blocking write
77+ # when nothing was written
78+ break
79+
3580 del self ._write_buf [:n ]
3681
82+ def close (self ):
83+ """
84+ Close the stream and its underlying file object.
85+
86+ This method is designed to be idempotent (it can be called multiple
87+ times without side effects). It gracefully handles a race condition
88+ where the underlying socket may have already been closed by the remote
89+ client or another thread.
90+
91+ A SysCallError or OSError with errno.EBADF or errno.ENOTCONN is caught
92+ and ignored, as these indicate a normal, expected connection teardown.
93+ Other exceptions are re-raised.
94+ """
95+ if self .closed : # pylint: disable=W0125
96+ return
97+
98+ try :
99+ super ().close ()
100+ except (OSError , SysCallError ) as e :
101+ error_code = e .errno if isinstance (e , OSError ) else e .args [0 ]
102+ if error_code in {errno .EBADF , errno .ENOTCONN }:
103+ # The socket is already closed, which is expected during
104+ # a race condition.
105+ return
106+ raise
107+
37108
38109class StreamReader (io .BufferedReader ):
39110 """Socket stream reader."""
0 commit comments