Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 2 additions & 6 deletions examples/client.rb
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,7 @@
code = s.code
puts "Response code: #{code}"
rescue Wreq::BuilderError => e
puts "❌ BuilderError caught!"
puts "Error message: #{e.message}"
puts "Error class: #{e.class}"
puts "Backtrace (first 3 lines):"
puts e.backtrace.first(3).map { |line| " #{line}" }
warn e.full_message(highlight: false)
rescue => e
puts "❌ Unexpected error: #{e.class} - #{e.message}"
warn e.full_message(highlight: false)
end
5 changes: 1 addition & 4 deletions examples/error.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,5 @@
begin
Wreq.get("not-a-valid-url")
rescue Wreq::Error => error
puts "#{error.class}: #{error.message}"
puts "builder: #{error.builder?}"
puts "uri: #{error.uri.inspect}"
puts "status: #{error.status.inspect}"
warn error.full_message(highlight: false)
end
30 changes: 15 additions & 15 deletions examples/proxy.rb
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,10 @@
response = client.get("https://httpbin.io/ip")
puts "Status: #{response.code}"
puts "Response: #{response.text}"
rescue Wreq::RequestError => e
puts "❌ Request failed: #{e.message}"
rescue Wreq::Error => e
warn e.full_message(highlight: false)
rescue => e
puts "❌ Error: #{e.class} - #{e.message}"
warn e.full_message(highlight: false)
end

# ==============================================================================
Expand All @@ -44,10 +44,10 @@
response = client.get("https://httpbin.io/ip")
puts "Status: #{response.code}"
puts "Response: #{response.text}"
rescue Wreq::RequestError => e
puts "❌ Request failed: #{e.message}"
rescue Wreq::Error => e
warn e.full_message(highlight: false)
rescue => e
puts "❌ Error: #{e.class} - #{e.message}"
warn e.full_message(highlight: false)
end

# ==============================================================================
Expand All @@ -64,10 +64,10 @@
response = client.get("https://httpbin.io/ip")
puts "Status: #{response.code}"
puts "Response: #{response.text}"
rescue Wreq::RequestError => e
puts "❌ Request failed: #{e.message}"
rescue Wreq::Error => e
warn e.full_message(highlight: false)
rescue => e
puts "❌ Error: #{e.class} - #{e.message}"
warn e.full_message(highlight: false)
end

# ==============================================================================
Expand All @@ -85,10 +85,10 @@
response = client.get("https://httpbin.io/ip")
puts "Status: #{response.code}"
puts "Response: #{response.text}"
rescue Wreq::RequestError => e
puts "❌ Request failed: #{e.message}"
rescue Wreq::Error => e
warn e.full_message(highlight: false)
rescue => e
puts "❌ Error: #{e.class} - #{e.message}"
warn e.full_message(highlight: false)
end

# ==============================================================================
Expand All @@ -106,8 +106,8 @@
response = client.get("https://httpbin.io/ip")
puts "Status: #{response.code}"
puts "Response: #{response.text}"
rescue Wreq::RequestError => e
puts "❌ Request failed: #{e.message}"
rescue Wreq::Error => e
warn e.full_message(highlight: false)
rescue => e
puts "❌ Error: #{e.class} - #{e.message}"
warn e.full_message(highlight: false)
end
46 changes: 44 additions & 2 deletions lib/wreq_ruby/error.rb
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,14 @@ module Wreq
# Use the predicates when code needs every native fact. Errors created by
# the binding return false for all of them. New facts may be exposed as
# predicates without changing the exception class for existing failures.
# `detailed_message` includes the active facts, and `full_message` adds the
# backtrace and exception causes. Neither method includes `uri`.
#
# @example Rescue any wreq-ruby runtime error
# begin
# Wreq.get("not-a-valid-url")
# rescue Wreq::Error => error
# warn "#{error.class}: #{error.message}"
# warn "invalid request" if error.builder?
# warn error.full_message(highlight: false)
# end
class Error < RuntimeError
# Get the URI recorded by the native error.
Expand Down Expand Up @@ -298,3 +299,44 @@ class DecodingError < Error; end
class BuilderError < Error; end
end
end

# ======================== Ruby API Extensions ========================

module Wreq
class Error
NATIVE_DETAILS = {
builder?: :builder,
body?: :body,
tls?: :tls,
decoding?: :decoding,
redirect?: :redirect,
status?: :status,
upgrade?: :upgrade,
connection_reset?: :connection_reset,
timeout?: :timeout,
proxy_connect?: :proxy_connect,
connect?: :connect,
request?: :request
}.freeze
private_constant :NATIVE_DETAILS

# Return Ruby's detailed exception message with active native error facts.
#
# More specific facts appear before the connection and request stages.
# `full_message` calls this method and adds the backtrace and exception
# causes. The recorded URI is deliberately omitted because it may contain
# credentials or other sensitive values.
#
# @param highlight [Boolean] Whether Ruby should add terminal highlighting
# @param options [Hash] Additional options accepted by Exception
# @return [String] Detailed message suitable for diagnostic output
def detailed_message(highlight: false, **options)
message = super
details = NATIVE_DETAILS.filter_map do |predicate, label|
label if public_send(predicate)
end

details.empty? ? message : "#{message}\n wreq: #{details.inspect}"
end
end
end
25 changes: 23 additions & 2 deletions test/error_hierarchy_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -64,13 +64,17 @@ def test_root_and_specific_errors_can_be_rescued
assert_predicate root_error, :builder?
assert_nil root_error.status
assert_equal [:builder?], active_native_predicates(root_error)
assert_detailed_facts root_error, [:builder]
assert_equal root_error.message, root_error.to_s
assert_includes root_error.full_message(highlight: false), "\n wreq: [:builder]\n"
assert_raises(Wreq::BuilderError) { Wreq.get("not-a-valid-url") }
end

def test_binding_generated_errors_have_no_native_predicates
error = assert_raises(Wreq::BuilderError) { Wreq::Headers.new(Object.new) }

assert_empty active_native_predicates(error)
refute_includes error.detailed_message(highlight: false), "\n wreq:"
end

def test_upstream_request_error_contract
Expand All @@ -82,6 +86,7 @@ def test_upstream_request_error_contract
end

assert_equal %i[request? connect?], active_native_predicates(error)
assert_detailed_facts error, %i[connect request]
end

with_status_server(502) do |proxy|
Expand All @@ -94,12 +99,14 @@ def test_upstream_request_error_contract
end

assert_equal %i[request? proxy_connect?], active_native_predicates(error)
assert_detailed_facts error, %i[proxy_connect request]
end

with_hanging_server do |url, _accepted|
error = assert_raises(Wreq::TimeoutError) { client.get(url, timeout: 1) }

assert_equal %i[timeout? request?], active_native_predicates(error)
assert_detailed_facts error, %i[timeout request]
end
end

Expand Down Expand Up @@ -165,6 +172,7 @@ def test_raise_for_status_exposes_status_without_consuming_body
refute_respond_to error, :retryable?
refute_includes error.message, "response-secret"
refute_includes error.inspect, "response-secret"
assert_detailed_facts error, [:status]
assert_equal body, response.text
end
end
Expand All @@ -191,6 +199,13 @@ def test_native_error_messages_hide_sensitive_request_data
assert_includes [true, false], error.public_send(predicate)
end

diagnostics = [
error.message,
error.inspect,
error.detailed_message(highlight: false, custom: true),
error.full_message(highlight: false)
]

[
"uri-user",
"uri-password",
Expand All @@ -201,8 +216,7 @@ def test_native_error_messages_hide_sensitive_request_data
"authorization-secret",
"cookie-secret"
].each do |secret|
refute_includes error.message, secret
refute_includes error.inspect, secret
diagnostics.each { |output| refute_includes output, secret }
end
end

Expand Down Expand Up @@ -234,6 +248,13 @@ def active_native_predicates(error)
NATIVE_ERROR_PREDICATES.select { |predicate| error.public_send(predicate) }
end

def assert_detailed_facts(error, facts)
assert_equal(
"#{error.message} (#{error.class})\n wreq: #{facts.inspect}",
error.detailed_message(highlight: false)
)
end

def closed_local_port
server = TCPServer.new("127.0.0.1", 0)
server.addr[1]
Expand Down