Skip to content

Make Wreq::Headers an idiomatic Ruby collection #121

Description

@ZilvinasKucinskas

Priority: P1

Describe the issue

Wreq::Headers has good native primitives—case-insensitive get, duplicate-preserving get_all, set, append, remove, clear, length, and block iteration—but it does not implement the Ruby collection API documented by the gem.

The v1.2.4 Ruby documentation shows headers[name], headers[name] = value, headers.to_h, and blockless headers.each. At runtime those first three methods are missing and blockless each raises LocalJumpError.

Reproduction

require "wreq"

headers = Wreq::Headers.new
headers.append("Set-Cookie", "a=1")
headers.append("Set-Cookie", "b=2")

headers.get_all("set-cookie") # => ["a=1", "b=2"]
headers.each                  # raises LocalJumpError: no block given

headers.respond_to?(:[])     # => false
headers.respond_to?(:[]=)    # => false
headers.respond_to?(:fetch)  # => false
headers.respond_to?(:delete) # => false
headers.respond_to?(:size)   # => false
headers.respond_to?(:to_a)   # => false
headers.respond_to?(:to_h)   # => false

Wreq::Headers.new("Accept" => "text/html")
# raises ArgumentError: wrong number of arguments

Proposed Ruby API

Preserve the existing methods and add familiar aliases/protocols:

class Wreq::Headers
  include Enumerable

  # []= delegates to set; delete delegates to remove;
  # size delegates to length.
end

headers = Wreq::Headers.new(
  accept: "text/html",
  "Set-Cookie" => ["a=1", "b=2"]
)

Expected behavior:

headers["content-type"]
headers["set-cookie"]        # => ["a=1", "b=2"]
headers["authorization"] = "Bearer token"
headers[:content_type] = "application/json"
headers.fetch("x-request-id")
headers.delete("authorization")
headers.size

headers.each                 # => Enumerator
headers.each.to_a            # preserves every field-value occurrence
headers.select { |name, _| name.start_with?("x-") }

Duplicate-safe conversion

HTTP permits repeated fields, and Set-Cookie must not be comma-folded. Conversion must not silently discard duplicate values.

Use the shape established by HTTP::Headers: [], fetch, and to_h return nil for a missing field, a String for one value, and an Array for repeated values. Keep the existing get(name) behavior (first value or nil) and get_all(name) behavior (always an Array) for callers that need a fixed return type.

For example:

headers.to_h
# => {
#   "content-type" => "text/html",
#   "set-cookie" => ["a=1", "b=2"]
# }

Keep get_all(name) as the unambiguous way to retrieve every value. Document whether each yields every occurrence or one pair per unique name; yielding every occurrence matches current native behavior and avoids data loss.

length/size should count field-value occurrences, matching each and the current native length. keys.length remains the way to count unique field names.

String names should retain their spelling/casing behavior. Symbol names should follow the established Ruby HTTP convention of turning underscores/dashes into a canonical field name (:content_type to Content-Type). Constructor and setter array values should append one field occurrence per value rather than joining Set-Cookie unsafely.

Collection return values should also be deliberate: block iteration and clear should return self, while each without a block returns an Enumerator. []= naturally evaluates to the assigned value in Ruby; document the return values of set, append, and delete rather than inheriting native nil accidentally.

Response-header semantics

Document whether response headers are a mutable snapshot or a read-only view. Object identity is not part of the requested contract: callers should not need response.headers.equal?(response.headers) to be true, and an implementation may return a fresh snapshot when that is the safest native ownership model.

Acceptance criteria

  • Wreq::Headers includes Enumerable.
  • Wreq::Headers.new accepts a Hash, another header collection, or an enumerable of name/value pairs; the zero-argument form remains valid.
  • each returns an Enumerator without a block and returns self with a block.
  • [], []=, fetch, delete, size, to_a, to_h, and to_hash are implemented and documented.
  • [], fetch, and to_h use the documented nil/String/Array shape; get and get_all retain their existing fixed shapes.
  • Lookups remain case-insensitive.
  • String and Symbol header names are accepted with documented wire-name normalization.
  • Array values preserve repeated field occurrences.
  • Repeated fields survive iteration and conversion without silent loss or unsafe comma folding.
  • Existing get, get_all, set, append, remove, clear, and length remain available. Their value behavior remains compatible except for the intentional Ruby-collection normalization that clear returns self instead of its current nil.
  • Response-header mutability and snapshot/view semantics are documented; no wrapper-identity guarantee is required.
  • Tests cover construction, casing, missing keys, fetch defaults/blocks, duplicate Set-Cookie, clear and its return value, enumerator chaining, and documented response snapshot/view semantics.

Ruby ecosystem precedent

  • Net::HTTPHeader provides [], []=, fetch, delete, size, blockless enumeration, get_fields, and duplicate-preserving to_hash values.
  • HTTP::Headers includes Enumerable, returns an enumerator without a block, and uses nil/String/Array lookup and conversion semantics for repeated fields.
  • HTTPX response headers support case-insensitive [], to_h, and to_a.
  • Faraday::Utils::Headers subclasses Hash and preserves case-insensitive lookup.
  • wreq-python's HeaderMap provides duplicate-aware mapping and iteration behavior while allowing response header wrappers to be copied snapshots.

Relevant wreq-ruby source

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    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