All notable changes to this project will be documented in this file.
The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.
- Ship RBS signatures for downstream consumers. Previously only
sig/http.rbswas packaged, but it referencedLLHttp::ParserandLLHttp::Delegate(defined only in the unshippedsig/deps.rbs), causingCannot find type LLHttp::Delegateerrors in Steep when loadinglibrary "http". Public LLHttp stubs are now shipped insig/llhttp.rbs, andsig/manifest.yamldeclares stdlib dependencies so consumers don't need to re-list them.
- Fix RBS syntax error.
- Improve gem push workflow security and reliability.
- Exclude test files from gem package, reducing gem size by 50% (from 175 KB to 87 KB).
- Merged
http-form_datagem into the mainhttpgem. TheHTTP::FormDatamodule (includingPart,File,Multipart,Urlencoded, andCompositeIO) is now shipped directly withhttpinstead of being a separate dependency. The public API is unchanged.
Inflaterno longer raisesZlib::BufErrorwhen a response declaresContent-Encoding: gzip(or deflate) but the body is not valid compressed data. This commonly occurred when following redirects withauto_inflateenabled, because the redirect response had aContent-Encodingheader but a non-compressed body. (#621)- Persistent connections now auto-flush unread response bodies before sending
the next request, instead of raising
StateError. Bodies up to 1 MiB are drained transparently; larger bodies cause the connection to close and reopen. This prevents the silent body clobbering described in #371, where an unread response body would return""after a subsequent request. (#371) Response#content_lengthnow handles duplicateContent-Lengthheaders per RFC 7230 Section 3.3.2. When all values are identical, they are collapsed into a single valid value. When values conflict,nilis returned instead of raisingTypeError. (#566)- HTTP 1xx informational responses (e.g.
100 Continue) are now transparently skipped, returning the final response. This was a regression introduced when the parser was migrated from http-parser to llhttp. (#667) - Redirect loop detection now considers cookies, so a redirect back to the same URL with different cookies is no longer falsely detected as an endless loop. Fixes cookie-dependent redirect flows where a server sets a cookie on one hop and expects it on the next. (#544)
- Per-operation timeouts (
HTTP.timeout(read: n, write: n, connect: n)) no longer default unspecified values to 0.25 seconds. Omitted timeouts now mean no timeout for that operation, matching the behavior when no timeout is configured at all. (#579) - Per-operation timeout handler now correctly handles
:wait_writablefromread_nonblockand:wait_readablefromwrite_nonblockon SSL sockets during TLS renegotiation. Previously these symbols were returned as data instead of being waited on. (#358) - Persistent sessions now follow cross-origin redirects instead of raising
StateError.HTTP.persistentreturns anHTTP::Sessionthat pools oneHTTP::Clientper origin, so redirects to a different domain transparently open (and reuse) a separate persistent connection. Cookie management is preserved across all hops. (#557) - Chaining configuration methods (
.headers,.auth,.cookies, etc.) on a persistent session no longer breaks connection reuse. Child sessions created by chaining now share the parent's connection pool, soHTTP.persistent(host).headers(...).get(path)reuses the same underlying TCP connection across calls. (#372)
- BREAKING
HTTP.persistentnow returns anHTTP::Sessioninstead of anHTTP::Client. The session pools persistent clients per origin and exposes the same chainable API (get,post,headers,follow, etc.) plus aclosemethod that shuts down all pooled connections. Code that calledHTTP::Client-only methods on the return value will need updating. (#557) - BREAKING Convert options hash parameters to explicit keyword arguments
across the public API. Methods like
HTTP.get(url, body: "data")continue to work, but passing an explicit hash (e.g.,HTTP.get(url, {body: "data"})) is no longer supported, and unrecognized keyword arguments now raiseArgumentError. Affected methods: all HTTP verb methods (get,post, etc.),request,follow,retriable,URI.new,Request.new,Response.new,Redirector.new,Retriable::Performer.new,Retriable::DelayCalculator.new, andTimeout::Null.new(and subclasses).HTTP::URI.newalso no longer acceptsAddressable::URIobjects. - BREAKING
addressableis no longer a runtime dependency. It is now lazy-loaded only when parsing non-ASCII (IRI) URIs or normalizing internationalized hostnames. Install theaddressablegem if you need non-ASCII URI support. ASCII-only URIs use Ruby's stdlibURIparser exclusively. - BREAKING Extract request building into
HTTP::Request::Builder. Thebuild_requestmethod has been removed fromClient,Session, and the top-levelHTTPmodule. UseHTTP::Request::Builder.new(options).build(verb, uri)to construct requests without executing them.
- Block form for verb methods and
requestthat auto-closes the connection after the block returns.HTTP.get(url) { |response| response.status }yields the response, closes the underlying connection, and returns the block's value. Works with all verb methods and chained options. (#270) - HTTP caching feature (
HTTP.use(:caching)) that stores and reuses responses according to RFC 7234. SupportsCache-Control(max-age,no-cache,no-store),Expires,ETag/If-None-Match, andLast-Modified/If-Modified-Sincefor freshness checks and conditional revalidation. Ships with a default in-memory store; custom stores can be passed viastore:option. Only GET and HEAD responses are cached. (#223) HTTP.digest_auth(user:, pass:)for HTTP Digest Authentication (RFC 2617 / RFC 7616). Automatically handles 401 challenges with digest credentials, supporting MD5, SHA-256, MD5-sess, and SHA-256-sess algorithms with quality-of-protection negotiation. Works as a chainable feature:HTTP.digest_auth(user: "admin", pass: "secret").get(url)(#448)- Happy Eyeballs (RFC 8305) support via Ruby 3.4's native
TCPSocketimplementation. Connection attempts now try multiple addresses (IPv6 and IPv4) concurrently, improving reliability on dual-stack networks. Connect timeouts are passed natively toTCPSocketinstead of usingTimeout.timeout, avoidingThread.raiseinterference with the Happy Eyeballs state machine. (#739) HTTP.base_urifor setting a base URI that resolves relative request paths per RFC 3986. Supports chaining (HTTP.base_uri("https://api.example.com/v1") .get("users")), and integrates withpersistentconnections by deriving the host when omitted (#519, #512, #493)Request::Body#loggable?andResponse::Body#loggable?predicates, and abinary_formatteroption for the logging feature. Binary bodies (IO/Enumerable request sources, binary-encoded request strings, and binary-encoded responses) are now formatted instead of dumped raw, preventing unreadable log output when transferring files like images or audio. Available formatters::stats(default, logs byte count),:base64(logs base64-encoded content), or a customProc. Invalid formatter values raiseArgumentError(#784)Feature#on_requestandFeature#around_requestlifecycle hooks, called before/around each request attempt (including retries), for per-attempt side effects like instrumentation spans and circuit breakers (#826)- Pattern matching support (
deconstruct_keys) for Response, Response::Status, Headers, ContentType, and URI (#642) - Combined global and per-operation timeouts: global and per-operation timeouts
are no longer mutually exclusive. Use
HTTP.timeout(global: 60, read: 30, write: 30, connect: 5)to set both a global request timeout and individual operation limits (#773)
HTTP::URI.form_encodenow encodes newlines as%0Ainstead of%0D%0A(#449)- Thread-safety:
Headers::Normalizercache is now per-thread viaThread.current, eliminating a potential race condition when multiple threads share a normalizer instance - Instrumentation feature now correctly starts a new span for each retry
attempt, fixing
NoMethodErrorwithActiveSupport::Notificationswhen using.retriablewith the instrumentation feature (#826) - Raise
HTTP::URI::InvalidErrorfor malformed or schemeless URIs andArgumentErrorfor nil or empty URIs, instead of confusingUnsupportedSchemeErrororAddressable::URI::InvalidURIError(#565) - Strip
AuthorizationandCookieheaders when following redirects to a different origin (scheme, host, or port) to prevent credential leakage (#516, #770) - AutoInflate now preserves the response charset encoding instead of
defaulting to
Encoding::BINARY(#535) LocalJumpErrorwhen using instrumentation with instrumenters that unconditionally yield in#instrument(e.g.,ActiveSupport::Notifications) (#673)- Logging feature no longer eagerly consumes the response body at debug level;
body chunks are now logged as they are streamed, preserving
response.body.each(#785)
HTTP::URIsetter methods (scheme=,user=,password=,authority=,origin=,port=,request_uri=,fragment=) and normalized accessors (normalized_user,normalized_password,normalized_port,normalized_path,normalized_query) that were delegated toAddressable::URIbut never used internallyHTTP::URI#originis no longer delegated toAddressable::URI. The new implementation follows RFC 6454, normalizing scheme and host to lowercase and excluding user info from the origin stringHTTP::URI#request_uriis no longer delegated toAddressable::URIHTTP::URI#omitis no longer delegated toAddressable::URIand now returnsHTTP::URIinstead ofAddressable::URI(#491)HTTP::URI#query_valuesandHTTP::URI#query_values=delegations toAddressable::URI. Query parameter merging now uses stdlibURI.decode_www_form/URI.encode_www_formHTTP::URIdelegations fornormalized_scheme,normalized_authority,normalized_fragment, andauthoritytoAddressable::URI. The URI normalizer now inlines these operations directlyHTTP::URI#joinis no longer delegated toAddressable::URIand now returnsHTTP::URIinstead ofAddressable::URI. Uses stdlibURI.joinwith automatic percent-encoding of non-ASCII characters (#491)HTTP::URI.form_encodeno longer delegates toAddressable::URI. Uses stdlibURI.encode_www_forminstead
- BREAKING
HTTP::Response::Statusno longer inherits fromDelegator. It now usesComparableandForwardableinstead, providingto_i,to_int, and<=>for numeric comparisons and range matching. Code that called__getobj__/__setobj__or relied on implicit delegation of arbitraryIntegermethods (e.g.,status.even?) will need to be updated to usestatus.codeinstead - BREAKING Chainable option methods (
.headers,.timeout,.cookies,.auth,.follow,.via,.use,.encoding,.nodelay,.basic_auth,.accept) now return a thread-safeHTTP::Sessioninstead ofHTTP::Client.Sessioncreates a newClientfor each request, making it safe to share a configured session across threads.HTTP.persistentstill returnsHTTP::Clientsince persistent connections require mutable state. Code that calls HTTP verb methods (.get,.post, etc.) or accesses.default_optionsis unaffected. Code that checksis_a?(HTTP::Client)on the return value of chainable methods will need to be updated to check forHTTP::Session - BREAKING
.retriablenow returnsHTTP::Sessioninstead ofHTTP::Retriable::Client. Retry is a session-level option: it flows throughHTTP::OptionsintoHTTP::Client#perform, eliminating the need for separateRetriable::ClientandRetriable::Sessionclasses - Improved error message when request body size cannot be determined to suggest
setting
Content-Lengthexplicitly or using chunkedTransfer-Encoding(#560) - BREAKING
Connection#readpartialnow raisesEOFErrorinstead of returningnilat end-of-stream, and supports anoutbufparameter, conforming to theIO#readpartialAPI.Body#readpartialandInflater#readpartialalso raiseEOFError(#618) - BREAKING Stricter timeout options parsing:
.timeout()with a Hash now rejects unknown keys, non-numeric values, string keys, and empty hashes (#754) - Bumped min llhttp dependency version
- BREAKING Handle responses in the reverse order from the requests (#776)
- BREAKING
Response#cookiesnow returnsArray<HTTP::Cookie>instead ofHTTP::CookieJar. Thecookiesoption has been removed fromOptions;Chainable#cookiesnow sets theCookieheader directly with no implicit merging — the last.cookies()call wins (#536) - Cookie jar management during redirects moved from
RedirectortoSession.Redirectoris now a pure redirect-following engine with no cookie awareness;Session#requestmanages cookies across redirect hops - BREAKING
HTTP::Options.new,HTTP::Client.new, andHTTP::Session.newnow accept keyword arguments instead of an options hash. For example,HTTP::Options.new(response: :body)continues to work, butHTTP::Options.new({response: :body})must be updated toHTTP::Options.new(**options). Invalid option names now raiseArgumentErrorautomatically (#447)
- BREAKING Drop Ruby 2.x support
- BREAKING Remove
Headers::Mixinand the[]/[]=delegators onRequestandResponse. Userequest.headers["..."]andresponse.headers["..."]instead (#537)