From e3d8dd420fde317bbe52dce878d3ae2fbfe2fdf5 Mon Sep 17 00:00:00 2001 From: nobe4 Date: Tue, 13 Oct 2026 08:50:56 +0200 Subject: [PATCH 1/3] fix(github): catch network ReadTimeout Today we got a ERROR -- : Caught Net::ReadTimeout POSTing to https://api.github.com/graphql: Net::ReadTimeout with # This is not worthy of an ERROR; it's safe to just retry. Turns out we didn't have any rescue for this case, so I've added it. I've also flattened the file a little bit, having a global rescue list instead of splitting them. --- lib/entitlements/service/github.rb | 37 +++++++++++-------- lib/version.rb | 2 +- spec/unit/entitlements/service/github_spec.rb | 8 ++++ 3 files changed, 30 insertions(+), 17 deletions(-) diff --git a/lib/entitlements/service/github.rb b/lib/entitlements/service/github.rb index e0e06b2..104608e 100644 --- a/lib/entitlements/service/github.rb +++ b/lib/entitlements/service/github.rb @@ -392,23 +392,28 @@ def graphql_http_post_real(query) return { code: response.code.to_i, data: { "body" => response.body } } end - begin - data = JSON.parse(response.body) - if data.key?("errors") - # Synthesized 500 below triggers a retry, so log at WARN. Note: some GraphQL - # `errors` are permanent (bad query, auth, schema). The retry wrapper's final - # "Giving up" ERROR will surface persistent cases. - Entitlements.logger.warn "Errors reported: #{data['errors'].inspect}" - return { code: 500, data: } - end - { code: response.code.to_i, data: } - rescue JSON::ParserError => e - # JSON parse errors mean the API returned something we can't interpret. The - # synthesized 500 below triggers a retry, but the cause is more likely a real - # protocol/server problem than a transient network blip, so log at ERROR. - Entitlements.logger.error "#{e.class} #{e.message}: #{response.body.inspect}" - { code: 500, data: { "body" => response.body } } + data = JSON.parse(response.body) + if data.key?("errors") + # Synthesized 500 below triggers a retry, so log at WARN. Note: some GraphQL + # `errors` are permanent (bad query, auth, schema). The retry wrapper's final + # "Giving up" ERROR will surface persistent cases. + Entitlements.logger.warn "Errors reported: #{data['errors'].inspect}" + return { code: 500, data: } end + + { code: response.code.to_i, data: } + + rescue Net::ReadTimeout => e + # Defend against network blips, those can be safely retried via the + # synthesized 500 below, so log at WARN. + Entitlements.logger.warn "#{e.class} #{e.message}" + { code: 500, data: nil } + rescue JSON::ParserError => e + # JSON parse errors mean the API returned something we can't interpret. The + # synthesized 500 below triggers a retry, but the cause is more likely a real + # protocol/server problem than a transient network blip, so log at ERROR. + Entitlements.logger.error "#{e.class} #{e.message}: #{response.body.inspect}" + { code: 500, data: { "body" => response.body } } rescue => e # Catch-all for any unexpected exception (network blip OR local code bug). # We retry below via the synthesized 500, but log at ERROR because this diff --git a/lib/version.rb b/lib/version.rb index 0dd6539..7f933cc 100644 --- a/lib/version.rb +++ b/lib/version.rb @@ -2,6 +2,6 @@ module Entitlements module Version - VERSION = "1.2.5" + VERSION = "1.2.6" end end diff --git a/spec/unit/entitlements/service/github_spec.rb b/spec/unit/entitlements/service/github_spec.rb index 4403a94..ea90a58 100644 --- a/spec/unit/entitlements/service/github_spec.rb +++ b/spec/unit/entitlements/service/github_spec.rb @@ -450,6 +450,14 @@ expect(response).to eq(code: 200, data: answer) end + it "logs at warn and returns a retryable response for a read timeout" do + timeout = Net::ReadTimeout.new + stub_request(:post, "https://github.fake/api/v3/graphql").to_raise(timeout) + expect(logger).to receive(:warn).with("#{timeout.class} #{timeout.message}") + response = subject.send(:graphql_http_post_real, "nonsense") + expect(response).to eq(code: 500, data: nil) + end + it "logs at error and returns code=500 and exception message for an unhandled exception" do exc = StandardError.new("Oh no you don't") stub_request(:post, "https://github.fake/api/v3/graphql").to_raise(exc) From ab817d4940a6e5789def4a76f3d19f746cde0f5d Mon Sep 17 00:00:00 2001 From: nobe4 Date: Tue, 13 Oct 2026 08:54:13 +0200 Subject: [PATCH 2/3] fix(gem): run bundle to update the locked version --- Gemfile.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Gemfile.lock b/Gemfile.lock index 9019485..e5e595e 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,7 +1,7 @@ PATH remote: . specs: - entitlements-github-plugin (1.2.5) + entitlements-github-plugin (1.2.6) contracts (~> 0.17.0) faraday (~> 2.0) faraday-net_http_persistent (~> 2.3) From e09f54feeaf1e300cc6a5c50b956d4eb47d266fa Mon Sep 17 00:00:00 2001 From: nobe4 Date: Tue, 13 Oct 2026 09:01:33 +0200 Subject: [PATCH 3/3] fix(test): update specs to reflect new logic --- lib/entitlements/service/github.rb | 1 + spec/unit/entitlements/service/github_spec.rb | 6 +++--- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/lib/entitlements/service/github.rb b/lib/entitlements/service/github.rb index 104608e..7743a68 100644 --- a/lib/entitlements/service/github.rb +++ b/lib/entitlements/service/github.rb @@ -406,6 +406,7 @@ def graphql_http_post_real(query) rescue Net::ReadTimeout => e # Defend against network blips, those can be safely retried via the # synthesized 500 below, so log at WARN. + reset_graphql_http Entitlements.logger.warn "#{e.class} #{e.message}" { code: 500, data: nil } rescue JSON::ParserError => e diff --git a/spec/unit/entitlements/service/github_spec.rb b/spec/unit/entitlements/service/github_spec.rb index ea90a58..27defea 100644 --- a/spec/unit/entitlements/service/github_spec.rb +++ b/spec/unit/entitlements/service/github_spec.rb @@ -340,9 +340,9 @@ expect(replacement_http).to receive(:start) expect(replacement_http).to receive(:request).and_return(response) expect_any_instance_of(Object).to receive(:sleep).with(1).once - expect(logger).to receive(:error). - with("Caught Net::ReadTimeout POSTing to https://github.fake/api/v3/graphql: Net::ReadTimeout with \"execution expired\"") - expect(logger).to receive(:warn). + expect(logger).to receive(:warn).ordered. + with("Net::ReadTimeout Net::ReadTimeout with \"execution expired\"") + expect(logger).to receive(:warn).ordered. with("GraphQL failed on try 1 of 3. Will retry.") expect(subject.send(:graphql_http_post, query)).to eq(code: 200, data: answer)