Skip to content

Commit 6c393a1

Browse files
authored
Merge pull request #614 from KennaSecurity/SUP-1886-update-veracode-code
SUP-1886-update veracode retry on 504
2 parents 119038c + adacfba commit 6c393a1

7 files changed

Lines changed: 825 additions & 30 deletions

File tree

lib/http.rb

Lines changed: 52 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -11,25 +11,46 @@ module Http
1111
Faraday::ConnectionFailed, Faraday::ClientError, Net::OpenTimeout, Errno::ECONNREFUSED, EOFError, Faraday::ServerError
1212
]
1313

14-
def connection(verify_ssl = true, max_retries = 5, hmac_client: nil)
14+
def connection(verify_ssl = true, max_retries = 5, hmac_client: nil, retry_options: nil)
15+
default_config = {
16+
max: max_retries,
17+
interval: 0.1,
18+
max_interval: 30,
19+
backoff_factor: 5,
20+
methods: %i[get post],
21+
exceptions: RETRY_EXCEPTIONS,
22+
retry_statuses: [429, 500, 502, 503, 504],
23+
retry_block: method(:log_retry),
24+
exhausted_retries_block: method(:log_retries_exhausted)
25+
}
26+
27+
priority_config = nil
28+
if retry_options
29+
# `retry_options` configures a dedicated retry policy for a specific set of
30+
# statuses (e.g. 504). It is applied as a separate, outer retry middleware so
31+
# those statuses get their own max/interval, while every other error keeps the
32+
# default behaviour below. The prioritised statuses are removed from the default
33+
# middleware so they are never retried twice.
34+
priority_statuses = Array(retry_options[:retry_statuses])
35+
priority_config = default_config.merge(retry_options).merge(
36+
methods: [], # rely solely on retry_if so only the prioritised statuses retry
37+
retry_if: ->(env, exception) { priority_statuses.include?(retry_status(env, exception)) }
38+
)
39+
40+
default_config[:retry_statuses] -= priority_statuses
41+
default_config[:methods] = []
42+
default_config[:retry_if] = ->(env, exception) { !priority_statuses.include?(retry_status(env, exception)) }
43+
end
44+
1545
Faraday.new do |faraday|
1646
faraday.request :multipart
1747
faraday.request :json
1848
faraday.ssl.verify = verify_ssl
1949
if @options && @options[:debug] == true
2050
faraday.response :logger # This logs to STDOUT by default
2151
end
22-
faraday.request :retry, {
23-
max: max_retries,
24-
interval: 0.1,
25-
max_interval: 30,
26-
backoff_factor: 5,
27-
methods: %i[get post],
28-
exceptions: RETRY_EXCEPTIONS,
29-
retry_statuses: [429, 500, 502, 503, 504],
30-
retry_block: method(:log_retry),
31-
exhausted_retries_block: method(:log_retries_exhausted)
32-
}
52+
faraday.request :retry, priority_config if priority_config
53+
faraday.request :retry, default_config
3354
if hmac_client
3455
require_relative './faraday_middlewares/faraday_hmac_middleware'
3556
faraday.use FaradayHmac, hmac_client
@@ -40,20 +61,31 @@ def connection(verify_ssl = true, max_retries = 5, hmac_client: nil)
4061
end
4162
end
4263

43-
def http_get(url, headers, max_retries = 5, verify_ssl = true, hmac_client: nil)
44-
connection(verify_ssl, max_retries, hmac_client:).run_request(:get, url, nil, headers)
64+
# Resolves the HTTP status for a retry decision. `raise_error` runs before the retry
65+
# middleware, so error responses arrive here as exceptions; we read the status from
66+
# the exception when present and fall back to the response env otherwise.
67+
def retry_status(env, exception)
68+
if exception.respond_to?(:response_status) && exception.response_status
69+
exception.response_status
70+
else
71+
env&.status
72+
end
73+
end
74+
75+
def http_get(url, headers, max_retries = 5, verify_ssl = true, hmac_client: nil, retry_options: nil)
76+
connection(verify_ssl, max_retries, hmac_client:, retry_options:).run_request(:get, url, nil, headers)
4577
end
4678

47-
def http_post(url, headers, payload, max_retries = 5, verify_ssl = true, hmac_client: nil)
48-
connection(verify_ssl, max_retries, hmac_client:).run_request(:post, url, payload, headers)
79+
def http_post(url, headers, payload, max_retries = 5, verify_ssl = true, hmac_client: nil, retry_options: nil)
80+
connection(verify_ssl, max_retries, hmac_client:, retry_options:).run_request(:post, url, payload, headers)
4981
end
5082

51-
def http_put(url, headers, payload, max_retries = 5, verify_ssl = true)
52-
connection(verify_ssl, max_retries).run_request(:put, url, payload, headers)
83+
def http_put(url, headers, payload, max_retries = 5, verify_ssl = true, retry_options: nil)
84+
connection(verify_ssl, max_retries, retry_options:).run_request(:put, url, payload, headers)
5385
end
5486

55-
def http_delete(url, headers, max_retries = 5, verify_ssl = true)
56-
connection(verify_ssl, max_retries).run_request(:delete, url, nil, headers)
87+
def http_delete(url, headers, max_retries = 5, verify_ssl = true, retry_options: nil)
88+
connection(verify_ssl, max_retries, retry_options:).run_request(:delete, url, nil, headers)
5789
end
5890

5991
def log_retry(retry_count:, exception:, will_retry_in:, **_kwargs)

spec/lib/http_spec.rb

Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
# frozen_string_literal: true
2+
3+
require "rspec_helper"
4+
require_relative "../../lib/http"
5+
6+
class TestHelper
7+
include Kenna::Toolkit::Helpers::Http
8+
9+
attr_reader :options
10+
11+
def initialize(options = {})
12+
@options = options
13+
end
14+
end
15+
16+
RSpec.describe Kenna::Toolkit::Helpers::Http do
17+
subject(:helper) { TestHelper.new }
18+
19+
describe "#connection" do
20+
context "without retry_options" do
21+
it "uses default retry configuration" do
22+
conn = helper.connection
23+
# Verify connection is created without errors
24+
expect(conn).to be_a(Faraday::Connection)
25+
end
26+
end
27+
28+
context "with custom retry_options" do
29+
it "merges retry_statuses instead of replacing them" do
30+
custom_options = {
31+
retry_statuses: [504]
32+
}
33+
conn = helper.connection(true, 5, retry_options: custom_options)
34+
expect(conn).to be_a(Faraday::Connection)
35+
end
36+
37+
it "preserves default exceptions while adding custom ones" do
38+
custom_options = {
39+
exceptions: [Faraday::TooManyRequestsError]
40+
}
41+
conn = helper.connection(true, 5, retry_options: custom_options)
42+
expect(conn).to be_a(Faraday::Connection)
43+
end
44+
45+
it "allows overriding other retry options" do
46+
custom_options = {
47+
interval: 60,
48+
max_interval: 60,
49+
backoff_factor: 2
50+
}
51+
conn = helper.connection(true, 5, retry_options: custom_options)
52+
expect(conn).to be_a(Faraday::Connection)
53+
end
54+
end
55+
56+
context "HMAC client support" do
57+
it "creates connection with HMAC middleware when hmac_client is provided" do
58+
mock_client = double("hmac_client")
59+
conn = helper.connection(true, 5, hmac_client: mock_client)
60+
expect(conn).to be_a(Faraday::Connection)
61+
end
62+
end
63+
64+
context "SSL verification" do
65+
it "disables SSL verification when verify_ssl is false" do
66+
conn = helper.connection(false)
67+
expect(conn).to be_a(Faraday::Connection)
68+
end
69+
end
70+
end
71+
72+
describe "#http_get" do
73+
it "makes GET requests" do
74+
stub_request(:get, "https://example.com/test")
75+
.to_return(body: "success", status: 200)
76+
77+
response = helper.http_get("https://example.com/test", {})
78+
expect(response.status).to eq(200)
79+
expect(response.body).to eq("success")
80+
end
81+
82+
it "accepts retry_options parameter" do
83+
stub_request(:get, "https://example.com/test")
84+
.to_return(body: "success", status: 200)
85+
86+
retry_opts = { retry_statuses: [504] }
87+
response = helper.http_get("https://example.com/test", {}, 5, true, retry_options: retry_opts)
88+
expect(response.status).to eq(200)
89+
end
90+
end
91+
92+
describe "#http_post" do
93+
it "makes POST requests with retry options" do
94+
stub_request(:post, "https://example.com/test")
95+
.to_return(body: "created", status: 201)
96+
97+
retry_opts = { retry_statuses: [504] }
98+
response = helper.http_post("https://example.com/test", {}, { foo: "bar" }, 5, true, retry_options: retry_opts)
99+
expect(response.status).to eq(201)
100+
expect(response.body).to eq("created")
101+
end
102+
end
103+
104+
describe "#http_put" do
105+
it "makes PUT requests with retry options" do
106+
stub_request(:put, "https://example.com/test")
107+
.to_return(body: "updated", status: 200)
108+
109+
retry_opts = { retry_statuses: [504] }
110+
response = helper.http_put("https://example.com/test", {}, { foo: "bar" }, 5, true, retry_options: retry_opts)
111+
expect(response.status).to eq(200)
112+
expect(response.body).to eq("updated")
113+
end
114+
end
115+
116+
describe "#http_delete" do
117+
it "makes DELETE requests with retry options" do
118+
stub_request(:delete, "https://example.com/test")
119+
.to_return(body: "deleted", status: 200)
120+
121+
retry_opts = { retry_statuses: [504] }
122+
response = helper.http_delete("https://example.com/test", {}, 5, true, retry_options: retry_opts)
123+
expect(response.status).to eq(200)
124+
expect(response.body).to eq("deleted")
125+
end
126+
end
127+
end

0 commit comments

Comments
 (0)