v1.0.0 — the modernization + security-hardening release
The 2026 modernization + security-hardening release. From 1.0.0 on this
project follows SemVer strictly: breaking changes only in majors.
Breaking changes
Read this list before upgrading from 0.8.x; everything else below is additive
or a compatible fix. See the README's "Upgrading" section for migration steps.
- Host-derived (dynamic) backends are refused by default. With no
:backendand noenv["rack.backend"], requests now get502unless you
passallow_dynamic_backend: true. A bareRack::Proxy.newis no longer an
open proxy. Combine the opt-in with abackend_allowed?allowlist. net_http_hackedis gone — the file,require "net_http_hacked", and
thebegin_request_hacked/end_request_hackedmethods (see Removed).- The bundled examples left the gem —
require "rack_proxy_examples/..."
raisesLoadError; copy the snippets fromexamples/instead (see Removed). - Backend failures no longer raise.
OpenSSL::SSL::SSLError,EOFError,
timeouts, resets, and malformed backend responses now become a502triplet
instead of an exception —rescue-based error handling aroundproxy.call
must inspect the status instead. - Hop-by-hop request headers are no longer forwarded (
Connection,TE,
Transfer-Encoding,Proxy-Authorization,Upgrade, anything named by
Connection). - gzip backend bodies are forwarded verbatim in
streaming: falsemode
(previously they were transparently inflated);rewrite_responsehooks that
read body text must inflate it themselves. - Supported runtimes: Ruby >= 3.0 and Rack >= 2.0, < 4 (was Ruby >= 2.6,
rack unpinned). - Smaller wire/API changes: body-less POST/PUT sends
Content-Length: 0on
the streaming path; the streaming body is thread-affine (iterate it on the
thread that called the app, as mainstream servers do);HttpStreamingResponse
raisesIOErroron use-after-close; early termination closes the backend
connection instead of draining it.
Security
- Refuse Host-derived backends unless
allow_dynamic_backend: trueis set
(see Breaking changes) — closes the default open-proxy/SSRF pivot. - Strip hop-by-hop headers from the forwarded request (Connection, TE,
Transfer-Encoding, Proxy-Authorization, …, plus any header named by the inbound
Connectionheader), closing a Content-Length/Transfer-Encoding request-smuggling
surface. Hop-by-hop headers were previously stripped only from the response. - Add
backend_allowed?(backend)— an overridable per-request allowlist hook,
consulted for every request (static backends included). A refused backend
responds502. - Add
:max_response_lengthto cap the backend response size (bounds memory
against a hostile/huge backend). Enforced incrementally while streaming. - Add
:ca_file/:cert_storeso private-CA backends can be verified under the
defaultVERIFY_PEERinstead of disabling verification. - Add
:open_timeout/:write_timeoutto bound connect and per-write stalls
(previously only:read_timeoutwas configurable).
Added
:min_version/:max_versionTLS options (mapping toNet::HTTP#min_version=
/#max_version=).:ssl_versionstill works but is deprecated (it pins an
exact protocol and forbids TLS 1.3).HttpStreamingResponse#closeso Rack servers release the backend connection on
early termination (HEAD, 304, client disconnect) instead of leaking it until GC.- Opt-in request hardening:
strip_credentials: truedrops the client's
Cookie/Authorizationheaders from the forwarded request, and
replace_x_forwarded_for: trueforwards only this hop'sREMOTE_ADDR
instead of appending to the client-suppliedX-Forwarded-Forchain. - Project scaffolding:
SECURITY.md,CHANGELOG.md,CONTRIBUTING.md,
CLAUDE.md/AGENTS.md, GitHub Actions CI (Ruby 3.1–3.4 × Rack 2/3),
Dependabot, issue/PR templates, SimpleCov with a ratcheted coverage floor
(COVERAGE=1), and Standard (standardrb) +bundler-auditenforced by a
CI lint job.
Changed
- The streaming path no longer monkey-patches
net/http.
Rack::HttpStreamingResponsenow runs the public block form of
Net::HTTP#requestinside a Fiber (pausing at the response head, resuming
per body chunk), replacing the 2010-era patch of privatenet/httpinternals
innet_http_hacked.rb. Behavioral notes: the streaming session sets
max_retries = 0so a transport error can never silently replay the request
mid-stream; early termination (client abort, HEAD, 204/304) now closes
the backend connection immediately instead of draining the remaining body;
and a body-less POST/PUT/PATCH on the streaming path now carries
Content-Length: 0(matching the non-streaming path and plainNet::HTTP—
the old patched path sent noContent-Lengthat all). - Backend and construction failures now map to status codes instead of raising a
500:400(malformed request URI),501(unknown HTTP method),502
(broadened backend-error set incl.ECONNRESET,EPIPE, read/write timeouts,
EOFError,OpenSSL::SSL::SSLError, protocol errors, and malformed backend
responses —Net::HTTPBadResponse/Net::HTTPHeaderSyntaxError). - gzip-encoded backend responses are forwarded verbatim (Content-Encoding and
Content-Length preserved) instead of being transparently inflated. - Skip all
1xxinterim responses (including103 Early Hints) on the streaming
path, so a backend's103is no longer mistaken for the final response. rackdependency constrained to>= 2.0, < 4; the library nowrequires rack
itself.- Test suite is fully offline (local WEBrick server); the previous live-host
tests are gated behindLIVE=1.
Deprecated
:ssl_version— use:min_version/:max_version.
Removed
-
lib/net_http_hacked.rb— the 2010-era monkey-patch of privateNet::HTTP
internals. The library stopped using it when streaming moved to the public
Net::HTTPAPI (see Changed);require "net_http_hacked"and the
begin_request_hacked/end_request_hackedmethods are gone. If external
code still depends on them, vendor the file from a 0.8.x release — and plan
to migrate; it breaks under modern net/http refactors. -
The bundled examples moved from the gem load path
(lib/rack_proxy_examples/) toexamples/in the repository.
require "rack_proxy_examples/..."no longer works — copy the example class
into your app instead (they were never safe to require blindly: each one
installs itself into the Rails middleware stack when Rails is booted).
Upgrading from 0.8.x: see the README upgrading guide.
Full diff: v0.8.3...v1.0.0