Skip to content

Commit 3aae91b

Browse files
Handle OpenSSL::SSL::SSLError in connection manager
Add SSL error handling to read/write operations so that transient SSL errors (like "SSL_read: unexpected eof while reading") trigger automatic reconnection and retry, similar to how EOFError and SystemCallError are handled. SSL errors during initial connection (handshake failures, certificate verification) continue to propagate as configuration errors. Fixes #1045 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 00e0c9f commit 3aae91b

4 files changed

Lines changed: 61 additions & 4 deletions

File tree

lib/dalli/protocol.rb

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,5 +15,15 @@ module Protocol
1515
else
1616
[Timeout::Error]
1717
end
18+
19+
# SSL errors that occur during read/write operations (not during initial
20+
# handshake) should trigger reconnection. These indicate transient network
21+
# issues, not configuration problems.
22+
SSL_ERRORS =
23+
if defined?(OpenSSL::SSL::SSLError)
24+
[OpenSSL::SSL::SSLError]
25+
else
26+
[]
27+
end
1828
end
1929
end

lib/dalli/protocol/base.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,7 @@ def pipeline_next_responses
106106
end
107107

108108
values
109-
rescue SystemCallError, *TIMEOUT_ERRORS, EOFError => e
109+
rescue SystemCallError, *TIMEOUT_ERRORS, *SSL_ERRORS, EOFError => e
110110
@connection_manager.error_on_request!(e)
111111
end
112112

lib/dalli/protocol/connection_manager.rb

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -150,19 +150,19 @@ def read_line
150150
data = @sock.gets("\r\n")
151151
error_on_request!('EOF in read_line') if data.nil?
152152
data
153-
rescue SystemCallError, *TIMEOUT_ERRORS, EOFError => e
153+
rescue SystemCallError, *TIMEOUT_ERRORS, *SSL_ERRORS, EOFError => e
154154
error_on_request!(e)
155155
end
156156

157157
def read(count)
158158
@sock.readfull(count)
159-
rescue SystemCallError, *TIMEOUT_ERRORS, EOFError => e
159+
rescue SystemCallError, *TIMEOUT_ERRORS, *SSL_ERRORS, EOFError => e
160160
error_on_request!(e)
161161
end
162162

163163
def write(bytes)
164164
@sock.write(bytes)
165-
rescue SystemCallError, *TIMEOUT_ERRORS => e
165+
rescue SystemCallError, *TIMEOUT_ERRORS, *SSL_ERRORS => e
166166
error_on_request!(e)
167167
end
168168

test/integration/test_network.rb

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -178,6 +178,53 @@
178178
end
179179
end
180180

181+
it 'handles SSL error during read operations' do
182+
with_nil_logger do
183+
memcached(p, 19_191) do |dc|
184+
# First set a value so we have something to get
185+
dc.set('ssl_test_key', 'test_value')
186+
187+
server = dc.send(:ring).server_for_key('ssl_test_key')
188+
189+
# Stub error_on_request! to verify SSLError triggers error handling
190+
# This confirms the SSL error is caught by the rescue clause
191+
error_handled = false
192+
original_error_on_request = server.instance_variable_get(:@connection_manager).method(:error_on_request!)
193+
194+
server.instance_variable_get(:@connection_manager).define_singleton_method(:error_on_request!) do |err|
195+
error_handled = true if err.is_a?(OpenSSL::SSL::SSLError)
196+
original_error_on_request.call(err)
197+
end
198+
199+
ssl_error = OpenSSL::SSL::SSLError.new('SSL_read: unexpected eof while reading')
200+
201+
# Binary protocol uses readfull for reading, meta protocol uses gets
202+
stub_method = p == :binary ? :readfull : :gets
203+
server.sock.stub(stub_method, proc { raise ssl_error }) do
204+
# The operation will retry with a new connection and may succeed
205+
# What matters is the SSLError is caught, not propagated
206+
dc.get('ssl_test_key')
207+
rescue Dalli::NetworkError
208+
# Expected if retries exhausted
209+
end
210+
211+
assert error_handled, 'SSLError should trigger error_on_request!'
212+
end
213+
end
214+
end
215+
216+
it 'handles SSL error during pipelined get' do
217+
with_nil_logger do
218+
memcached(p, 19_191) do |dc|
219+
ssl_error = OpenSSL::SSL::SSLError.new('SSL_read: unexpected eof while reading')
220+
221+
dc.send(:ring).server_for_key('abc').sock.stub(:write, proc { raise ssl_error }) do
222+
assert_empty dc.get_multi(['abc'])
223+
end
224+
end
225+
end
226+
end
227+
181228
it 'handles asynchronous Thread#raise' do
182229
with_nil_logger do
183230
memcached(p, 19_191) do |dc|

0 commit comments

Comments
 (0)