From 2e3b6c6ea9b7fc245b0c447f01d26d1750c318b3 Mon Sep 17 00:00:00 2001 From: gngpp Date: Sat, 25 Jul 2026 12:27:50 +0800 Subject: [PATCH] fix: restore Wreq::InterruptError --- lib/wreq_ruby/error.rb | 4 +++ src/client/body/stream.rs | 2 +- src/error.rs | 15 +++++++++-- src/rt.rs | 2 +- test/error_handling_test.rb | 54 +++++++++++++++++++++++++++++++++++++ 5 files changed, 73 insertions(+), 4 deletions(-) diff --git a/lib/wreq_ruby/error.rb b/lib/wreq_ruby/error.rb index 65dd4dc..20fe041 100644 --- a/lib/wreq_ruby/error.rb +++ b/lib/wreq_ruby/error.rb @@ -2,6 +2,10 @@ unless defined?(Wreq) module Wreq + # Keep interruption outside StandardError so a broad transport rescue + # never swallows a Ruby interrupt. + class InterruptError < Interrupt; end + # System-level and runtime errors # Memory allocation failed. diff --git a/src/client/body/stream.rs b/src/client/body/stream.rs index 88c194b..91d59ca 100644 --- a/src/client/body/stream.rs +++ b/src/client/body/stream.rs @@ -113,7 +113,7 @@ impl BodySender { /// # Errors /// /// Returns `IOError` after either channel side has closed. An interrupted - /// wait raises Ruby's standard `Interrupt` exception. + /// wait raises `Wreq::InterruptError`. pub fn push(ruby: &Ruby, rb_self: &Self, data: RString) -> Result<(), Error> { // Clone during the shared borrow, then release it before waiting // for capacity. Request attachment needs a mutable borrow. diff --git a/src/error.rs b/src/error.rs index 15fa60e..f97a5f9 100644 --- a/src/error.rs +++ b/src/error.rs @@ -84,14 +84,18 @@ define_exception!(DECODING_ERROR, "DecodingError", exception_runtime_error); // Configuration and builder errors define_exception!(BUILDER_ERROR, "BuilderError", exception_runtime_error); +// Keep interruption outside StandardError so a broad transport rescue +// never swallows a Ruby interrupt. +define_exception!(INTERRUPT_ERROR, "InterruptError", exception_interrupt); + /// Memory error constant pub fn memory_error(ruby: &Ruby) -> MagnusError { MagnusError::new(ruby.get_inner(&MEMORY), RACE_CONDITION_ERROR_MSG) } -/// Create Ruby's standard thread interruption error. +/// Create a `Wreq::InterruptError` when Ruby interrupts a request. pub fn interrupt_error(ruby: &Ruby) -> MagnusError { - MagnusError::new(ruby.exception_interrupt(), "request interrupted") + MagnusError::new(ruby.get_inner(&INTERRUPT_ERROR), "request interrupted") } /// Map a Tokio runtime initialization failure to `Wreq::BuilderError`. @@ -312,5 +316,12 @@ pub fn include(ruby: &Ruby, gem_module: &RModule) -> Result<(), MagnusError> { "BuilderError", exception_runtime_error ); + initialize_exception!( + ruby, + gem_module, + INTERRUPT_ERROR, + "InterruptError", + exception_interrupt + ); Ok(()) } diff --git a/src/rt.rs b/src/rt.rs index 28feba9..f3cf9c3 100644 --- a/src/rt.rs +++ b/src/rt.rs @@ -29,7 +29,7 @@ enum BlockOnError { /// # Errors /// /// Returns `Wreq::BuilderError` if the Tokio runtime cannot be initialized, -/// Ruby's standard `Interrupt` if Ruby interrupts the request, or the error produced +/// `Wreq::InterruptError` if Ruby interrupts the request, or the error produced /// by `map_err` if the future fails. pub fn try_block_on(ruby: &Ruby, future: F, map_err: M) -> Result where diff --git a/test/error_handling_test.rb b/test/error_handling_test.rb index 692c3fb..e229dba 100644 --- a/test/error_handling_test.rb +++ b/test/error_handling_test.rb @@ -1,6 +1,37 @@ require "test_helper" +require "socket" +require "timeout" class ErrorHandlingTest < Minitest::Test + def test_interrupt_error_stays_outside_standard_error + assert_equal Interrupt, Wreq::InterruptError.superclass + refute_operator Wreq::InterruptError, :<, StandardError + end + + def test_request_interruption_raises_interrupt_error + request_thread = nil + with_hanging_server do |url, accepted| + request_thread = Thread.new do + Wreq.get(url, timeout: 60) + rescue Interrupt, StandardError => error + error + end + request_thread.report_on_exception = false + + Timeout.timeout(5) { accepted.pop } + request_thread.wakeup + + assert request_thread.join(5), "Interrupted request thread should stop" + + error = request_thread.value + assert_instance_of Wreq::InterruptError, error + refute_kind_of StandardError, error + end + ensure + request_thread&.kill + request_thread&.join(1) + end + def test_network_error_handling # Try to connect to a non-existent domain response = Wreq.get("https://definitely-not-a-real-domain-12345.com") @@ -89,4 +120,27 @@ def test_proxy_error_handling end end end + + private + + def with_hanging_server + server = TCPServer.new("127.0.0.1", 0) + accepted = Queue.new + thread = Thread.new do + socket = server.accept + accepted << true + sleep + rescue IOError, SystemCallError + nil + ensure + socket&.close unless socket&.closed? + end + thread.report_on_exception = false + + yield "http://127.0.0.1:#{server.addr[1]}/", accepted + ensure + server&.close unless server&.closed? + thread&.kill + thread&.join(1) + end end