Skip to content

Provide a stable error hierarchy and safe, class-specific metadata #111

Description

@ZilvinasKucinskas

Priority: P1

Describe the issue

wreq-ruby exposes useful specific exception classes, but each normal transport error directly subclasses RuntimeError. There is no shared Wreq::Error, and useful native details such as an HTTP status are reduced to a formatted message.

That makes robust rescue logic difficult: callers must enumerate every current exception class and parse human-readable strings. A stable root exception would make the binding easier to integrate and safer to upgrade.

Reproduction

Tested with wreq 1.2.4:

require "wreq"

Wreq::ConnectionError.superclass # => RuntimeError
Wreq::TimeoutError.superclass    # => RuntimeError
Wreq::BodyError.superclass       # => RuntimeError

begin
  Wreq.get("http://127.0.0.1:1/?token=example-secret")
rescue Wreq::ConnectionError => error
  error.message # includes the complete URI query
end

Proposed Ruby API

module Wreq
  # Keep RuntimeError ancestry for 1.x compatibility. RuntimeError itself
  # inherits from StandardError.
  class Error < RuntimeError; end

  class ConnectionError < Error; end
  class TimeoutError < Error; end
  class BodyError < Error; end
  # Existing regular transport errors also inherit from Error.

  # Keep interruption outside StandardError so a broad transport rescue
  # never swallows a Ruby interrupt.
  class InterruptError < Interrupt; end
end

Making Wreq::Error a direct child of StandardError would be a reasonable clean-slate design, but it would stop existing rescue RuntimeError handlers from catching these exceptions. Inserting the root below RuntimeError preserves that public behavior while still providing the library-specific rescue point. A direct-superclass change can be considered only in a major release.

The exception subclass should identify the error category. A second generic kind symbol would duplicate that hierarchy and is not a common Ruby HTTP-client convention. Likewise, retryable? is usually caller policy: retry safety depends on the request method, whether its body is replayable, and whether the server might already have processed it.

Add metadata only to the exception class where it has clear semantics. For example:

class Wreq::StatusError < Wreq::Error
  attr_reader :status
end

The underlying Rust error exposes an optional URI and status. StatusError#status is useful and unambiguous. A URI reader is less clearly safe because it can contain credentials or query secrets; if exposed, document its type and sensitivity, and do not include sensitive components in default string representations.

Preserve Ruby's standard exception chaining through Exception#cause where an underlying Ruby exception exists. There is no need for a parallel wrapped_exception API unless native exception chaining cannot represent the source.

Opt-in HTTP status errors

The response API should also expose the upstream status check without changing the default scraping-friendly behavior:

response = client.get("https://example.com/missing")
response.raise_for_status!
# raises Wreq::StatusError with status == 404

Response#raise_for_status! should return self for a successful response. It should remain opt-in because a 4xx or 5xx response is still a valid HTTP response, and callers commonly need its headers and body. A client-level raise_for_status: true convenience can be considered separately, but is not required for the root hierarchy.

Sensitive data handling

Exception messages and inspect output should not include URL userinfo, query values, fragments, proxy credentials, authorization headers, or cookies by default. The Rust API explicitly provides without_uri for this concern, but the Ruby binding currently formats the complete Rust error before raising it.

Acceptance criteria

  • Every regular wreq-ruby transport exception inherits from Wreq::Error, which remains both a RuntimeError and a StandardError for 1.x compatibility.
  • Existing specific exception constants remain available for backward compatibility.
  • Wreq::InterruptError remains an Interrupt, not a StandardError.
  • Wreq::StatusError#status returns an integer status without parsing #message.
  • Response#raise_for_status! returns the response for non-error statuses and raises Wreq::StatusError for 4xx/5xx statuses.
  • Any additional metadata is class-specific, documented, and backed by data the native error actually retains.
  • No generic kind or retryable? API is required by this issue.
  • Exception messages redact sensitive URI components by default.
  • Tests cover rescuing the root class, rescuing a specific subclass, status metadata, chaining, redaction, and interrupt behavior.

Ruby ecosystem precedent

  • HTTPX defines HTTPX::Error < StandardError with timeout subclasses.
  • http.rb defines HTTP::Error as the root for client errors.
  • HTTPX and http.rb put response/status metadata on HTTP-status-specific errors rather than defining generic kind, URI, and retry-policy readers on every error.
  • Faraday's Faraday::Error exposes response metadata and preserves a wrapped exception.
  • Excon documents an Excon::Error family for transport failures.
  • HTTPX supports redacting sensitive debug data.
  • The underlying wreq::Error exposes optional URI and status data and warns that URIs may contain secrets.
  • wreq-python exposes the same upstream status check as Response.raise_for_status() while leaving it disabled by default.

Relevant wreq-ruby source

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions