Skip to content

Reject unknown and invalid client and request options #115

Description

@ZilvinasKucinskas

Priority: P0

Related API contract: Provide a stable error hierarchy and safe, class-specific metadata

Describe the issue

Wreq::Client currently ignores unknown keys in client and request option hashes. A misspelling can therefore disable a timeout, proxy, redirect, TLS, or other transport control without any feedback.

Ruby callers generally expect keyword-like configuration to fail fast. Accepting extension keys is useful for middleware-oriented libraries, but wreq-ruby owns a finite native option surface and can validate it precisely.

Reproduction

Tested with wreq 1.2.4:

require "wreq"

client = Wreq::Client.new(timout: 1) # accepted; `timout` is ignored

client.get(
  "http://127.0.0.1:1",
  history: true # accepted; request proceeds to the transport
)

Known options can also accept values that are never applied. For example, Wreq::Client.new(proxy: Object.new) currently constructs a client instead of rejecting the unsupported value.

Constructor arity and mutually exclusive options are also not validated:

Wreq::Client.new("ignored") # creates a default client
Wreq::Client.new({}, {})    # extra positional argument is ignored

client.post(url, json: {a: 1}, body: "raw")
# sends body `raw` with Content-Type: application/json

client.post(url, form: {a: 1}, json: {b: 2})
# sends JSON bytes with Content-Type: application/x-www-form-urlencoded

client.post(url, bearer_auth: "token", basic_auth: ["user", "password"])
# emits two Authorization header fields

These combinations produce a valid-looking request object but an internally inconsistent wire request.

No-argument constructor safety

The binding's zero-argument branch calls Rust wreq::Client::new(), while the configured branch calls the fallible builder and maps its error to Ruby. The underlying Client::new() documentation states that it can panic when TLS or resolver initialization fails.

A Ruby constructor should use the fallible builder on every path. An environmental initialization failure must raise a normal Wreq::BuilderError/Wreq::TlsError; it must not unwind or abort through the native boundary.

Proposed Ruby behavior

  • Validate keys independently for client options and request options.
  • Raise ArgumentError for unknown keys and include every invalid key in the message.
  • Raise TypeError or ArgumentError for invalid values of known options.
  • Enforce constructor arity and reject non-Hash option arguments.
  • Reject mutually exclusive body options (body, form, and json) and authentication options (auth, bearer_auth, and basic_auth).
  • Reject contradictory protocol controls such as enabling both HTTP/1-only and HTTP/2-only modes.
  • Reject dependent options that cannot take effect, such as max_redirects when redirects are disabled or not enabled.
  • On platforms where an option is compiled out, raise a documented Wreq::BuilderError or a specific Wreq::UnsupportedOptionError < Wreq::BuilderError rather than accepting and ignoring it. Do not use NotImplementedError: it inherits from ScriptError, so normal rescue StandardError and the proposed Wreq::Error transport rescue would not catch it.
  • Preserve normal Ruby keyword behavior whether the caller passes keywords directly or expands a hash with **options.
  • If practical, suggest a close match for a typo:
Wreq::Client.new(timout: 1)
# ArgumentError: unknown option: :timout; did you mean :timeout?

This validation should apply consistently to Wreq::Client.new, Client#request, convenience verbs such as #get, and other native option entry points. APIs that intentionally accept arbitrary keys should document that exception explicitly.

Acceptance criteria

  • Unknown client option keys raise before a native client or connection pool is created.
  • Unknown request option keys raise before DNS or network I/O begins.
  • Invalid values for known options raise and name the option.
  • Conflicting body/authentication/protocol options raise before request construction.
  • Unsupported platform-specific and ineffective dependent options fail explicitly.
  • Compiled-out option failures remain catchable through StandardError and, once available, the shared Wreq::Error hierarchy.
  • Client and value-object constructors reject extra positional arguments instead of ignoring them.
  • Zero-option and configured client construction both use a fallible native build path; initialization failure raises a Ruby exception rather than panicking.
  • Error messages do not include secrets from unrelated option values.
  • Tests cover a typo, multiple unknown keys, a wrong value type, constructor arity, each mutually exclusive group, **hash expansion, and valid options.
  • Public API documentation lists the accepted keys for each entry point.

Ruby ecosystem precedent

  • http.rb raises ArgumentError for unknown timeout options in its per-operation timeout implementation.
  • Faraday stores request configuration in a defined RequestOptions structure, so an unknown writer fails instead of disappearing silently.
  • Ruby methods with explicit keywords raise ArgumentError for unknown keywords by default.

wreq-python's builder extracts recognized values from **kwargs without rejecting leftovers. That is a useful warning about thin-binding extractors, not a reason for Ruby transport controls to ignore misspellings.

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