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

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions