Recommended Free Tools
A reverse proxy can fail in ways that are easy to misdiagnose because it sits between two parties and two protocols. In a 2025 account of the Rust project ferryman-edge, developer Bipin C describes five bugs involving HTTP version translation, route selection, circuit-breaker probes, streamed request bodies and trusted headers. They are not bugs unique to Rust or necessarily present in every proxy; they show how mistakes at the proxy’s boundaries can turn a client-side problem into an upstream failure—or route a request somewhere it should never go.
What ferryman-edge does
Bipin C describes ferryman-edge as a small layer-7 reverse proxy written in Rust. Its request path includes mutual TLS authentication, RS256 bearer-token verification, per-tenant GCRA rate limiting, and an upstream circuit breaker with active health checks. Certificates and routes can be hot-reloaded with SIGUSR1; connections already established retain their handshake TLS configuration, while new connections use the reloaded configuration.
The author says reusable components were published as ferryman-edge-core and gives cargo install ferryman-edge as the installation command. These are descriptions in the author’s 2025 article; current package availability and versions are not established here. Read Bipin C’s account.
1. HTTP/2 clients received 502s when the upstream used plain HTTP
The listener negotiated HTTP/2 or HTTP/1.1 with clients using ALPN, but the upstream connection was plain http://. The proxy carried the inbound request’s HTTP/2 version through to Hyper’s legacy client, which rejected an HTTP/2-versioned request on an HTTP/1 connection with UserUnsupportedVersion. The client-facing and upstream-facing protocols are separate; supporting h2 at the listener does not make a plain HTTP upstream speak h2.
The Tool Desk
Outbyte PC Repair FREEClear out junk files and repair common Windows errorsFree Scan →Outbyte Driver Updater FREEScan for outdated or missing drivers - takes under a minuteDriver Scan →#1 Best Overall
The fix described by C resets the request version to HTTP/1.1 before forwarding. The response version also needs normalization: a Python http.server upstream could return HTTP/1.0, which otherwise led to an HTTP/1.0 status line being sent to an HTTP/1.1 keep-alive client. C says an end-to-end test exercises a real h2 request.
2. An open breaker sent a specific route to a broader backend
Routes use longest-prefix matching on path-segment boundaries. The original lookup combined matching with a check that the selected upstream was routable. If the most-specific route matched but its breaker was open, iteration could continue to a broader catch-all route. In C’s example, a request for /svc-a/x fell through to / after the /svc-a breaker opened—a change in destination, not merely a temporary failure.
Rank #2
The fix separates identity from availability: first choose the most-specific matching route, then check whether that route is routable. If it is not, return 503 rather than silently sending the request to a different backend. A breaker should affect whether the winning route can serve the request, not which route wins.
3. Multiple requests became half-open recovery probes
After a circuit breaker’s cooldown, one request should be allowed to test whether the upstream has recovered. C says the implementation used a compare-and-swap on a state byte, but an ABA window could allow more than one request through as a half-open probe. The shipped approach uses the last-transition timestamp as the compare-and-swap token instead.
Rank #3
The article reports a concurrency test that released eight threads behind a barrier, repeated the test 200 times, and checked that exactly one request was admitted each time. It also describes a zero-second cooldown edge case: requests in the same second could all appear eligible. The configuration now rejects a zero cooldown, according to C.
4. An abandoned client upload could trip a shared upstream breaker
With streaming request bodies, reading the body is part of the upstream call. C reports that a client disconnect or a body-length-limit error could therefore be counted as an upstream failure. Because the breaker was shared for a route, one authenticated tenant could affect other tenants using it.
Rank #4
The fix walks the error source chain to identify client-body errors—including the configured length-limit error and Hyper user errors—and distinguish them from upstream failures. The author also describes separating deadlines: body reading receives its own deadline and returns 408 on timeout; the upstream timeout starts once the body is available, with a wrapper recording stream completion where needed. Reading the body before route lookup also means a client-side failure does not consume a half-open recovery probe.
5. Hop-by-hop stripping removed the proxy’s tenant header
After verifying a JWT, the proxy adds x-ferryman-tenant using the token subject, first removing any client-supplied value. It then strips hop-by-hop headers and headers named by the Connection header. Because sanitization originally happened after the trusted header was stamped, a client could send Connection: keep-alive, x-ferryman-tenant and cause the proxy’s own tenant header to be stripped.
Best Value
The correction is ordering: strip hop-by-hop and connection-nominated headers first, then add the trusted tenant identity. C says the regression test covers HTTP/1.1; HTTP/2 forbids the Connection header.
Other project-specific fixes the author mentions
- Tokio timer guard: A
select!guard was checked when selection began rather than when the timer branch fired. C says the relevant flag is now checked inside that branch. - JWT issuer validation: The article says
jsonwebtoken9 checks issuer and audience only when those claims are present. To require an issuer, the implementation also needsrequired_spec_claimsto includeiss. - Process and build environment: C reports that Linux process-name truncation affected
pgrep -x, and that a glibc mismatch between a trixie builder and bookworm runtime led the project to pin its builder to bookworm. These are observations about this project’s setup, not universal guarantees about those tools or platforms.
What the author measured—and what remains unmeasured
The figures below are reported by Bipin C in the article, interpreted as published in 2025 from search metadata. They have not been independently reproduced.
| Reported result | Qualification |
|---|---|
| 3,725 of 3,725 requests succeeded | In a 60-second hot-reload run using a release build, eight curl workers and two SIGUSR1 signals. The author says every request used a fresh curl process to exercise a new mTLS handshake. |
| 0.68 µs cache-hit JWT verification; about 150 µs cache-miss verification | Attributed to the author’s Criterion measurements. |
| 16 MB RSS | Reported after the hot-reload run described above. |
| 119 ms TLS-handshake p99 | Not representative according to the author, because client and server shared one machine. |
| 50,000 requests per second | Target, not a measured result. The available wrk/wrk2 setup could not present a client certificate, and the author said an mTLS-capable load generator was still needed. |
The common thread: keep failure attribution intact
Each fix protects a boundary. The proxy must preserve which protocol belongs to each connection, which route actually matched, whether a failure came from the client or upstream, who is allowed to probe a recovering backend, and whether a header is trusted or client-controlled. Blurring any of those distinctions can turn an ordinary failure into a security, availability or routing problem.
Quick Recap
Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.




