fix: reset timeout callbacks when requests are routed to a different connection

this may happen in a few contexts, such as connection exhaustion, but more importantly, when a request is retried in a different connection; if the request successfully sets the callbacks before the connection raises an issue and the request is retried in a new one, the callback from the faulty connection are carried with it, and triggered at a time when the connection is back in the connection pool, or worse, used in a different thread

this fix relies on :idle transition callback, which is called before request is routed around
This commit is contained in:
HoneyryderChuck 2025-03-03 11:50:22 +00:00
parent 999b6a603a
commit 88f8f5d287
7 changed files with 18 additions and 6 deletions

View File

@ -4,7 +4,7 @@ module HTTPX
module Callbacks
def on(type, &action)
callbacks(type) << action
self
action
end
def once(type, &block)
@ -12,7 +12,6 @@ module HTTPX
block.call(*args, &callback)
:delete
end
self
end
def emit(type, *args)

View File

@ -903,13 +903,13 @@ module HTTPX
end
def set_request_timeout(label, request, timeout, start_event, finish_events, &callback)
request.once(start_event) do
request.set_timeout_callback(start_event) do
timer = @current_selector.after(timeout, callback)
request.active_timeouts << label
Array(finish_events).each do |event|
# clean up request timeouts if the connection errors out
request.once(event) do
request.set_timeout_callback(event) do
timer.cancel
request.active_timeouts.delete(label)
end

View File

@ -25,6 +25,7 @@ module HTTPX
class_eval(<<-MOD, __FILE__, __LINE__ + 1)
def on_#{meth}(&blk) # def on_connection_opened(&blk)
on(:#{meth}, &blk) # on(:connection_opened, &blk)
self # self
end # end
MOD
end

View File

@ -36,6 +36,7 @@ module HTTPX
class_eval(<<-MOD, __FILE__, __LINE__ + 1)
def on_#{meth}(&blk) # def on_circuit_open(&blk)
on(:#{meth}, &blk) # on(:circuit_open, &blk)
self # self
end # end
MOD
end

View File

@ -284,6 +284,15 @@ module HTTPX
def expects?
@headers["expect"] == "100-continue" && @informational_status == 100 && !@response
end
def set_timeout_callback(event, &callback)
clb = once(event, &callback)
# reset timeout callbacks when requests get rerouted to a different connection
once(:idle) do
callbacks(event).delete(clb)
end
end
end
end

View File

@ -4,8 +4,8 @@ module HTTPX
end
module Callbacks
def on: (Symbol) { (*untyped) -> void } -> self
def once: (Symbol) { (*untyped) -> void } -> self
def on: (Symbol) { (*untyped) -> void } -> ^(*untyped) -> void
def once: (Symbol) { (*untyped) -> void } -> ^(*untyped) -> void
def emit: (Symbol, *untyped) -> void
def callbacks_for?: (Symbol) -> bool

View File

@ -64,6 +64,8 @@ module HTTPX
def request_timeout: () -> Numeric?
def set_timeout_callback: (Symbol event) { (*untyped) -> void } -> void
private
def initialize_body: (Options options) -> Transcoder::_Encoder?