Skip to content

Report a TLS close without close_notify as EOF - #845

Open
benubois wants to merge 1 commit into
httprb:mainfrom
benubois:eof
Open

Report a TLS close without close_notify as EOF#845
benubois wants to merge 1 commit into
httprb:mainfrom
benubois:eof

Conversation

@benubois

@benubois benubois commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

OpenSSL 3 raises SSL_read: unexpected eof while reading instead of signalling end of stream when a peer closes the connection without a close_notify alert. Nothing rescues it, so it escapes as a bare OpenSSL error.

Reproduction

require "http"

url = "https://troglobit.com/index.xml"

HTTP.get(url).to_s # => 200 ok

HTTP.use(:auto_inflate).headers(accept_encoding: "gzip, deflate").get(url).to_s
# => OpenSSL::SSL::SSLError: SSL_read: unexpected eof while reading

Seems to be related to :auto_inflate

request response headers
default Content-Length: 186934, Connection: close
gzip Content-Encoding: gzip, no Content-Length, no Transfer-Encoding

With a Content-Length the parser finishes on the length and never issues the read that would notice the close. Asking for gzip drops the Content-Length, so the body runs until the connection closes, and that read is the one that raises.

OpenSSL 3 raises "SSL_read: unexpected eof while reading" instead of
signalling end of stream when a peer closes the connection without a
close_notify alert. Nothing in the read path rescued it: the error is not
an EOFError, and OpenSSL::SSL::SSLError is not one of the IOError,
SocketError or SystemCallError that read_more converts, so it escaped as
a bare OpenSSL error that callers cannot rescue through HTTP::Error.

That also meant TLS never reached #check_premature_eof, so the same
server behaviour produced a clean body over http and an exception over
https. Reporting the close as EOF puts TLS back on that path, where the
response framing decides: a body delimited by connection close finishes
normally, one framed by Content-Length or chunked encoding that ends
early raises HTTP::ConnectionError. Other SSL errors still propagate.

Global#read_nonblock moves to Null so both polling handlers share one
translation, and nil already means EOF to each of their callers.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant