@@ -24,8 +24,8 @@ use rustls::server::ServerConfig;
2424use rustls:: server:: ServerConnection ;
2525use rustls:: sign:: CertifiedKey ;
2626use rustpython_vm:: builtins:: PyBaseExceptionRef ;
27- use rustpython_vm:: function:: ArgBytesLike ;
2827use rustpython_vm:: convert:: IntoPyException ;
28+ use rustpython_vm:: function:: ArgBytesLike ;
2929use rustpython_vm:: { AsObject , PyObjectRef , PyPayload , PyResult , TryFromObject } ;
3030use std:: io:: Read ;
3131use std:: sync:: { Arc , Once } ;
@@ -1528,6 +1528,29 @@ fn ssl_read_tls_records(
15281528 Ok ( ( ) )
15291529}
15301530
1531+ /// Check if an exception is a connection closed error
1532+ /// In SSL context, these errors indicate unexpected connection termination without proper TLS shutdown
1533+ fn is_connection_closed_error ( exc : & PyBaseExceptionRef , vm : & VirtualMachine ) -> bool {
1534+ use rustpython_vm:: stdlib:: errno:: errors;
1535+
1536+ // Check for ConnectionAbortedError, ConnectionResetError (Python exception types)
1537+ if exc. fast_isinstance ( vm. ctx . exceptions . connection_aborted_error )
1538+ || exc. fast_isinstance ( vm. ctx . exceptions . connection_reset_error )
1539+ {
1540+ return true ;
1541+ }
1542+
1543+ // Also check OSError with specific errno values (ECONNABORTED, ECONNRESET)
1544+ if exc. fast_isinstance ( vm. ctx . exceptions . os_error )
1545+ && let Ok ( errno) = exc. as_object ( ) . get_attr ( "errno" , vm)
1546+ && let Ok ( errno_int) = errno. try_int ( vm)
1547+ && let Ok ( errno_val) = errno_int. try_to_primitive :: < i32 > ( vm)
1548+ {
1549+ return errno_val == errors:: ECONNABORTED || errno_val == errors:: ECONNRESET ;
1550+ }
1551+ false
1552+ }
1553+
15311554/// Ensure TLS data is available for reading
15321555/// Returns the number of bytes read from the socket
15331556fn ssl_ensure_data_available (
@@ -1563,7 +1586,27 @@ fn ssl_ensure_data_available(
15631586 // else: non-blocking socket (timeout=0) or blocking socket (timeout=None) - skip select
15641587 }
15651588
1566- let data = socket. sock_recv ( 2048 , vm) . map_err ( SslError :: Py ) ?;
1589+ let data = match socket. sock_recv ( 2048 , vm) {
1590+ Ok ( data) => data,
1591+ Err ( e) => {
1592+ // Before returning socket error, check if rustls already has a queued TLS alert
1593+ // This mirrors CPython/OpenSSL behavior: SSL errors take precedence over socket errors
1594+ // On Windows, TCP RST may arrive before we read the alert, but rustls may have
1595+ // already received and buffered the alert from a previous read
1596+ if let Err ( rustls_err) = conn. process_new_packets ( ) {
1597+ return Err ( SslError :: from_rustls ( rustls_err) ) ;
1598+ }
1599+ // In SSL context, connection closed errors (ECONNABORTED, ECONNRESET) indicate
1600+ // unexpected connection termination - the peer closed without proper TLS shutdown.
1601+ // This is semantically equivalent to "EOF occurred in violation of protocol"
1602+ // because no close_notify alert was received.
1603+ // On Windows, TCP RST can arrive before we read the TLS alert, causing these errors.
1604+ if is_connection_closed_error ( & e, vm) {
1605+ return Err ( SslError :: Eof ) ;
1606+ }
1607+ return Err ( SslError :: Py ( e) ) ;
1608+ }
1609+ } ;
15671610
15681611 // Get the size of received data
15691612 let bytes_read = data
0 commit comments