Skip to content

Commit 82b7638

Browse files
Fix Ruby SDK silently swallowing API errors (SIGN-17019)
Add nil guard on e.code before range comparison to prevent NoMethodError on timeout/connection failures, and re-raise unmatched ApiError exceptions so 5XX errors propagate instead of returning nil. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 31136f9 commit 82b7638

14 files changed

Lines changed: 415 additions & 67 deletions

integration_test.rb

Lines changed: 274 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,274 @@
1+
#!/usr/bin/env ruby
2+
# Integration test for verifying the fix in the Dropbox Sign Ruby SDK
3+
#
4+
# This script uses Typhoeus mocking (the SDK's HTTP client) to test error handling.
5+
# Run from the repo root:
6+
# cd repos/ruby && bundle install && cd ../..
7+
# ruby -I repos/ruby/lib integration_test.rb
8+
#
9+
# Or with bundler:
10+
# BUNDLE_GEMFILE=repos/ruby/Gemfile bundle exec ruby -I repos/ruby/lib integration_test.rb
11+
12+
$LOAD_PATH.unshift File.join(__dir__, 'repos/ruby/lib')
13+
14+
begin
15+
require 'dropbox-sign'
16+
require 'typhoeus'
17+
rescue LoadError => e
18+
puts "Missing dependency: #{e.message}"
19+
puts "\nTo run this test, install from the local SDK:"
20+
puts " cd repos/ruby && bundle install && cd ../.."
21+
puts " ruby -I repos/ruby/lib integration_test.rb"
22+
exit 1
23+
end
24+
25+
class SDKErrorHandlingTest
26+
def initialize
27+
@config = Dropbox::Sign::Configuration.default
28+
@config.username = "test_api_key"
29+
@api_client = Dropbox::Sign::ApiClient.new(@config)
30+
@account_api = Dropbox::Sign::AccountApi.new(@api_client)
31+
@test_results = []
32+
end
33+
34+
def run_all_tests
35+
puts "=" * 80
36+
puts "Dropbox Sign Ruby SDK - Error Handling Integration Test"
37+
puts "=" * 80
38+
puts ""
39+
40+
test_connection_failure
41+
test_server_error_500
42+
test_server_error_503
43+
test_timeout_error
44+
test_client_error_400
45+
test_client_error_404
46+
test_success_response
47+
48+
print_summary
49+
end
50+
51+
private
52+
53+
def test_connection_failure
54+
test("Connection failure (code=0, libcurl error)") do
55+
response = Typhoeus::Response.new(
56+
code: 0,
57+
return_message: "Connection refused"
58+
)
59+
Typhoeus.stub(/account/).and_return(response)
60+
61+
begin
62+
@account_api.account_get
63+
{ status: :fail, reason: "No error raised (returned nil)" }
64+
rescue Dropbox::Sign::ApiError => e
65+
if e.code == 0
66+
{ status: :pass, reason: "ApiError raised with code=0: #{e.message}" }
67+
else
68+
{ status: :fail, reason: "Wrong error code: #{e.code}" }
69+
end
70+
rescue => e
71+
{ status: :fail, reason: "Wrong exception: #{e.class}: #{e.message}" }
72+
end
73+
end
74+
end
75+
76+
def test_server_error_500
77+
test("Server error (HTTP 500)") do
78+
response = Typhoeus::Response.new(
79+
code: 500,
80+
body: '{"error":{"error_msg":"Internal server error","error_name":"ServerError"}}',
81+
headers: { 'Content-Type' => 'application/json' }
82+
)
83+
Typhoeus.stub(/account/).and_return(response)
84+
85+
begin
86+
@account_api.account_get
87+
{ status: :fail, reason: "No error raised (returned nil)" }
88+
rescue Dropbox::Sign::ApiError => e
89+
if e.code == 500
90+
{ status: :pass, reason: "ApiError raised with code=500" }
91+
else
92+
{ status: :fail, reason: "Wrong error code: #{e.code}" }
93+
end
94+
rescue => e
95+
{ status: :fail, reason: "Wrong exception: #{e.class}: #{e.message}" }
96+
end
97+
end
98+
end
99+
100+
def test_server_error_503
101+
test("Server error (HTTP 503)") do
102+
response = Typhoeus::Response.new(
103+
code: 503,
104+
body: '{"error":{"error_msg":"Service unavailable","error_name":"ServiceUnavailable"}}',
105+
headers: { 'Content-Type' => 'application/json' }
106+
)
107+
Typhoeus.stub(/account/).and_return(response)
108+
109+
begin
110+
@account_api.account_get
111+
{ status: :fail, reason: "No error raised (returned nil)" }
112+
rescue Dropbox::Sign::ApiError => e
113+
if e.code == 503
114+
{ status: :pass, reason: "ApiError raised with code=503" }
115+
else
116+
{ status: :fail, reason: "Wrong error code: #{e.code}" }
117+
end
118+
rescue => e
119+
{ status: :fail, reason: "Wrong exception: #{e.class}: #{e.message}" }
120+
end
121+
end
122+
end
123+
124+
def test_timeout_error
125+
test("Timeout error") do
126+
response = Typhoeus::Response.new(
127+
code: 0,
128+
return_message: "Timeout was reached",
129+
mock: true
130+
)
131+
response.instance_variable_set(:@options, response.instance_variable_get(:@options).merge(return_code: :operation_timedout))
132+
Typhoeus.stub(/account/).and_return(response)
133+
134+
begin
135+
@account_api.account_get
136+
{ status: :fail, reason: "No error raised" }
137+
rescue Dropbox::Sign::ApiError => e
138+
{ status: :pass, reason: "ApiError raised (code=#{e.code.inspect}, msg=#{e.message})" }
139+
rescue NoMethodError => e
140+
{ status: :fail, reason: "NoMethodError (nil comparison bug): #{e.message}" }
141+
rescue => e
142+
{ status: :fail, reason: "Unexpected: #{e.class}: #{e.message}" }
143+
end
144+
end
145+
end
146+
147+
def test_client_error_400
148+
test("Client error (HTTP 400)") do
149+
response = Typhoeus::Response.new(
150+
code: 400,
151+
body: '{"error":{"error_msg":"Bad request","error_name":"BadRequest"}}',
152+
headers: { 'Content-Type' => 'application/json' }
153+
)
154+
Typhoeus.stub(/account/).and_return(response)
155+
156+
begin
157+
@account_api.account_get
158+
{ status: :fail, reason: "No error raised" }
159+
rescue Dropbox::Sign::ApiError => e
160+
if e.code == 400
161+
{ status: :pass, reason: "ApiError raised with code=400" }
162+
else
163+
{ status: :fail, reason: "Wrong error code: #{e.code}" }
164+
end
165+
rescue => e
166+
{ status: :fail, reason: "Wrong exception: #{e.class}: #{e.message}" }
167+
end
168+
end
169+
end
170+
171+
def test_client_error_404
172+
test("Client error (HTTP 404)") do
173+
response = Typhoeus::Response.new(
174+
code: 404,
175+
body: '{"error":{"error_msg":"Not found","error_name":"NotFound"}}',
176+
headers: { 'Content-Type' => 'application/json' }
177+
)
178+
Typhoeus.stub(/account/).and_return(response)
179+
180+
begin
181+
@account_api.account_get
182+
{ status: :fail, reason: "No error raised" }
183+
rescue Dropbox::Sign::ApiError => e
184+
if e.code == 404
185+
{ status: :pass, reason: "ApiError raised with code=404" }
186+
else
187+
{ status: :fail, reason: "Wrong error code: #{e.code}" }
188+
end
189+
rescue => e
190+
{ status: :fail, reason: "Wrong exception: #{e.class}: #{e.message}" }
191+
end
192+
end
193+
end
194+
195+
def test_success_response
196+
test("Success response (HTTP 200)") do
197+
response = Typhoeus::Response.new(
198+
code: 200,
199+
body: '{"account":{"account_id":"abc123","email_address":"test@example.com"}}',
200+
headers: { 'Content-Type' => 'application/json' }
201+
)
202+
Typhoeus.stub(/account/).and_return(response)
203+
204+
begin
205+
result = @account_api.account_get
206+
if result && result.account
207+
{ status: :pass, reason: "Request succeeded, got account_id=#{result.account.account_id}" }
208+
else
209+
{ status: :fail, reason: "Unexpected result: #{result.inspect}" }
210+
end
211+
rescue => e
212+
{ status: :fail, reason: "Unexpected error: #{e.class}: #{e.message}" }
213+
end
214+
end
215+
end
216+
217+
def test(name)
218+
Typhoeus::Expectation.clear
219+
print " Testing: #{name}..."
220+
221+
result = yield
222+
@test_results << result.merge(name: name)
223+
224+
case result[:status]
225+
when :pass
226+
puts " PASS"
227+
puts " #{result[:reason]}" if result[:reason]
228+
when :fail
229+
puts " FAIL"
230+
puts " #{result[:reason]}" if result[:reason]
231+
when :warn
232+
puts " WARN"
233+
puts " #{result[:reason]}" if result[:reason]
234+
end
235+
end
236+
237+
def print_summary
238+
passed = @test_results.count { |r| r[:status] == :pass }
239+
failed = @test_results.count { |r| r[:status] == :fail }
240+
warned = @test_results.count { |r| r[:status] == :warn }
241+
total = @test_results.size
242+
243+
puts ""
244+
puts "=" * 80
245+
puts "SUMMARY"
246+
puts "=" * 80
247+
puts " Total: #{total}"
248+
puts " Passed: #{passed}"
249+
puts " Failed: #{failed}"
250+
puts " Warned: #{warned}"
251+
puts ""
252+
253+
if failed > 0
254+
puts "FAILED TESTS:"
255+
@test_results.select { |r| r[:status] == :fail }.each do |r|
256+
puts " - #{r[:name]}"
257+
puts " Reason: #{r[:reason]}"
258+
end
259+
puts ""
260+
puts "The bug is NOT fixed."
261+
exit 1
262+
elsif warned > 0
263+
puts "Some tests raised warnings. Review the output above."
264+
exit 0
265+
else
266+
puts "All tests passed! The fix is working correctly."
267+
exit 0
268+
end
269+
end
270+
end
271+
272+
# Run tests
273+
tester = SDKErrorHandlingTest.new
274+
tester.run_all_tests

sdks/ruby/lib/dropbox-sign/api/account_api.rb

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,7 @@ def account_create_with_http_info(account_create_request, opts = {})
111111
range_code = "4XX".split('').first
112112
range_code_left = "#{range_code}00".to_i
113113
range_code_right = "#{range_code}99".to_i
114-
if e.code >= range_code_left && e.code <= range_code_right
114+
if e.code && e.code >= range_code_left && e.code <= range_code_right
115115
body = @api_client.convert_to_type(
116116
JSON.parse("[#{e.response_body}]", :symbolize_names => true)[0],
117117
"Dropbox::Sign::ErrorResponse"
@@ -123,6 +123,7 @@ def account_create_with_http_info(account_create_request, opts = {})
123123
e.message
124124
end
125125

126+
raise e
126127
end
127128

128129
if @api_client.config.debugging
@@ -205,7 +206,7 @@ def account_get_with_http_info(opts = {})
205206
range_code = "4XX".split('').first
206207
range_code_left = "#{range_code}00".to_i
207208
range_code_right = "#{range_code}99".to_i
208-
if e.code >= range_code_left && e.code <= range_code_right
209+
if e.code && e.code >= range_code_left && e.code <= range_code_right
209210
body = @api_client.convert_to_type(
210211
JSON.parse("[#{e.response_body}]", :symbolize_names => true)[0],
211212
"Dropbox::Sign::ErrorResponse"
@@ -217,6 +218,7 @@ def account_get_with_http_info(opts = {})
217218
e.message
218219
end
219220

221+
raise e
220222
end
221223

222224
if @api_client.config.debugging
@@ -314,7 +316,7 @@ def account_update_with_http_info(account_update_request, opts = {})
314316
range_code = "4XX".split('').first
315317
range_code_left = "#{range_code}00".to_i
316318
range_code_right = "#{range_code}99".to_i
317-
if e.code >= range_code_left && e.code <= range_code_right
319+
if e.code && e.code >= range_code_left && e.code <= range_code_right
318320
body = @api_client.convert_to_type(
319321
JSON.parse("[#{e.response_body}]", :symbolize_names => true)[0],
320322
"Dropbox::Sign::ErrorResponse"
@@ -326,6 +328,7 @@ def account_update_with_http_info(account_update_request, opts = {})
326328
e.message
327329
end
328330

331+
raise e
329332
end
330333

331334
if @api_client.config.debugging
@@ -423,7 +426,7 @@ def account_verify_with_http_info(account_verify_request, opts = {})
423426
range_code = "4XX".split('').first
424427
range_code_left = "#{range_code}00".to_i
425428
range_code_right = "#{range_code}99".to_i
426-
if e.code >= range_code_left && e.code <= range_code_right
429+
if e.code && e.code >= range_code_left && e.code <= range_code_right
427430
body = @api_client.convert_to_type(
428431
JSON.parse("[#{e.response_body}]", :symbolize_names => true)[0],
429432
"Dropbox::Sign::ErrorResponse"
@@ -435,6 +438,7 @@ def account_verify_with_http_info(account_verify_request, opts = {})
435438
e.message
436439
end
437440

441+
raise e
438442
end
439443

440444
if @api_client.config.debugging

0 commit comments

Comments
 (0)