Repository navigation
feat(http): trust the proxy's rewritten path and let a route take OPTIONS - #275
Closed
ChiragAgg5k wants to merge 2 commits into
Closed
ChiragAgg5k wants to merge 2 commits into
ChiragAgg5k wants to merge 2 commits into
Conversation
A gateway that rewrites every request to one internal route tells the origin which path the client asked for, and Traefik's replacePath does so by appending X-Replaced-Path rather than setting it. A client copy survives to the left of the real value, and reading the header through getHeaderLine hands the application both joined together, with the forged half deciding any prefix check. TrustedHeaders gains a path list beside ip and proto, trusting x-replaced-path by default as proto trusts x-forwarded-proto. getOriginalURI() reads the first trusted header that carries a value and falls back to the request URI. The rewriting hop appends, so the last value is its own and anything before it came from the client.
…hooks execute() stops every OPTIONS at the options hooks, so a catch-all that proxies to another service could never see a preflight. The only way around it was to rewrite the method before the framework saw it and carry the real one in a header, which any client could then forge. Route::options() opts a route in; a matching OPTIONS then runs its action with the method intact. Nothing changes for routes that do not ask.
ChiragAgg5k
requested review from
Meldiron,
eldadfux and
lohanidamodar
as code owners
September 15, 2026 15:27
Benchmark resultshttp — Swoole modes (4 cores, 200 VUs, 20s/run)
a = HYPERLOOP_A (process), b = HYPERLOOP_B (coroutine) Shared CI runners — treat absolute numbers as rough, compare modes within a run. Commit d05f81f. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Two additive changes to
utopia-php/httpso an application behind a rewriting gateway no longer has to reconstruct the client's path and method by hand.Trusted rewrite path.
TrustedHeadersgains apathlist besideipandproto, trustingx-replaced-pathby default the wayprototrustsx-forwarded-proto.Request::getOriginalURI()returns the path the client asked for before the proxy rewrote it, or the request URI when no trusted header carries one. Traefik'sreplacePathappends the header rather than setting it, so a client-supplied copy survives to the left of the real value andgetHeaderLinejoins the two. The resolver takes the last value, which is the one the hop in front of this server wrote.TrustedHeaders::REPLACED_PATHandTrustedHeaders::FORWARDED_PROTOname the headers.Routes may take OPTIONS.
Route::options()opts a route in, andHttp::executethen runs its action for a matching OPTIONS instead of stopping at the options hooks. Only the wildcard can match OPTIONS in practice, so this is how a catch-all forwards a preflight without rewriting the method first and carrying the real one in a forgeable header.Usage
Declare trust once where the server is built. The defaults already trust
x-forwarded-protoandx-replaced-path; a deployment names something else, or withdraws trust, here:Read the pre-rewrite path from the request. Behind Traefik, a request rewritten to
/v1/router/gatewaywith the wire headersX-Replaced-Path: /forgedandX-Replaced-Path: /submitresolves to the proxy's value:Not behind a rewriting proxy? Withdraw trust and the fallback is the request URI:
Let a catch-all receive preflights. Without
options()the wildcard still stops at the options hooks as before:Why
Splits the fix in appwrite-labs/edge#1420 into the library. Edge trusts
X-Replaced-PaththroughgetHeaderLine, so a forged value controls prefix checks, and inventsX-Replaced-Methodbecause the framework would not dispatch OPTIONS to the wildcard. With this change Edge readsgetOriginalURI()andgetMethod(), deletes its method rewrite, and the method header ceases to exist.Verification
bin/monorepo check httppasses Pint, PHPStan and Rector. The unit suite is 145 tests green; the single deprecation is pre-existing inRouteTest. New tests cover default trust, opt-out, a header of another name, a client copy in front of the proxy value for both FPM and Swoole (two raw header lines), and the wildcard running an OPTIONS action while a non-opted wildcard still stops at the hooks. The e2e tier was not run locally because Docker is down.