diff --git a/lib/net/http.rb b/lib/net/http.rb index 7fd4c3e..6673140 100644 --- a/lib/net/http.rb +++ b/lib/net/http.rb @@ -2495,10 +2495,17 @@ def begin_transport(req) debug 'Conn close because of keep_alive_timeout' @socket.close connect - elsif @socket.io.to_io.wait_readable(0) && @socket.eof? - debug "Conn close because of EOF" - @socket.close - connect + elsif @socket.io.to_io.wait_readable(0) + # Check for EOF without blocking. + # With TLS 1.3, servers may send NewSessionTicket after responses, + # making the socket appear readable when only handshake data is + # pending. Using eof? here would block waiting for app data. + # See: https://bugs.ruby-lang.org/issues/19017 + if eof_without_blocking? + debug "Conn close because of EOF" + @socket.close + connect + end end end @@ -2527,6 +2534,28 @@ def end_transport(req, res) end end + # Non-blocking EOF check for TLS connections. + # Returns true if connection is at EOF, false otherwise. + # Unlike @socket.eof?, this won't block when TLS 1.3 NewSessionTicket + # messages are pending on the connection. + def eof_without_blocking? + result = @socket.io.read_nonblock(1, exception: false) + case result + when nil + # EOF - connection was closed + true + when :wait_readable, :wait_writable + # No data available yet, but connection is alive + false + when String + # Got actual data - push it back for later reads + @socket.io.ungetc(result) + false + end + rescue EOFError + true + end + def keep_alive?(req, res) return false if req.connection_close? if @curr_http_version <= '1.0'