Skip to content

Commit a051e7c

Browse files
committed
Assign Net::HTTPRequest to exceptions
With the changes made in [2ea5401][], the underlying [Net::HTTPRequest][] instance is available to the connection throughout the lifetime of the request. With that instance available, exceptions raised during the request-response handshake can be constructed with the `Net::HTTPRequest` instance. Access to the `Net::HTTPRequest` from code that rescues from those exceptions is valuable: * for debugging purposes * for observability and logging * for retrying requests, etc. However, the exception classes are documented and therefore their constructors are part of the public API. Inserting a `request` positional argument is a breaking change. To account for that, this commit includes deprecations for subclasses that call `super` from within `#initialize` to instruct them on changing the signature to account for the `Net::HTTPRequest` instance. [2ea5401]: 2ea5401 [Net::HTTPRequest]: https://docs.ruby-lang.org/en/master/Net/HTTPRequest.html
1 parent 4fffccc commit a051e7c

File tree

5 files changed

+73
-32
lines changed

5 files changed

+73
-32
lines changed

Gemfile

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ gem "rubocop-performance"
1818
gem "rubocop-rails"
1919
gem "rubocop-rails-omakase"
2020

21+
gem "minitest", "< 6"
2122
gem "minitest-bisect"
2223

2324
gemspec

lib/active_resource/base.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1509,7 +1509,7 @@ def save
15091509
# of the <tt>before_*</tt> callbacks throw +:abort+ the action is
15101510
# cancelled and <tt>save!</tt> raises ActiveResource::ResourceInvalid.
15111511
def save!
1512-
save || raise(ResourceInvalid.new(self))
1512+
save || raise(ResourceInvalid.new(nil, self))
15131513
end
15141514

15151515
##

lib/active_resource/connection.rb

Lines changed: 21 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -136,52 +136,52 @@ def request(method, path, *arguments)
136136
payload[:request] = request
137137
payload[:result] = http.request(request)
138138
end
139-
handle_response(result)
139+
handle_response(request, result)
140140
rescue Timeout::Error => e
141-
raise TimeoutError.new(e.message)
141+
raise TimeoutError.new(request, e.message)
142142
rescue OpenSSL::SSL::SSLError => e
143-
raise SSLError.new(e.message)
143+
raise SSLError.new(request, e.message)
144144
rescue Errno::ECONNREFUSED => e
145-
raise ConnectionRefusedError.new(e.message)
145+
raise ConnectionRefusedError.new(request, e.message)
146146
end
147147

148148
# Handles response and error codes from the remote service.
149-
def handle_response(response)
149+
def handle_response(request, response)
150150
case response.code.to_i
151151
when 301, 302, 303, 307
152-
raise(Redirection.new(response))
152+
raise(Redirection.new(request, response))
153153
when 200...400
154154
response
155155
when 400
156-
raise(BadRequest.new(response))
156+
raise(BadRequest.new(request, response))
157157
when 401
158-
raise(UnauthorizedAccess.new(response))
158+
raise(UnauthorizedAccess.new(request, response))
159159
when 402
160-
raise(PaymentRequired.new(response))
160+
raise(PaymentRequired.new(request, response))
161161
when 403
162-
raise(ForbiddenAccess.new(response))
162+
raise(ForbiddenAccess.new(request, response))
163163
when 404
164-
raise(ResourceNotFound.new(response))
164+
raise(ResourceNotFound.new(request, response))
165165
when 405
166-
raise(MethodNotAllowed.new(response))
166+
raise(MethodNotAllowed.new(request, response))
167167
when 409
168-
raise(ResourceConflict.new(response))
168+
raise(ResourceConflict.new(request, response))
169169
when 410
170-
raise(ResourceGone.new(response))
170+
raise(ResourceGone.new(request, response))
171171
when 412
172-
raise(PreconditionFailed.new(response))
172+
raise(PreconditionFailed.new(request, response))
173173
when 422
174-
raise(ResourceInvalid.new(response))
174+
raise(ResourceInvalid.new(request, response))
175175
when 429
176-
raise(TooManyRequests.new(response))
176+
raise(TooManyRequests.new(request, response))
177177
when 451
178-
raise(UnavailableForLegalReasons.new(response))
178+
raise(UnavailableForLegalReasons.new(request, response))
179179
when 401...500
180-
raise(ClientError.new(response))
180+
raise(ClientError.new(request, response))
181181
when 500...600
182-
raise(ServerError.new(response))
182+
raise(ServerError.new(request, response))
183183
else
184-
raise(ConnectionError.new(response, "Unknown response code: #{response.code}"))
184+
raise(ConnectionError.new(request, response, "Unknown response code: #{response.code}"))
185185
end
186186
end
187187

lib/active_resource/exceptions.rb

Lines changed: 41 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,19 @@
22

33
module ActiveResource
44
class ConnectionError < StandardError # :nodoc:
5-
attr_reader :response
5+
attr_reader :request, :response
66

7-
def initialize(response, message = nil)
7+
def initialize(request, response = nil, message = nil)
8+
if request.is_a?(Net::HTTPResponse) && (response.is_a?(String) || response.nil?)
9+
ActiveResource.deprecator.warn(<<~WARN)
10+
ConnectionError subclasses must be constructed with a request. Call super with a Net::HTTPRequest instance as the first argument.
11+
WARN
12+
13+
message = response
14+
response, request = request, nil
15+
end
16+
17+
@request = request
818
@response = response
919
@message = message
1020
end
@@ -13,6 +23,7 @@ def to_s
1323
return @message if @message
1424

1525
message = +"Failed."
26+
message << " Request = #{request.method} #{request.uri}." if request.respond_to?(:method) && request.respond_to?(:uri)
1627
message << " Response code = #{response.code}." if response.respond_to?(:code)
1728
message << " Response message = #{response.message}." if response.respond_to?(:message)
1829
message
@@ -21,22 +32,47 @@ def to_s
2132

2233
# Raised when a Timeout::Error occurs.
2334
class TimeoutError < ConnectionError
24-
def initialize(message)
35+
def initialize(request, message = nil)
36+
if request.is_a?(String)
37+
ActiveResource.deprecator.warn(<<~WARN)
38+
TimeoutError subclasses must be constructed with a request. Call super with a Net::HTTPRequest instance as the first argument.
39+
WARN
40+
41+
message, request = request, nil
42+
end
43+
44+
@request = request
2545
@message = message
2646
end
2747
def to_s; @message ; end
2848
end
2949

3050
# Raised when a OpenSSL::SSL::SSLError occurs.
3151
class SSLError < ConnectionError
32-
def initialize(message)
52+
def initialize(request, message = nil)
53+
if request.is_a?(String)
54+
ActiveResource.deprecator.warn(<<~WARN)
55+
SSLError subclasses must be constructed with a request. Call super with a Net::HTTPRequest instance as the first argument.
56+
WARN
57+
58+
message, request = request, nil
59+
end
60+
61+
@request = request
3362
@message = message
3463
end
3564
def to_s; @message ; end
3665
end
3766

3867
# Raised when a Errno::ECONNREFUSED occurs.
39-
class ConnectionRefusedError < Errno::ECONNREFUSED; end
68+
class ConnectionRefusedError < Errno::ECONNREFUSED
69+
attr_reader :request
70+
71+
def initialize(request, message)
72+
@request = request
73+
super(message)
74+
end
75+
end
4076

4177
# 3xx Redirection
4278
class Redirection < ConnectionError # :nodoc:

test/cases/connection_test.rb

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -243,7 +243,8 @@ def test_timeout
243243
@http = mock("new Net::HTTP")
244244
@conn.expects(:http).returns(@http)
245245
@http.expects(:request).raises(Timeout::Error, "execution expired")
246-
assert_raise(ActiveResource::TimeoutError) { @conn.get("/people_timeout.json") }
246+
error = assert_raise(ActiveResource::TimeoutError) { @conn.get("/people_timeout.json") }
247+
assert_kind_of Net::HTTPRequest, error.request
247248
end
248249

249250
def test_setting_timeout
@@ -285,21 +286,24 @@ def test_ssl_error
285286
http = Net::HTTP.new("")
286287
@conn.expects(:http).returns(http)
287288
http.expects(:request).raises(OpenSSL::SSL::SSLError, "Expired certificate")
288-
assert_raise(ActiveResource::SSLError) { @conn.get("/people/1.json") }
289+
error = assert_raise(ActiveResource::SSLError) { @conn.get("/people/1.json") }
290+
assert_kind_of Net::HTTPRequest, error.request
289291
end
290292

291293
def test_handle_econnrefused
292294
http = Net::HTTP.new("")
293295
@conn.expects(:http).returns(http)
294296
http.expects(:request).raises(Errno::ECONNREFUSED, "Failed to open TCP connection")
295-
assert_raise(ActiveResource::ConnectionRefusedError) { @conn.get("/people/1.json") }
297+
error = assert_raise(ActiveResource::ConnectionRefusedError) { @conn.get("/people/1.json") }
298+
assert_kind_of Net::HTTPRequest, error.request
296299
end
297300

298301
def test_handle_econnrefused_with_backwards_compatible_error
299302
http = Net::HTTP.new("")
300303
@conn.expects(:http).returns(http)
301304
http.expects(:request).raises(Errno::ECONNREFUSED, "Failed to open TCP connection")
302-
assert_raise(Errno::ECONNREFUSED) { @conn.get("/people/1.json") }
305+
error = assert_raise(Errno::ECONNREFUSED) { @conn.get("/people/1.json") }
306+
assert_kind_of Net::HTTPRequest, error.request
303307
end
304308

305309
def test_auth_type_can_be_string
@@ -378,7 +382,7 @@ def assert_redirect_raises(code)
378382
end
379383

380384
def handle_response(response)
381-
@conn.__send__(:handle_response, response)
385+
@conn.__send__(:handle_response, nil, response)
382386
end
383387

384388
def decode(response)

0 commit comments

Comments
 (0)