Skip to content

Commit bb0753c

Browse files
committed
Verify http connection abort on thread kill
1 parent 1bfca0a commit bb0753c

1 file changed

Lines changed: 74 additions & 0 deletions

File tree

test/thread_safety_test.rb

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
# frozen_string_literal: true
22

33
require_relative "test_helper"
4+
require "socket"
45

56
class ThreadSafetyTest < Minitest::Test
67
def test_gvl_released_during_network_io
@@ -91,4 +92,77 @@ def test_thread_kill_cancels_request
9192
"Thread.kill took #{elapsed.round(2)}s to interrupt the request " \
9293
"(expected < 3s; cancellation may not be working)"
9394
end
95+
96+
def test_thread_kill_aborts_inflight_connection
97+
# Use a local TCP server to prove the HTTP connection is actually
98+
# closed (not just that the Ruby thread exits). The server accepts
99+
# the connection, reads the request, then tries to keep writing to
100+
# the socket. Once the client aborts, the write will fail with a
101+
# broken-pipe / connection-reset error.
102+
server = TCPServer.new("127.0.0.1", 0)
103+
port = server.addr[1]
104+
105+
server_error = nil
106+
client_disconnected = false
107+
chunks_sent = 0
108+
109+
server_thread = Thread.new do
110+
conn = server.accept
111+
request = +""
112+
loop do
113+
request << conn.readpartial(4096)
114+
break if request.include?("\r\n\r\n")
115+
end
116+
117+
conn.write "HTTP/1.1 200 OK\r\nTransfer-Encoding: chunked\r\n\r\n"
118+
119+
begin
120+
50.times do
121+
conn.write "5\r\nhello\r\n"
122+
chunks_sent += 1
123+
sleep 0.1
124+
end
125+
# If we get here, the client never disconnected
126+
conn.write "0\r\n\r\n"
127+
rescue Errno::EPIPE, Errno::ECONNRESET, Errno::EPROTOTYPE, IOError
128+
client_disconnected = true
129+
ensure
130+
conn.close rescue nil
131+
end
132+
end
133+
134+
client = Wreq::Client.new(
135+
timeout: 30,
136+
emulation: false,
137+
verify_cert: false,
138+
verify_host: false,
139+
http1_only: true
140+
)
141+
142+
request_thread = Thread.new do
143+
client.get("http://127.0.0.1:#{port}/slow")
144+
rescue => e
145+
# Expected — request interrupted
146+
e
147+
end
148+
149+
sleep 0.5
150+
151+
request_thread.kill
152+
request_thread.join(5)
153+
154+
server_thread.join(5)
155+
156+
assert client_disconnected,
157+
"Expected the server to detect a client disconnect (broken pipe) " \
158+
"after Thread.kill, but the connection was not aborted"
159+
160+
assert chunks_sent >= 1,
161+
"Expected at least 1 chunk to be sent before disconnect, got #{chunks_sent}"
162+
assert chunks_sent < 10,
163+
"Expected the connection to be aborted before 10 chunks were sent, " \
164+
"but #{chunks_sent} chunks were sent"
165+
ensure
166+
server&.close rescue nil
167+
end
94168
end

0 commit comments

Comments
 (0)