From f0b201f5caff3bfb8a307c1ecab1a8219cebc226 Mon Sep 17 00:00:00 2001 From: Jack Rothrock Date: Mon, 28 Sep 2026 10:33:41 -0600 Subject: [PATCH 1/3] Fix flaky HTTPX and Typhoeus instrument tests Stub external HTTP with WebMock instead of hitting real services (news.ycombinator.com, google.com, httpbin.org, example.com), which caused intermittent failures from network/DNS/rate-limit variance. Co-Authored-By: Claude Opus 4.8 (1M context) --- gems/instruments.gemfile | 1 + gems/typhoeus.gemfile | 1 + test/unit/instruments/httpx_test.rb | 38 ++++++++++++++++++++++---- test/unit/instruments/typhoeus_test.rb | 15 +++++++++- 4 files changed, 49 insertions(+), 6 deletions(-) diff --git a/gems/instruments.gemfile b/gems/instruments.gemfile index 7e0124de..2c25febd 100644 --- a/gems/instruments.gemfile +++ b/gems/instruments.gemfile @@ -7,6 +7,7 @@ gem 'moped' gem 'actionpack' gem 'actionview' gem 'httpx' +gem 'webmock' gem 'grape' # json 3.0 (released 2026-09) dropped the `quirks_mode` keyword that Grape 3's # ActiveSupport JSON formatter still passes, so any Grape 3 JSON request raises diff --git a/gems/typhoeus.gemfile b/gems/typhoeus.gemfile index 12dcfb9a..790a1e75 100644 --- a/gems/typhoeus.gemfile +++ b/gems/typhoeus.gemfile @@ -1,3 +1,4 @@ eval_gemfile("../Gemfile") gem 'typhoeus' +gem 'webmock' diff --git a/test/unit/instruments/httpx_test.rb b/test/unit/instruments/httpx_test.rb index ac48a023..5ff55190 100644 --- a/test/unit/instruments/httpx_test.rb +++ b/test/unit/instruments/httpx_test.rb @@ -4,17 +4,31 @@ require 'scout_apm/instruments/httpx' require 'httpx' + require 'webmock' class HTTPXTest < Minitest::Test + include WebMock::API + def setup + WebMock.enable! + WebMock.disable_net_connect! + @context = ScoutApm::AgentContext.new @recorder = FakeRecorder.new ScoutApm::Agent.instance.context.recorder = @recorder ScoutApm::Instruments::HTTPX.new(@context).install(prepend: false) end + def teardown + WebMock.reset! + WebMock.allow_net_connect! + WebMock.disable! + end def test_httpx + stub_request(:get, /news\.ycombinator\.com/).to_return(status: 200, body: "") + stub_request(:get, /google\.com/).to_return(status: 200, body: "") + responses = HTTPX.get( "https://news.ycombinator.com/news", "https://news.ycombinator.com/news?p=2", @@ -27,11 +41,18 @@ def test_httpx end def test_httpx_post_request + stub_request(:post, /httpbin\.org\/post/).to_return(status: 200, body: "") + HTTPX.post("https://httpbin.org/post", json: { test: "data" }) assert_recorded(@recorder, "HTTP", "POST", "httpbin.org/post") end def test_instruments_httpx_error_handling + # An error response still flows through Session#request, so the layer is + # started and stopped, and the request is recorded exactly once. + stub_request(:get, /thisshouldnotexistatall12345\.com/) + .to_return(status: 500, body: "") + begin HTTPX.get("https://thisshouldnotexistatall12345.com") rescue @@ -41,6 +62,10 @@ def test_instruments_httpx_error_handling end def test_httpx_request_retry + # A timed-out request is still dispatched through Session#request, so the + # layer is recorded even though the response errors. + stub_request(:get, /httpbin\.org\/delay/).to_timeout + begin HTTPX.with(timeout: { connect_timeout: 0.25, request_timeout: 0.25 }) .get("https://httpbin.org/delay/5") @@ -50,14 +75,17 @@ def test_httpx_request_retry end def test_multiple_plugins + stub_request(:get, /news\.ycombinator\.com/).to_return(status: 200, body: "") + stub_request(:get, "http://httpbin.org/redirect/2") + .to_return(status: 302, headers: { "Location" => "http://httpbin.org/redirect/1" }) + stub_request(:get, "http://httpbin.org/redirect/1") + .to_return(status: 302, headers: { "Location" => "http://httpbin.org/get" }) + stub_request(:get, "http://httpbin.org/get").to_return(status: 200, body: "") + session = HTTPX.plugin(:persistent).plugin(:follow_redirects) session.get("https://news.ycombinator.com/news") - begin - session.get("http://httpbin.org/redirect/2") - rescue - skip "httpbin.org not available" - end + session.get("http://httpbin.org/redirect/2") assert_equal 2, @recorder.requests.length, "Expected 2 requests to be recorded" end diff --git a/test/unit/instruments/typhoeus_test.rb b/test/unit/instruments/typhoeus_test.rb index bf59e05f..eec57206 100644 --- a/test/unit/instruments/typhoeus_test.rb +++ b/test/unit/instruments/typhoeus_test.rb @@ -4,15 +4,28 @@ require 'scout_apm/instruments/typhoeus' require 'typhoeus' + require 'webmock' class TyphoeusTest < Minitest::Test + include WebMock::API + def setup + WebMock.enable! + WebMock.disable_net_connect! + stub_request(:any, /example\.com/).to_return(status: 200, body: "") + @context = ScoutApm::AgentContext.new @recorder = FakeRecorder.new ScoutApm::Agent.instance.context.recorder = @recorder ScoutApm::Instruments::Typhoeus.new(@context).install(prepend: false) end + def teardown + WebMock.reset! + WebMock.allow_net_connect! + WebMock.disable! + end + def test_instruments_typhoeus_hydra hydra = Typhoeus::Hydra.new 2.times.map{ hydra.queue(Typhoeus::Request.new("example.com", followlocation: true)) } @@ -39,4 +52,4 @@ def assert_recorded(recorder, type, name, desc = nil) assert_equal desc, req.root_layer.desc if !desc.nil? end end -end \ No newline at end of file +end From de79c190001b11899c08dfbc47d99597726cae62 Mon Sep 17 00:00:00 2001 From: Jack Rothrock Date: Mon, 28 Sep 2026 10:45:16 -0600 Subject: [PATCH 2/3] Add webmock to Ruby 4 instruments gemfile The Ruby 4 instruments matrix uses instruments-ruby4.gemfile, which was missing webmock, so httpx_test failed to load. Co-Authored-By: Claude Opus 4.8 (1M context) --- gems/instruments-ruby4.gemfile | 1 + 1 file changed, 1 insertion(+) diff --git a/gems/instruments-ruby4.gemfile b/gems/instruments-ruby4.gemfile index 6831864e..c616132b 100644 --- a/gems/instruments-ruby4.gemfile +++ b/gems/instruments-ruby4.gemfile @@ -10,4 +10,5 @@ gem 'moped' gem 'actionpack' gem 'actionview' gem 'httpx' +gem 'webmock' gem 'grape' From 31e2c9af0de56112737307bad1c5b2351ba9974f Mon Sep 17 00:00:00 2001 From: Jack Rothrock Date: Mon, 28 Sep 2026 10:49:26 -0600 Subject: [PATCH 3/3] Reset thread-local request between instrument tests TyphoeusTest/HTTPXTest#setup overrode the base setup without calling super, so Thread.current[:scout_request] was never cleared. When a prior test left an open TrackedRequest on the thread, RequestManager.lookup reused it and the HTTP layer nested under the stale root instead of recording, causing intermittent "recorded no layers" failures dependent on test order/seed. Call super in setup/teardown to restore the reset. Co-Authored-By: Claude Opus 4.8 (1M context) --- test/unit/instruments/httpx_test.rb | 3 +++ test/unit/instruments/typhoeus_test.rb | 3 +++ 2 files changed, 6 insertions(+) diff --git a/test/unit/instruments/httpx_test.rb b/test/unit/instruments/httpx_test.rb index 5ff55190..dab38f93 100644 --- a/test/unit/instruments/httpx_test.rb +++ b/test/unit/instruments/httpx_test.rb @@ -10,6 +10,8 @@ class HTTPXTest < Minitest::Test include WebMock::API def setup + super # clears Thread.current[:scout_request] so a stale request from a + # prior test can't swallow this test's layers WebMock.enable! WebMock.disable_net_connect! @@ -23,6 +25,7 @@ def teardown WebMock.reset! WebMock.allow_net_connect! WebMock.disable! + super end def test_httpx diff --git a/test/unit/instruments/typhoeus_test.rb b/test/unit/instruments/typhoeus_test.rb index eec57206..f5630c14 100644 --- a/test/unit/instruments/typhoeus_test.rb +++ b/test/unit/instruments/typhoeus_test.rb @@ -10,6 +10,8 @@ class TyphoeusTest < Minitest::Test include WebMock::API def setup + super # clears Thread.current[:scout_request] so a stale request from a + # prior test can't swallow this test's layers WebMock.enable! WebMock.disable_net_connect! stub_request(:any, /example\.com/).to_return(status: 200, body: "") @@ -24,6 +26,7 @@ def teardown WebMock.reset! WebMock.allow_net_connect! WebMock.disable! + super end def test_instruments_typhoeus_hydra