Skip to content

Commit 2174a31

Browse files
toby676YOU54F
authored andcommitted
fix: retry HTTP requests on 5xx status codes (502, 503, 504)
Add RetriableHttpStatusError class that raises on 502/503/504 responses, which triggers automatic retry via the existing Retry.until_true mechanism. Add comprehensive tests with stderr output mocked to avoid noisy test output. This ensures transient server errors don't cause pact publication/verification to fail immediately.
1 parent 9f6e706 commit 2174a31

File tree

2 files changed

+122
-1
lines changed

2 files changed

+122
-1
lines changed

lib/pact/hal/http_client.rb

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,18 @@
66

77
module Pact
88
module Hal
9+
class RetriableHttpStatusError < StandardError
10+
RETRIABLE_STATUS_CODES = [502, 503, 504].freeze
11+
12+
attr_reader :status_code, :response_body
13+
14+
def initialize status_code, response_body
15+
@status_code = status_code
16+
@response_body = response_body
17+
super("HTTP #{status_code} error: #{response_body}")
18+
end
19+
end
20+
921
class HttpClient
1022
attr_accessor :username, :password, :verbose, :token
1123

@@ -65,9 +77,15 @@ def perform_request request, uri
6577
end
6678
http.verify_mode = OpenSSL::SSL::VERIFY_NONE
6779
end
68-
http.start do |http|
80+
result = http.start do |http|
6981
http.request request
7082
end
83+
84+
# Raise error on specific 5xx status codes to trigger retry
85+
status_code = result.code.to_i
86+
raise RetriableHttpStatusError.new(status_code, result.body) if RetriableHttpStatusError::RETRIABLE_STATUS_CODES.include?(status_code)
87+
88+
result
7189
end
7290
Response.new(response)
7391
end

spec/lib/pact/hal/http_client_spec.rb

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,109 @@ module Hal
8282
it "returns a response" do
8383
expect(do_get.body).to eq({"some" => "json"})
8484
end
85+
86+
context "when server returns 502 Bad Gateway" do
87+
before do
88+
allow(Retry).to receive(:until_true).and_call_original
89+
allow($stderr).to receive(:puts)
90+
end
91+
92+
let!(:request) do
93+
stub_request(:get, "http://example.org/").
94+
to_return(status: 502, body: "<html>Bad Gateway</html>").then.
95+
to_return(status: 200, body: response_body, headers: {'Content-Type' => 'application/json'})
96+
end
97+
98+
it "retries and succeeds on second attempt" do
99+
expect(do_get.body).to eq({"some" => "json"})
100+
expect(request).to have_been_made.times(2)
101+
end
102+
end
103+
104+
context "when server returns 503 Service Unavailable" do
105+
before do
106+
allow(Retry).to receive(:until_true).and_call_original
107+
allow($stderr).to receive(:puts)
108+
end
109+
110+
let!(:request) do
111+
stub_request(:get, "http://example.org/").
112+
to_return(status: 503, body: "<html>Service Unavailable</html>").then.
113+
to_return(status: 200, body: response_body, headers: {'Content-Type' => 'application/json'})
114+
end
115+
116+
it "retries and succeeds on second attempt" do
117+
expect(do_get.body).to eq({"some" => "json"})
118+
expect(request).to have_been_made.times(2)
119+
end
120+
end
121+
122+
context "when server returns 504 Gateway Timeout" do
123+
before do
124+
allow(Retry).to receive(:until_true).and_call_original
125+
allow($stderr).to receive(:puts)
126+
end
127+
128+
let!(:request) do
129+
stub_request(:get, "http://example.org/").
130+
to_return(status: 504, body: "<html>Gateway Timeout</html>").then.
131+
to_return(status: 200, body: response_body, headers: {'Content-Type' => 'application/json'})
132+
end
133+
134+
it "retries and succeeds on second attempt" do
135+
expect(do_get.body).to eq({"some" => "json"})
136+
expect(request).to have_been_made.times(2)
137+
end
138+
end
139+
140+
context "when server returns 500 Internal Server Error" do
141+
before do
142+
allow(Retry).to receive(:until_true).and_call_original
143+
end
144+
145+
let!(:request) do
146+
stub_request(:get, "http://example.org/").
147+
to_return(status: 500, body: '{"error": "internal server error"}', headers: {'Content-Type' => 'application/json'})
148+
end
149+
150+
it "does not retry on 500 status code" do
151+
expect(do_get.body).to eq({"error" => "internal server error"})
152+
expect(request).to have_been_made.times(1)
153+
end
154+
end
155+
156+
context "when server returns 404 Not Found" do
157+
before do
158+
allow(Retry).to receive(:until_true).and_call_original
159+
end
160+
161+
let!(:request) do
162+
stub_request(:get, "http://example.org/").
163+
to_return(status: 404, body: '{"error": "not found"}', headers: {'Content-Type' => 'application/json'})
164+
end
165+
166+
it "does not retry on 4xx status codes" do
167+
expect(do_get.body).to eq({"error" => "not found"})
168+
expect(request).to have_been_made.times(1)
169+
end
170+
end
171+
172+
context "when server returns 502 three times" do
173+
before do
174+
allow(Retry).to receive(:until_true).and_call_original
175+
allow($stderr).to receive(:puts)
176+
end
177+
178+
let!(:request) do
179+
stub_request(:get, "http://example.org/").
180+
to_return(status: 502, body: "<html>Bad Gateway</html>")
181+
end
182+
183+
it "raises RetriableHttpStatusError after max retries" do
184+
expect { do_get }.to raise_error(Pact::Hal::RetriableHttpStatusError, /HTTP 502 error/)
185+
expect(request).to have_been_made.times(3)
186+
end
187+
end
85188
end
86189

87190
describe "post" do

0 commit comments

Comments
 (0)