Skip to content

Commit 0d8463f

Browse files
committed
Replace **options with explicit keyword arguments
Convert all methods that accepted arbitrary **options or **opts to restricted keyword argument lists, preventing silent acceptance of misspelled or unknown option keys.
1 parent 7eda14f commit 0d8463f

9 files changed

Lines changed: 96 additions & 74 deletions

File tree

CHANGELOG.md

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -9,15 +9,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
99

1010
### Changed
1111

12-
- **BREAKING** `HTTP::URI.new` no longer accepts `Addressable::URI` objects;
13-
pass a component Hash instead (e.g., `HTTP::URI.new(scheme: "http", host: "example.com")`)
14-
- **BREAKING** Convert options hash parameters to keyword arguments across the
15-
public API. Methods like `HTTP.get(url, body: "data")` continue to work, but
16-
passing an explicit hash (e.g., `HTTP.get(url, {body: "data"})`) is no longer
17-
supported. Affected methods: all HTTP verb methods (`get`, `post`, etc.),
18-
`request`, `follow`, `Request.new`, `Response.new`, `Redirector.new`,
19-
`Retriable::Performer.new`, `Retriable::DelayCalculator.new`, and
20-
`Timeout::Null.new` (and subclasses)
12+
- **BREAKING** Convert options hash parameters to explicit keyword arguments
13+
across the public API. Methods like `HTTP.get(url, body: "data")` continue to
14+
work, but passing an explicit hash (e.g., `HTTP.get(url, {body: "data"})`) is
15+
no longer supported, and unrecognized keyword arguments now raise
16+
`ArgumentError`. Affected methods: all HTTP verb methods (`get`, `post`,
17+
etc.), `request`, `follow`, `retriable`, `URI.new`, `Request.new`,
18+
`Response.new`, `Redirector.new`, `Retriable::Performer.new`,
19+
`Retriable::DelayCalculator.new`, and `Timeout::Null.new` (and subclasses).
20+
`HTTP::URI.new` also no longer accepts `Addressable::URI` objects.
2121
- **BREAKING** Extract request building into `HTTP::Request::Builder`. The
2222
`build_request` method has been removed from `Client`, `Session`, and the
2323
top-level `HTTP` module. Use `HTTP::Request::Builder.new(options).build(verb, uri)`

lib/http/chainable.rb

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -157,12 +157,15 @@ def via(*proxy)
157157
# @example
158158
# HTTP.follow.get("http://example.com")
159159
#
160-
# @param options [Hash] redirect options
160+
# @param [Boolean] strict (true) redirector hops policy
161+
# @param [Integer] max_hops (5) maximum allowed redirect hops
162+
# @param [#call, nil] on_redirect optional redirect callback
161163
# @return [HTTP::Session]
162164
# @see Redirector#initialize
163165
# @api public
164-
def follow(**options)
165-
branch default_options.with_follow(options)
166+
def follow(strict: nil, max_hops: nil, on_redirect: nil)
167+
opts = { strict: strict, max_hops: max_hops, on_redirect: on_redirect }.compact
168+
branch default_options.with_follow(opts)
166169
end
167170

168171
# Make a request with the given headers
@@ -313,8 +316,11 @@ def use(*features)
313316
# @param (see Performer#initialize)
314317
# @return [HTTP::Session]
315318
# @api public
316-
def retriable(**options)
317-
branch default_options.with_retriable(options.empty? || options)
319+
def retriable(tries: nil, delay: nil, exceptions: nil, retry_statuses: nil,
320+
on_retry: nil, max_delay: nil, should_retry: nil)
321+
opts = { tries: tries, delay: delay, exceptions: exceptions, retry_statuses: retry_statuses,
322+
on_retry: on_retry, max_delay: max_delay, should_retry: should_retry }.compact
323+
branch default_options.with_retriable(opts.empty? || opts)
318324
end
319325

320326
private

lib/http/client.rb

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -40,11 +40,21 @@ def initialize(default_options = nil, **)
4040
#
4141
# @param verb [Symbol] the HTTP method
4242
# @param uri [#to_s] the URI to request
43-
# @param opts [Hash] request options
4443
# @return [HTTP::Response] the response
4544
# @api public
46-
def request(verb, uri, **opts)
47-
opts = @default_options.merge(opts)
45+
def request(verb, uri,
46+
headers: nil, params: nil, form: nil, json: nil, body: nil,
47+
response: nil, encoding: nil, follow: nil, ssl: nil, ssl_context: nil,
48+
proxy: nil, nodelay: nil, features: nil, retriable: nil,
49+
socket_class: nil, ssl_socket_class: nil, timeout_class: nil,
50+
timeout_options: nil, keep_alive_timeout: nil, base_uri: nil, persistent: nil)
51+
opts = { headers: headers, params: params, form: form, json: json, body: body,
52+
response: response, encoding: encoding, follow: follow, ssl: ssl,
53+
ssl_context: ssl_context, proxy: proxy, nodelay: nodelay, features: features,
54+
retriable: retriable, socket_class: socket_class, ssl_socket_class: ssl_socket_class,
55+
timeout_class: timeout_class, timeout_options: timeout_options,
56+
keep_alive_timeout: keep_alive_timeout, base_uri: base_uri, persistent: persistent }.compact
57+
opts = @default_options.merge(opts)
4858
builder = Request::Builder.new(opts)
4959
req = builder.build(verb, uri)
5060
res = perform(req, opts)

lib/http/features/logging.rb

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -118,7 +118,7 @@ def log_request_details(request)
118118
headers = stringify_headers(request.headers)
119119
if request.body.loggable?
120120
source = request.body.source
121-
body = source.encoding == Encoding::BINARY ? format_binary(source) : source # steep:ignore
121+
body = source.encoding == Encoding::BINARY ? format_binary(source) : source
122122
logger.debug { "#{headers}\n\n#{body}" }
123123
else
124124
logger.debug { headers }
@@ -131,7 +131,7 @@ def log_request_details(request)
131131
def log_response_body_inline(response)
132132
body = response.body
133133
headers = stringify_headers(response.headers)
134-
if body.respond_to?(:encoding) && body.encoding == Encoding::BINARY # steep:ignore
134+
if body.respond_to?(:encoding) && body.encoding == Encoding::BINARY
135135
logger.debug { "#{headers}\n\n#{format_binary(body)}" } # steep:ignore
136136
else
137137
logger.debug { "#{headers}\n\n#{body}" }

lib/http/session.rb

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -64,12 +64,23 @@ def initialize(default_options = nil, **)
6464
#
6565
# @param verb [Symbol] the HTTP method
6666
# @param uri [#to_s] the URI to request
67-
# @param opts [Hash] request options
6867
# @return [HTTP::Response] the response
6968
# @api public
70-
def request(verb, uri, **opts)
69+
def request(verb, uri,
70+
headers: nil, params: nil, form: nil, json: nil, body: nil,
71+
response: nil, encoding: nil, follow: nil, ssl: nil, ssl_context: nil,
72+
proxy: nil, nodelay: nil, features: nil, retriable: nil,
73+
socket_class: nil, ssl_socket_class: nil, timeout_class: nil,
74+
timeout_options: nil, keep_alive_timeout: nil, base_uri: nil, persistent: nil)
7175
cookie_jar = CookieJar.new
72-
merged = default_options.merge(opts)
76+
merged = default_options.merge(
77+
{ headers: headers, params: params, form: form, json: json, body: body,
78+
response: response, encoding: encoding, follow: follow, ssl: ssl,
79+
ssl_context: ssl_context, proxy: proxy, nodelay: nodelay, features: features,
80+
retriable: retriable, socket_class: socket_class, ssl_socket_class: ssl_socket_class,
81+
timeout_class: timeout_class, timeout_options: timeout_options,
82+
keep_alive_timeout: keep_alive_timeout, base_uri: base_uri, persistent: persistent }.compact
83+
)
7384
builder = Request::Builder.new(merged)
7485
client = make_client(default_options)
7586

lib/http/timeout/null.rb

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -30,11 +30,15 @@ class Null
3030
# @example
3131
# HTTP::Timeout::Null.new(read_timeout: 5)
3232
#
33-
# @param options [Hash] timeout options
33+
# @param [Numeric, nil] read_timeout Read timeout in seconds
34+
# @param [Numeric, nil] write_timeout Write timeout in seconds
35+
# @param [Numeric, nil] connect_timeout Connect timeout in seconds
36+
# @param [Numeric, nil] global_timeout Global timeout in seconds
3437
# @api public
3538
# @return [HTTP::Timeout::Null]
36-
def initialize(**options)
37-
@options = options
39+
def initialize(read_timeout: nil, write_timeout: nil, connect_timeout: nil, global_timeout: nil)
40+
@options = { read_timeout: read_timeout, write_timeout: write_timeout,
41+
connect_timeout: connect_timeout, global_timeout: global_timeout }.compact
3842
end
3943

4044
# Connects to a socket

lib/http/uri.rb

Lines changed: 18 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -94,28 +94,28 @@ def self.percent_encode(string)
9494
end
9595
end
9696

97-
# Creates an HTTP::URI instance from the given options
97+
# Creates an HTTP::URI instance from the given keyword arguments
9898
#
9999
# @example
100100
# HTTP::URI.new(scheme: "http", host: "example.com")
101101
#
102-
# @param [Hash] options component hash
103-
#
104-
# @option options [String, #to_str] :scheme URI scheme
105-
# @option options [String, #to_str] :user for basic authentication
106-
# @option options [String, #to_str] :password for basic authentication
107-
# @option options [String, #to_str] :host name component
108-
# @option options [String, #to_str] :port network port to connect to
109-
# @option options [String, #to_str] :path component to request
110-
# @option options [String, #to_str] :query component distinct from path
111-
# @option options [String, #to_str] :fragment component at the end of the URI
102+
# @param [String, #to_str] scheme URI scheme
103+
# @param [String, #to_str] user for basic authentication
104+
# @param [String, #to_str] password for basic authentication
105+
# @param [String, #to_str] host name component
106+
# @param [Integer, #to_i] port network port to connect to
107+
# @param [String, #to_str] path component to request
108+
# @param [String, #to_str] query component distinct from path
109+
# @param [String, #to_str] fragment component at the end of the URI
112110
#
113111
# @api public
114112
# @return [HTTP::URI] new URI instance
115-
def initialize(options = {})
116-
raise TypeError, "expected Hash for options, got #{options.class}" unless options.is_a?(Hash)
117-
118-
@uri = Addressable::URI.new(options)
113+
def initialize(scheme: nil, user: nil, password: nil, host: nil,
114+
port: nil, path: nil, query: nil, fragment: nil)
115+
@uri = Addressable::URI.new(
116+
scheme: scheme, user: user, password: password, host: host,
117+
port: port, path: path, query: query, fragment: fragment
118+
)
119119
@host = process_ipv6_brackets(@uri.host)
120120
@normalized_host = process_ipv6_brackets(@uri.normalized_host)
121121
end
@@ -218,8 +218,8 @@ def request_uri
218218
# @return [HTTP::URI] new URI without the specified components
219219
def omit(*components)
220220
self.class.new(
221-
{ scheme: scheme, user: user, password: password, host: @uri.host,
222-
port: @uri.port, path: path, query: query, fragment: fragment }.except(*components)
221+
**{ scheme: scheme, user: user, password: password, host: @uri.host,
222+
port: @uri.port, path: path, query: query, fragment: fragment }.except(*components)
223223
)
224224
end
225225

@@ -235,7 +235,7 @@ def omit(*components)
235235
def join(other)
236236
base = self.class.percent_encode(String(self))
237237
ref = self.class.percent_encode(String(other))
238-
self.class.parse(::URI.join(base, ref)) # steep:ignore
238+
self.class.parse(::URI.join(base, ref))
239239
end
240240

241241
# Checks whether the URI scheme is HTTP

sig/http.rbs

Lines changed: 14 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -54,8 +54,8 @@ module HTTP
5454
?form: untyped,
5555
?json: untyped,
5656
?body: untyped,
57-
?follow: bool,
58-
?retriable: bool,
57+
?follow: bool | Hash[Symbol, untyped],
58+
?retriable: bool | Hash[Symbol, untyped],
5959
?base_uri: String | URI | nil,
6060
?persistent: String?,
6161
?ssl_context: OpenSSL::SSL::SSLContext?
@@ -77,7 +77,7 @@ module HTTP
7777
def default_options=: (Hash[Symbol, untyped] | Options opts) -> Options
7878
def nodelay: () -> Session
7979
def use: (*(Symbol | Hash[Symbol, Hash[Symbol, untyped]]) features) -> Session
80-
def retriable: (**untyped options) -> Session
80+
def retriable: (?tries: Integer?, ?delay: (Numeric | ^(Integer) -> Numeric)?, ?exceptions: Array[singleton(Exception)]?, ?retry_statuses: untyped, ?on_retry: (^(Request, Exception?, Response?) -> void)?, ?max_delay: Numeric?, ?should_retry: (^(Request, Exception?, Response?, Integer) -> bool)?) -> Session
8181

8282
private
8383

@@ -116,8 +116,8 @@ module HTTP
116116
?form: untyped,
117117
?json: untyped,
118118
?body: untyped,
119-
?follow: bool,
120-
?retriable: bool,
119+
?follow: bool | Hash[Symbol, untyped],
120+
?retriable: bool | Hash[Symbol, untyped],
121121
?base_uri: String | URI | nil,
122122
?persistent: String?,
123123
?ssl_context: OpenSSL::SSL::SSLContext?
@@ -165,8 +165,8 @@ module HTTP
165165
?form: untyped,
166166
?json: untyped,
167167
?body: untyped,
168-
?follow: bool,
169-
?retriable: bool,
168+
?follow: bool | Hash[Symbol, untyped],
169+
?retriable: bool | Hash[Symbol, untyped],
170170
?base_uri: String | URI | nil,
171171
?persistent: String?,
172172
?ssl_context: OpenSSL::SSL::SSLContext?
@@ -489,8 +489,8 @@ module HTTP
489489
?form: untyped,
490490
?json: untyped,
491491
?body: untyped,
492-
?follow: bool,
493-
?retriable: bool,
492+
?follow: bool | Hash[Symbol, untyped],
493+
?retriable: bool | Hash[Symbol, untyped],
494494
?base_uri: String | URI | nil,
495495
?persistent: String?,
496496
?ssl_context: OpenSSL::SSL::SSLContext?
@@ -586,9 +586,10 @@ module HTTP
586586

587587
def self.parse: (untyped uri) -> URI
588588
def self.form_encode: (untyped form_values, ?sort: bool) -> String
589-
def self.percent_encode: (String? string) -> String?
589+
def self.percent_encode: (String string) -> String
590+
| (String? string) -> String?
590591

591-
def initialize: (?Hash[Symbol, untyped] options) -> void
592+
def initialize: (?scheme: String?, ?user: String?, ?password: String?, ?host: String?, ?port: Integer?, ?path: String?, ?query: String?, ?fragment: String?) -> void
592593
def ==: (untyped other) -> bool
593594
def eql?: (untyped other) -> bool
594595
def hash: () -> Integer
@@ -620,7 +621,7 @@ module HTTP
620621

621622
private
622623

623-
def process_ipv6_brackets: (untyped raw_host, ?brackets: bool) -> untyped
624+
def process_ipv6_brackets: (untyped raw_host, ?brackets: bool) -> String?
624625
end
625626

626627
# Supported HTTP method verbs
@@ -1119,7 +1120,7 @@ module HTTP
11191120
attr_reader options: Hash[Symbol, Numeric]
11201121
attr_reader socket: untyped
11211122

1122-
def initialize: (**Numeric) -> void
1123+
def initialize: (?read_timeout: Numeric?, ?write_timeout: Numeric?, ?connect_timeout: Numeric?, ?global_timeout: Numeric?) -> void
11231124
def connect: (untyped socket_class, String host, Integer port, ?nodelay: bool) -> void
11241125
def connect_ssl: () -> void
11251126
def close: () -> void

test/http/uri_test.rb

Lines changed: 8 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -75,9 +75,8 @@
7575
end
7676

7777
describe "#initialize" do
78-
it "raises TypeError for invalid argument" do
79-
err = assert_raises(TypeError) { HTTP::URI.new(42) }
80-
assert_match(/expected Hash/, err.message)
78+
it "raises ArgumentError for positional argument" do
79+
assert_raises(ArgumentError) { HTTP::URI.new(42) }
8180
end
8281
end
8382

@@ -676,37 +675,28 @@ def remove_dot_segments(path)
676675
end
677676

678677
describe "#initialize" do
679-
it "accepts a Hash of options" do
678+
it "accepts keyword arguments" do
680679
uri = HTTP::URI.new(scheme: "http", host: "example.com")
681680

682681
assert_equal "http", uri.scheme
683682
assert_equal "example.com", uri.host
684683
end
685684

686-
it "raises TypeError for an Addressable::URI" do
685+
it "raises ArgumentError for an Addressable::URI" do
687686
addr_uri = Addressable::URI.parse("http://example.com")
688687

689-
assert_raises(TypeError) { HTTP::URI.new(addr_uri) }
688+
assert_raises(ArgumentError) { HTTP::URI.new(addr_uri) }
690689
end
691690

692-
it "includes the class name in TypeError message" do
693-
err = assert_raises(TypeError) { HTTP::URI.new(42) }
694-
assert_includes err.message, "Integer"
691+
it "raises ArgumentError for a positional argument" do
692+
assert_raises(ArgumentError) { HTTP::URI.new(42) }
695693
end
696694

697-
it "works with no arguments (default empty Hash)" do
695+
it "works with no arguments" do
698696
uri = HTTP::URI.new
699697

700698
assert_instance_of HTTP::URI, uri
701699
end
702-
703-
it "accepts a Hash subclass" do
704-
subclass = Class.new(Hash)
705-
opts = subclass[scheme: "http", host: "example.com"]
706-
uri = HTTP::URI.new(opts)
707-
708-
assert_equal "http://example.com", uri.to_s
709-
end
710700
end
711701

712702
describe "#deconstruct_keys" do

0 commit comments

Comments
 (0)