Skip to content

v1.0.0 — the modernization + security-hardening release

Choose a tag to compare

@ncr ncr released this 23 Jul 09:58
d9a9916

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
    :backend and no env["rack.backend"], requests now get 502 unless you
    pass allow_dynamic_backend: true. A bare Rack::Proxy.new is no longer an
    open proxy. Combine the opt-in with a backend_allowed? allowlist.
  • net_http_hacked is gone — the file, require "net_http_hacked", and
    the begin_request_hacked/end_request_hacked methods (see Removed).
  • The bundled examples left the gemrequire "rack_proxy_examples/..."
    raises LoadError; copy the snippets from examples/ instead (see Removed).
  • Backend failures no longer raise. OpenSSL::SSL::SSLError, EOFError,
    timeouts, resets, and malformed backend responses now become a 502 triplet
    instead of an exception — rescue-based error handling around proxy.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: false mode
    (previously they were transparently inflated); rewrite_response hooks 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: 0 on
    the streaming path; the streaming body is thread-affine (iterate it on the
    thread that called the app, as mainstream servers do); HttpStreamingResponse
    raises IOError on use-after-close; early termination closes the backend
    connection instead of draining it.

Security

  • Refuse Host-derived backends unless allow_dynamic_backend: true is 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
    Connection header), 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
    responds 502.
  • Add :max_response_length to cap the backend response size (bounds memory
    against a hostile/huge backend). Enforced incrementally while streaming.
  • Add :ca_file / :cert_store so private-CA backends can be verified under the
    default VERIFY_PEER instead of disabling verification.
  • Add :open_timeout / :write_timeout to bound connect and per-write stalls
    (previously only :read_timeout was configurable).

Added

  • :min_version / :max_version TLS options (mapping to Net::HTTP#min_version=
    / #max_version=). :ssl_version still works but is deprecated (it pins an
    exact protocol and forbids TLS 1.3).
  • HttpStreamingResponse#close so 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: true drops the client's
    Cookie/Authorization headers from the forwarded request, and
    replace_x_forwarded_for: true forwards only this hop's REMOTE_ADDR
    instead of appending to the client-supplied X-Forwarded-For chain.
  • 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-audit enforced by a
    CI lint job.

Changed

  • The streaming path no longer monkey-patches net/http.
    Rack::HttpStreamingResponse now runs the public block form of
    Net::HTTP#request inside a Fiber (pausing at the response head, resuming
    per body chunk), replacing the 2010-era patch of private net/http internals
    in net_http_hacked.rb. Behavioral notes: the streaming session sets
    max_retries = 0 so 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 plain Net::HTTP
    the old patched path sent no Content-Length at 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 1xx interim responses (including 103 Early Hints) on the streaming
    path, so a backend's 103 is no longer mistaken for the final response.
  • rack dependency constrained to >= 2.0, < 4; the library now requires rack
    itself.
  • Test suite is fully offline (local WEBrick server); the previous live-host
    tests are gated behind LIVE=1.

Deprecated

  • :ssl_version — use :min_version / :max_version.

Removed

  • lib/net_http_hacked.rb — the 2010-era monkey-patch of private Net::HTTP
    internals. The library stopped using it when streaming moved to the public
    Net::HTTP API (see Changed); require "net_http_hacked" and the
    begin_request_hacked / end_request_hacked methods 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/) to examples/ 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