@@ -2495,10 +2495,17 @@ def begin_transport(req)
24952495 debug 'Conn close because of keep_alive_timeout'
24962496 @socket . close
24972497 connect
2498- elsif @socket . io . to_io . wait_readable ( 0 ) && @socket . eof?
2499- debug "Conn close because of EOF"
2500- @socket . close
2501- connect
2498+ elsif @socket . io . to_io . wait_readable ( 0 )
2499+ # Check for EOF without blocking.
2500+ # With TLS 1.3, servers may send NewSessionTicket after responses,
2501+ # making the socket appear readable when only handshake data is
2502+ # pending. Using eof? here would block waiting for app data.
2503+ # See: https://bugs.ruby-lang.org/issues/19017
2504+ if eof_without_blocking?
2505+ debug "Conn close because of EOF"
2506+ @socket . close
2507+ connect
2508+ end
25022509 end
25032510 end
25042511
@@ -2527,6 +2534,28 @@ def end_transport(req, res)
25272534 end
25282535 end
25292536
2537+ # Non-blocking EOF check for TLS connections.
2538+ # Returns true if connection is at EOF, false otherwise.
2539+ # Unlike @socket.eof?, this won't block when TLS 1.3 NewSessionTicket
2540+ # messages are pending on the connection.
2541+ def eof_without_blocking?
2542+ result = @socket . io . read_nonblock ( 1 , exception : false )
2543+ case result
2544+ when nil
2545+ # EOF - connection was closed
2546+ true
2547+ when :wait_readable , :wait_writable
2548+ # No data available yet, but connection is alive
2549+ false
2550+ when String
2551+ # Got actual data - push it back for later reads
2552+ @socket . io . ungetc ( result )
2553+ false
2554+ end
2555+ rescue EOFError
2556+ true
2557+ end
2558+
25302559 def keep_alive? ( req , res )
25312560 return false if req . connection_close?
25322561 if @curr_http_version <= '1.0'
0 commit comments