diff --git a/examples/client.rb b/examples/client.rb index 5ca7270..12dc58e 100755 --- a/examples/client.rb +++ b/examples/client.rb @@ -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 diff --git a/examples/error.rb b/examples/error.rb index 8d53189..743b3bc 100644 --- a/examples/error.rb +++ b/examples/error.rb @@ -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 diff --git a/examples/proxy.rb b/examples/proxy.rb index ff48471..a412752 100644 --- a/examples/proxy.rb +++ b/examples/proxy.rb @@ -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 # ============================================================================== @@ -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 # ============================================================================== @@ -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 # ============================================================================== @@ -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 # ============================================================================== @@ -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 diff --git a/lib/wreq_ruby/error.rb b/lib/wreq_ruby/error.rb index 973fafd..80f3384 100644 --- a/lib/wreq_ruby/error.rb +++ b/lib/wreq_ruby/error.rb @@ -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. @@ -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 diff --git a/test/error_hierarchy_test.rb b/test/error_hierarchy_test.rb index d90ec02..257845d 100644 --- a/test/error_hierarchy_test.rb +++ b/test/error_hierarchy_test.rb @@ -64,6 +64,9 @@ 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 @@ -71,6 +74,7 @@ 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 @@ -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| @@ -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 @@ -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 @@ -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", @@ -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 @@ -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]