@@ -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,31 @@ 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+ if let Ok ( errno) = exc. as_object ( ) . get_attr ( "errno" , vm) {
1546+ if let Ok ( errno_int) = errno. try_int ( vm) {
1547+ if let Ok ( errno_val) = errno_int. try_to_primitive :: < i32 > ( vm) {
1548+ return errno_val == errors:: ECONNABORTED || errno_val == errors:: ECONNRESET ;
1549+ }
1550+ }
1551+ }
1552+ }
1553+ false
1554+ }
1555+
15311556/// Ensure TLS data is available for reading
15321557/// Returns the number of bytes read from the socket
15331558fn ssl_ensure_data_available (
@@ -1563,7 +1588,27 @@ fn ssl_ensure_data_available(
15631588 // else: non-blocking socket (timeout=0) or blocking socket (timeout=None) - skip select
15641589 }
15651590
1566- let data = socket. sock_recv ( 2048 , vm) . map_err ( SslError :: Py ) ?;
1591+ let data = match socket. sock_recv ( 2048 , vm) {
1592+ Ok ( data) => data,
1593+ Err ( e) => {
1594+ // Before returning socket error, check if rustls already has a queued TLS alert
1595+ // This mirrors CPython/OpenSSL behavior: SSL errors take precedence over socket errors
1596+ // On Windows, TCP RST may arrive before we read the alert, but rustls may have
1597+ // already received and buffered the alert from a previous read
1598+ if let Err ( rustls_err) = conn. process_new_packets ( ) {
1599+ return Err ( SslError :: from_rustls ( rustls_err) ) ;
1600+ }
1601+ // In SSL context, connection closed errors (ECONNABORTED, ECONNRESET) indicate
1602+ // unexpected connection termination - the peer closed without proper TLS shutdown.
1603+ // This is semantically equivalent to "EOF occurred in violation of protocol"
1604+ // because no close_notify alert was received.
1605+ // On Windows, TCP RST can arrive before we read the TLS alert, causing these errors.
1606+ if is_connection_closed_error ( & e, vm) {
1607+ return Err ( SslError :: Eof ) ;
1608+ }
1609+ return Err ( SslError :: Py ( e) ) ;
1610+ }
1611+ } ;
15671612
15681613 // Get the size of received data
15691614 let bytes_read = data
0 commit comments