Repository navigation
fix(net/ghttp): normalize HTTP method to uppercase in getHandlersWithCache to prevent route cache pollution (#4765) - #4812
Merged
Conversation
hailaz
approved these changes
Jul 30, 2026
This was referenced Aug 4, 2026
lingcoder
added a commit
to lingcoder/gf
that referenced
this pull request
Aug 5, 2026
…che (gogf#4830) The root cause of gogf#4765 was that the router searching cache built its key with serveHandlerKey, which uppercases `method`, while searchHandlers was called with the method as received. A request carrying a non-canonical method casing therefore looked its route up as "get" but stored the serveItem-less result under the canonical key "GET", making every subsequent well-formed request 404. gogf#4812 removed one instance of this by uppercasing the searching method as well, but only in the local variable holding the method of the request line. It did not reach the other source of the searching method, the Access-Control-Request-Method header of a preflight request, which is assigned to that same variable afterwards. So a preflight carrying `Access-Control-Request-Method: get` still poisons the cache item of the canonical GET route on master today. It did not reach `r.Method` either, so the router and the parameter layer stopped agreeing on the method of the very same request: GetQuery falls back to the request body only when `r.Method` equals GET, which no longer holds for a request the router did treat as GET. See gogf#4830. Give the searching cache its own key instead, one that preserves the case of `method`, and search with the method as received. Key and lookup are then consistent for both sources of the searching method, without changing what the router matches: - gogf#4765 is fixed at its root: a non-canonical request never shares a cache item with the canonical one, whichever source the method came from; - the method token stays case-sensitive, as RFC 9110 section 9.1 requires and as net/http.ServeMux behaves; - `r.Method` keeps the value received on the wire, so every internal check on it and every method based middleware agrees with the routing result. serveHandlerKey now delegates to serveCacheKey with the method uppercased, so both keys are built by one piece of code and differ only in that normalization. Route registration keeps normalizing, so BindHandler("get:/api/user", ...) still registers a GET route. Note that gogf#4812 has not been published in any release, so this does not change the behavior of any released version.
lingcoder
added a commit
to lingcoder/gf
that referenced
this pull request
Aug 5, 2026
…che (gogf#4830) The root cause of gogf#4765 was that the router searching cache built its key with serveHandlerKey, which uppercases `method`, while searchHandlers was called with the method as received. A request carrying a non-canonical method casing therefore looked its route up as "get" but stored the serveItem-less result under the canonical key "GET", making every subsequent well-formed request 404. gogf#4812 removed one instance of this by uppercasing the searching method as well, but only in the local variable holding the method of the request line. It did not reach the other source of the searching method, the Access-Control-Request-Method header of a preflight request, which is assigned to that same variable afterwards. So a preflight carrying `Access-Control-Request-Method: get` still poisons the cache item of the canonical GET route on master today. It did not reach `r.Method` either, so the router and the parameter layer stopped agreeing on the method of the very same request: GetQuery falls back to the request body only when `r.Method` equals GET, which no longer holds for a request the router did treat as GET. See gogf#4830. Give the searching cache its own key instead, one that preserves the case of `method`, and search with the method as received. Key and lookup are then consistent for both sources of the searching method, without changing what the router matches: - gogf#4765 is fixed at its root: a non-canonical request never shares a cache item with the canonical one, whichever source the method came from; - the method token stays case-sensitive, as RFC 9110 section 9.1 requires and as net/http.ServeMux behaves; - `r.Method` keeps the value received on the wire, so every internal check on it and every method based middleware agrees with the routing result. serveHandlerKey now delegates to serveCacheKey with the method uppercased, so both keys are built by one piece of code and differ only in that normalization. Route registration keeps normalizing, so BindHandler("get:/api/user", ...) still registers a GET route. Note that gogf#4812 has not been published in any release, so this does not change the behavior of any released version.
hailaz
pushed a commit
that referenced
this pull request
Aug 5, 2026
…che (#4833) ## Root cause #4765 was not really about the case of the HTTP method — it was a mismatch between the cache key and the lookup argument. `getHandlersWithCache` built its key with `serveHandlerKey`, which uppercases `method`: ```go return strings.ToUpper(method) + ":" + path + strings.ToLower(domain) ``` but called `searchHandlers` with the method as received. So a `get /api/user` request computed the key `GET:/api/user`, searched with `get`, matched no serving handler, and cached that `serveItem == nil` result under the **canonical** key. Every subsequent well-formed `GET` read it back and 404'd until restart. The negative result reaches the cache even when nothing matches: every server registers `internalMiddlewareServerTracing` as an `ALL:/*` middleware (`ghttp_server.go:118`), so `parsedItems` is never nil. That is why the issue reproduces with no user middleware at all. ## #4812 fixed one instance of this, and master still has the other #4812 uppercased the searching method too, but only in the local variable holding the method of the **request line**. The searching method has a second source, assigned to that same variable a few lines later: ```go if method == http.MethodOptions { if v := r.Header.Get("Access-Control-Request-Method"); v != "" { method = v // client-supplied, not normalized } } ``` So the original #4765 pollution is still live on master today, reachable cross-origin from a browser with no user code involved: ``` #1 OPTIONS + Access-Control-Request-Method: get -> 404 "Not Found" #2 GET -> 404 "Not Found" <- poisoned #3 GET -> 404 "Not Found" ``` ## And it left the router and the parameter layer disagreeing The normalization did not reach `r.Method` either, which feeds four framework-internal branches (`ghttp_request_param_query.go:38,60`, `ghttp_request_param.go:245,351`): ```go // net/ghttp/ghttp_request_param_query.go:38 if r.Method == http.MethodGet { r.parseBody() } ``` ``` sent "GET" -> 200 r.Method="GET" GetQuery(a)=frombody sent "get" -> 200 r.Method="get" GetQuery(a)= ``` Same route, same handler, different result from `r.GetQuery`. Details in #4830. ## This change Give the searching cache its own key, one that preserves the case of `method`, and search with the method as received. Key and lookup are consistent again for **both** sources of the searching method, without changing what the router matches: - #4765 is fixed at its root — a non-canonical request never shares a cache item with the canonical one, whichever source the method came from; - the method token stays case-sensitive, as [RFC 9110 §9.1](https://www.rfc-editor.org/rfc/rfc9110.html#section-9.1) requires (*"The method token is case-sensitive because it might be used as a gateway to object-based systems with case-sensitive method names."*) and as `net/http.ServeMux` behaves; - `r.Method` keeps the value received on the wire, so every internal check on it — and every method-based middleware — agrees with the routing result. `serveHandlerKey` now delegates to `serveCacheKey` with the method uppercased, so both keys are built by one piece of code and differ only in that normalization. Route **registration** keeps normalizing, so `s.BindHandler("get:/api/user", ...)` still registers a GET route. | request sequence | before #4812 | current master | this PR | |---|---|---|---| | 1st `get /api/user` | 404 | 200 | 404 | | 2nd `GET /api/user` | **404 (polluted)** | 200 | 200 | | 3rd `GET /api/user` | **404 (polluted)** | 200 | 200 | | 1st `OPTIONS` + `ACRM: get` | 404 | 404 | 404 | | 2nd `GET` | **404 (polluted)** | **404 (polluted)** | 200 | #4812 has not been published in any release, so no released version changes behavior here. ## Note on cache cardinality The searching cache is keyed by the method as received, so `get:/x` and `GET:/x` are now separate items. This does not introduce a new way to grow `serveCache`: the key already contains the client-controlled path, and distinct method tokens (`FOO:/x`, `BAR:/x`) already produced distinct keys before this change. Uppercasing only ever folded casing variants of the same token, so this changes a constant factor, not the bound. ## Tests `Test_Issue4765` is rewritten. Its assertions were written against the #4812 behavior (`get` → 200), which is precisely what this PR revisits, so they could not be kept. The new ones assert what #4765 actually reported — a non-canonical request 404s, and the canonical request right after it still succeeds, repeatedly — and cover both sources of the searching method, including the preflight case that master still fails. `Test_Issue4830` is added: `r.Method` is verbatim and `GetQuery` takes its GET branch for a routed GET, and a method-based middleware cannot be bypassed by casing. Both tests fail without the `ghttp_server_router_serve.go` change and pass with it. `go test ./net/ghttp/` shows no new failures against the master baseline. closes #4830
social4hyq
pushed a commit
to social4hyq/homebrew-core
that referenced
this pull request
Sep 20, 2026
gf 2.10.3 Created-by: HarmonybrewBot Commit-by: HarmonybrewBot Merged-by: HarmonybrewBot Description: Created by `brew bump` --- Created with `brew bump-formula-pr`.<details> <summary>release notes</summary> <pre>## What's Changed * fix: update gf cli to v2.10.2 by @github-actions[bot] in gogf/gf#4773 * feat(cmd/gf): gen dao suppport for dm database by @hailaz in gogf/gf#4776 * fix(ghttp): resolve the issue where the GetCertificate callback in TLS listener gets overwritten by @cashtang in gogf/gf#4789 * fix(net/ghttp): report invalid json body errors by @puneetdixit200 in gogf/gf#4775 * fix(gclient): add timeout configuration methods and improve client initialization by @hailaz in gogf/gf#4779 * fix(cmd/gf): missing pointer in generated service file (#4741) by @UncleChair in gogf/gf#4742 * fix(util/gvalid): date validation allows invalid formats like Y-MMMM and invalid dates like 2026-13-33 by @cococolanosugar in gogf/gf#4740 * fix(container/gmap): added WithError methods for KVMap and ListKVMap by @LanceAdd in gogf/gf#4714 * feat(contrib/drivers/mssql): convert uniqueidentifier to uuid.UUID by @NickWilde18 in gogf/gf#4756 * fix(os/glog): JsonHandler outputs flat JSON suitable for ES ingestion (#4790) by @yangyanqing in gogf/gf#4791 * feat(database/gdb): Added OmitZero function to filter zero value parameters by @LanceAdd in gogf/gf#4713 * Revert "feat(database/gdb): Added OmitZero function to filter zero value parameters" by @hailaz in gogf/gf#4818 * fix(net/ghttp): normalize HTTP method to uppercase in getHandlersWithCache to prevent route cache pollution (#4765) by @waterWang in gogf/gf#4812 * fix(os/gcfg): normalize config pattern in GetWithEnv by @webguosai in gogf/gf#4748 * fix(encoding/ghash): PJW64 should use 64-bit width by @guoxing-li in gogf/gf#4728 * fix(net/goai): add style=deepObject and explode=true for query params with object schema (#4784) by @waterWang in gogf/gf#4813 * feat(os/grpool): let can set limit/cap dynamically by @joy999 in gogf/gf#4794 * test(contrib/drivers/pgsql): expand test coverage to MySQL/MariaDB baseline + 3 framework fixes by @lingcoder in gogf/gf#4763 * fix(net/ghttp): use a case-preserving key for the router searching cache by @lingcoder in gogf/gf#4833 * chore(openspec): remove openspec docs from repo by @gqcn in gogf/gf#4838 * fix(util/gconv): support array-backed unmarshalling values by @ErzerLP in gogf/gf#4810 * fix(net/ghttp): windows web admin restart only shutdowns but doesn't restart by @lonevle in gogf/gf#4809 * feat(container/gtree): Add the function of popping left and right nodes of red-black tree by @LanceAdd in gogf/gf#4793 * feat:To support the loong64 architecture upgrade, we have upgraded go-sqlite and modernc.org/libc. by @lijunting2020 in gogf/gf#4847 * fix(util/gconv): unwrap reflect.Value and support pointer element slices by @hailaz in gogf/gf#4848 * fix(net/goai): nil pointer dereference when an array with enum has an interface element (#4746) by @lingcoder in gogf/gf#4844 * ci: update the CI/CD dependency versions and scripts by @lingcoder in gogf/gf#4852 * fix(os/grpool): stop Test_Limit4 asserting a sum that has a race window by @lingcoder in gogf/gf#4854 * fix(contrib/nacos): move to the upstream commit that fixes the RpcClient data race by @lingcoder in gogf/gf#4856 * ci: make the Apollo and nacos containers actually ready before tests run by @lingcoder in gogf/gf#4857 * fix(database/gdb): stop reporting an aborted row iteration as success by @jiang-bx in gogf/gf#4858 * ci: wait for the Apollo database before starting Apollo by @lingcoder in gogf/gf#4860 * test(contrib/drivers): expand gaussdb coverage to the MySQL/PgSQL baseline + 8 related bug fixes by @lingcoder in gogf/gf#4840 * fix(os/grpool): read the array before the queue in Test_Limit4 assertions by @lingcoder in gogf/gf#4862 * fix: v2.10.3 by @hailaz in gogf/gf#4866 ## New Contributors * @cashtang made their first contribution in gogf/gf#4789 * @puneetdixit200 made their first contribution in gogf/gf#4775 * @NickWilde18 made their first contribution in gogf/gf#4756 * @waterWang made their first contribution in gogf/gf#4812 * @webguosai made their first contribution in gogf/gf#4748 * @guoxing-li made their first contribution in gogf/gf#4728 * @ErzerLP made their first contribution in gogf/gf#4810 * @lonevle made their first contribution in gogf/gf#4809 * @lijunting2020 made their first contribution in gogf/gf#4847 * @jiang-bx made their first contribution in gogf/gf#4858 **Full Changelog**: https://github.com/gogf/gf/compare/v2.10.2...v2.10.3</pre> <p>View the full release notes at <a href="https://github.com/gogf/gf/releases/tag/v2.10.3">https://github.com/gogf/gf/releases/tag/v2.10.3</a>.</p> </details> <hr> See merge request: Harmonybrew/homebrew-core!17794
github-actions Bot
added a commit
to yuan71058/gf
that referenced
this pull request
Sep 20, 2026
…che (gogf#4833) ## Root cause gogf#4765 was not really about the case of the HTTP method — it was a mismatch between the cache key and the lookup argument. `getHandlersWithCache` built its key with `serveHandlerKey`, which uppercases `method`: ```go return strings.ToUpper(method) + ":" + path + strings.ToLower(domain) ``` but called `searchHandlers` with the method as received. So a `get /api/user` request computed the key `GET:/api/user`, searched with `get`, matched no serving handler, and cached that `serveItem == nil` result under the **canonical** key. Every subsequent well-formed `GET` read it back and 404'd until restart. The negative result reaches the cache even when nothing matches: every server registers `internalMiddlewareServerTracing` as an `ALL:/*` middleware (`ghttp_server.go:118`), so `parsedItems` is never nil. That is why the issue reproduces with no user middleware at all. ## gogf#4812 fixed one instance of this, and master still has the other gogf#4812 uppercased the searching method too, but only in the local variable holding the method of the **request line**. The searching method has a second source, assigned to that same variable a few lines later: ```go if method == http.MethodOptions { if v := r.Header.Get("Access-Control-Request-Method"); v != "" { method = v // client-supplied, not normalized } } ``` So the original gogf#4765 pollution is still live on master today, reachable cross-origin from a browser with no user code involved: ``` gogf#1 OPTIONS + Access-Control-Request-Method: get -> 404 "Not Found" gogf#2 GET -> 404 "Not Found" <- poisoned gogf#3 GET -> 404 "Not Found" ``` ## And it left the router and the parameter layer disagreeing The normalization did not reach `r.Method` either, which feeds four framework-internal branches (`ghttp_request_param_query.go:38,60`, `ghttp_request_param.go:245,351`): ```go // net/ghttp/ghttp_request_param_query.go:38 if r.Method == http.MethodGet { r.parseBody() } ``` ``` sent "GET" -> 200 r.Method="GET" GetQuery(a)=frombody sent "get" -> 200 r.Method="get" GetQuery(a)= ``` Same route, same handler, different result from `r.GetQuery`. Details in gogf#4830. ## This change Give the searching cache its own key, one that preserves the case of `method`, and search with the method as received. Key and lookup are consistent again for **both** sources of the searching method, without changing what the router matches: - gogf#4765 is fixed at its root — a non-canonical request never shares a cache item with the canonical one, whichever source the method came from; - the method token stays case-sensitive, as [RFC 9110 §9.1](https://www.rfc-editor.org/rfc/rfc9110.html#section-9.1) requires (*"The method token is case-sensitive because it might be used as a gateway to object-based systems with case-sensitive method names."*) and as `net/http.ServeMux` behaves; - `r.Method` keeps the value received on the wire, so every internal check on it — and every method-based middleware — agrees with the routing result. `serveHandlerKey` now delegates to `serveCacheKey` with the method uppercased, so both keys are built by one piece of code and differ only in that normalization. Route **registration** keeps normalizing, so `s.BindHandler("get:/api/user", ...)` still registers a GET route. | request sequence | before gogf#4812 | current master | this PR | |---|---|---|---| | 1st `get /api/user` | 404 | 200 | 404 | | 2nd `GET /api/user` | **404 (polluted)** | 200 | 200 | | 3rd `GET /api/user` | **404 (polluted)** | 200 | 200 | | 1st `OPTIONS` + `ACRM: get` | 404 | 404 | 404 | | 2nd `GET` | **404 (polluted)** | **404 (polluted)** | 200 | gogf#4812 has not been published in any release, so no released version changes behavior here. ## Note on cache cardinality The searching cache is keyed by the method as received, so `get:/x` and `GET:/x` are now separate items. This does not introduce a new way to grow `serveCache`: the key already contains the client-controlled path, and distinct method tokens (`FOO:/x`, `BAR:/x`) already produced distinct keys before this change. Uppercasing only ever folded casing variants of the same token, so this changes a constant factor, not the bound. ## Tests `Test_Issue4765` is rewritten. Its assertions were written against the gogf#4812 behavior (`get` → 200), which is precisely what this PR revisits, so they could not be kept. The new ones assert what gogf#4765 actually reported — a non-canonical request 404s, and the canonical request right after it still succeeds, repeatedly — and cover both sources of the searching method, including the preflight case that master still fails. `Test_Issue4830` is added: `r.Method` is verbatim and `GetQuery` takes its GET branch for a routed GET, and a method-based middleware cannot be bypassed by casing. Both tests fail without the `ghttp_server_router_serve.go` change and pass with it. `go test ./net/ghttp/` shows no new failures against the master baseline. closes gogf#4830
github-actions Bot
added a commit
to yuan71058/gf
that referenced
this pull request
Sep 21, 2026
…che (gogf#4833) ## Root cause gogf#4765 was not really about the case of the HTTP method — it was a mismatch between the cache key and the lookup argument. `getHandlersWithCache` built its key with `serveHandlerKey`, which uppercases `method`: ```go return strings.ToUpper(method) + ":" + path + strings.ToLower(domain) ``` but called `searchHandlers` with the method as received. So a `get /api/user` request computed the key `GET:/api/user`, searched with `get`, matched no serving handler, and cached that `serveItem == nil` result under the **canonical** key. Every subsequent well-formed `GET` read it back and 404'd until restart. The negative result reaches the cache even when nothing matches: every server registers `internalMiddlewareServerTracing` as an `ALL:/*` middleware (`ghttp_server.go:118`), so `parsedItems` is never nil. That is why the issue reproduces with no user middleware at all. ## gogf#4812 fixed one instance of this, and master still has the other gogf#4812 uppercased the searching method too, but only in the local variable holding the method of the **request line**. The searching method has a second source, assigned to that same variable a few lines later: ```go if method == http.MethodOptions { if v := r.Header.Get("Access-Control-Request-Method"); v != "" { method = v // client-supplied, not normalized } } ``` So the original gogf#4765 pollution is still live on master today, reachable cross-origin from a browser with no user code involved: ``` gogf#1 OPTIONS + Access-Control-Request-Method: get -> 404 "Not Found" gogf#2 GET -> 404 "Not Found" <- poisoned gogf#3 GET -> 404 "Not Found" ``` ## And it left the router and the parameter layer disagreeing The normalization did not reach `r.Method` either, which feeds four framework-internal branches (`ghttp_request_param_query.go:38,60`, `ghttp_request_param.go:245,351`): ```go // net/ghttp/ghttp_request_param_query.go:38 if r.Method == http.MethodGet { r.parseBody() } ``` ``` sent "GET" -> 200 r.Method="GET" GetQuery(a)=frombody sent "get" -> 200 r.Method="get" GetQuery(a)= ``` Same route, same handler, different result from `r.GetQuery`. Details in gogf#4830. ## This change Give the searching cache its own key, one that preserves the case of `method`, and search with the method as received. Key and lookup are consistent again for **both** sources of the searching method, without changing what the router matches: - gogf#4765 is fixed at its root — a non-canonical request never shares a cache item with the canonical one, whichever source the method came from; - the method token stays case-sensitive, as [RFC 9110 §9.1](https://www.rfc-editor.org/rfc/rfc9110.html#section-9.1) requires (*"The method token is case-sensitive because it might be used as a gateway to object-based systems with case-sensitive method names."*) and as `net/http.ServeMux` behaves; - `r.Method` keeps the value received on the wire, so every internal check on it — and every method-based middleware — agrees with the routing result. `serveHandlerKey` now delegates to `serveCacheKey` with the method uppercased, so both keys are built by one piece of code and differ only in that normalization. Route **registration** keeps normalizing, so `s.BindHandler("get:/api/user", ...)` still registers a GET route. | request sequence | before gogf#4812 | current master | this PR | |---|---|---|---| | 1st `get /api/user` | 404 | 200 | 404 | | 2nd `GET /api/user` | **404 (polluted)** | 200 | 200 | | 3rd `GET /api/user` | **404 (polluted)** | 200 | 200 | | 1st `OPTIONS` + `ACRM: get` | 404 | 404 | 404 | | 2nd `GET` | **404 (polluted)** | **404 (polluted)** | 200 | gogf#4812 has not been published in any release, so no released version changes behavior here. ## Note on cache cardinality The searching cache is keyed by the method as received, so `get:/x` and `GET:/x` are now separate items. This does not introduce a new way to grow `serveCache`: the key already contains the client-controlled path, and distinct method tokens (`FOO:/x`, `BAR:/x`) already produced distinct keys before this change. Uppercasing only ever folded casing variants of the same token, so this changes a constant factor, not the bound. ## Tests `Test_Issue4765` is rewritten. Its assertions were written against the gogf#4812 behavior (`get` → 200), which is precisely what this PR revisits, so they could not be kept. The new ones assert what gogf#4765 actually reported — a non-canonical request 404s, and the canonical request right after it still succeeds, repeatedly — and cover both sources of the searching method, including the preflight case that master still fails. `Test_Issue4830` is added: `r.Method` is verbatim and `GetQuery` takes its GET branch for a routed GET, and a method-based middleware cannot be bypassed by casing. Both tests fail without the `ghttp_server_router_serve.go` change and pass with it. `go test ./net/ghttp/` shows no new failures against the master baseline. closes gogf#4830
github-actions Bot
added a commit
to yuan71058/gf
that referenced
this pull request
Sep 22, 2026
…che (gogf#4833) ## Root cause gogf#4765 was not really about the case of the HTTP method — it was a mismatch between the cache key and the lookup argument. `getHandlersWithCache` built its key with `serveHandlerKey`, which uppercases `method`: ```go return strings.ToUpper(method) + ":" + path + strings.ToLower(domain) ``` but called `searchHandlers` with the method as received. So a `get /api/user` request computed the key `GET:/api/user`, searched with `get`, matched no serving handler, and cached that `serveItem == nil` result under the **canonical** key. Every subsequent well-formed `GET` read it back and 404'd until restart. The negative result reaches the cache even when nothing matches: every server registers `internalMiddlewareServerTracing` as an `ALL:/*` middleware (`ghttp_server.go:118`), so `parsedItems` is never nil. That is why the issue reproduces with no user middleware at all. ## gogf#4812 fixed one instance of this, and master still has the other gogf#4812 uppercased the searching method too, but only in the local variable holding the method of the **request line**. The searching method has a second source, assigned to that same variable a few lines later: ```go if method == http.MethodOptions { if v := r.Header.Get("Access-Control-Request-Method"); v != "" { method = v // client-supplied, not normalized } } ``` So the original gogf#4765 pollution is still live on master today, reachable cross-origin from a browser with no user code involved: ``` gogf#1 OPTIONS + Access-Control-Request-Method: get -> 404 "Not Found" gogf#2 GET -> 404 "Not Found" <- poisoned gogf#3 GET -> 404 "Not Found" ``` ## And it left the router and the parameter layer disagreeing The normalization did not reach `r.Method` either, which feeds four framework-internal branches (`ghttp_request_param_query.go:38,60`, `ghttp_request_param.go:245,351`): ```go // net/ghttp/ghttp_request_param_query.go:38 if r.Method == http.MethodGet { r.parseBody() } ``` ``` sent "GET" -> 200 r.Method="GET" GetQuery(a)=frombody sent "get" -> 200 r.Method="get" GetQuery(a)= ``` Same route, same handler, different result from `r.GetQuery`. Details in gogf#4830. ## This change Give the searching cache its own key, one that preserves the case of `method`, and search with the method as received. Key and lookup are consistent again for **both** sources of the searching method, without changing what the router matches: - gogf#4765 is fixed at its root — a non-canonical request never shares a cache item with the canonical one, whichever source the method came from; - the method token stays case-sensitive, as [RFC 9110 §9.1](https://www.rfc-editor.org/rfc/rfc9110.html#section-9.1) requires (*"The method token is case-sensitive because it might be used as a gateway to object-based systems with case-sensitive method names."*) and as `net/http.ServeMux` behaves; - `r.Method` keeps the value received on the wire, so every internal check on it — and every method-based middleware — agrees with the routing result. `serveHandlerKey` now delegates to `serveCacheKey` with the method uppercased, so both keys are built by one piece of code and differ only in that normalization. Route **registration** keeps normalizing, so `s.BindHandler("get:/api/user", ...)` still registers a GET route. | request sequence | before gogf#4812 | current master | this PR | |---|---|---|---| | 1st `get /api/user` | 404 | 200 | 404 | | 2nd `GET /api/user` | **404 (polluted)** | 200 | 200 | | 3rd `GET /api/user` | **404 (polluted)** | 200 | 200 | | 1st `OPTIONS` + `ACRM: get` | 404 | 404 | 404 | | 2nd `GET` | **404 (polluted)** | **404 (polluted)** | 200 | gogf#4812 has not been published in any release, so no released version changes behavior here. ## Note on cache cardinality The searching cache is keyed by the method as received, so `get:/x` and `GET:/x` are now separate items. This does not introduce a new way to grow `serveCache`: the key already contains the client-controlled path, and distinct method tokens (`FOO:/x`, `BAR:/x`) already produced distinct keys before this change. Uppercasing only ever folded casing variants of the same token, so this changes a constant factor, not the bound. ## Tests `Test_Issue4765` is rewritten. Its assertions were written against the gogf#4812 behavior (`get` → 200), which is precisely what this PR revisits, so they could not be kept. The new ones assert what gogf#4765 actually reported — a non-canonical request 404s, and the canonical request right after it still succeeds, repeatedly — and cover both sources of the searching method, including the preflight case that master still fails. `Test_Issue4830` is added: `r.Method` is verbatim and `GetQuery` takes its GET branch for a routed GET, and a method-based middleware cannot be bypassed by casing. Both tests fail without the `ghttp_server_router_serve.go` change and pass with it. `go test ./net/ghttp/` shows no new failures against the master baseline. closes gogf#4830
github-actions Bot
added a commit
to yuan71058/gf
that referenced
this pull request
Sep 23, 2026
…che (gogf#4833) ## Root cause gogf#4765 was not really about the case of the HTTP method — it was a mismatch between the cache key and the lookup argument. `getHandlersWithCache` built its key with `serveHandlerKey`, which uppercases `method`: ```go return strings.ToUpper(method) + ":" + path + strings.ToLower(domain) ``` but called `searchHandlers` with the method as received. So a `get /api/user` request computed the key `GET:/api/user`, searched with `get`, matched no serving handler, and cached that `serveItem == nil` result under the **canonical** key. Every subsequent well-formed `GET` read it back and 404'd until restart. The negative result reaches the cache even when nothing matches: every server registers `internalMiddlewareServerTracing` as an `ALL:/*` middleware (`ghttp_server.go:118`), so `parsedItems` is never nil. That is why the issue reproduces with no user middleware at all. ## gogf#4812 fixed one instance of this, and master still has the other gogf#4812 uppercased the searching method too, but only in the local variable holding the method of the **request line**. The searching method has a second source, assigned to that same variable a few lines later: ```go if method == http.MethodOptions { if v := r.Header.Get("Access-Control-Request-Method"); v != "" { method = v // client-supplied, not normalized } } ``` So the original gogf#4765 pollution is still live on master today, reachable cross-origin from a browser with no user code involved: ``` gogf#1 OPTIONS + Access-Control-Request-Method: get -> 404 "Not Found" gogf#2 GET -> 404 "Not Found" <- poisoned gogf#3 GET -> 404 "Not Found" ``` ## And it left the router and the parameter layer disagreeing The normalization did not reach `r.Method` either, which feeds four framework-internal branches (`ghttp_request_param_query.go:38,60`, `ghttp_request_param.go:245,351`): ```go // net/ghttp/ghttp_request_param_query.go:38 if r.Method == http.MethodGet { r.parseBody() } ``` ``` sent "GET" -> 200 r.Method="GET" GetQuery(a)=frombody sent "get" -> 200 r.Method="get" GetQuery(a)= ``` Same route, same handler, different result from `r.GetQuery`. Details in gogf#4830. ## This change Give the searching cache its own key, one that preserves the case of `method`, and search with the method as received. Key and lookup are consistent again for **both** sources of the searching method, without changing what the router matches: - gogf#4765 is fixed at its root — a non-canonical request never shares a cache item with the canonical one, whichever source the method came from; - the method token stays case-sensitive, as [RFC 9110 §9.1](https://www.rfc-editor.org/rfc/rfc9110.html#section-9.1) requires (*"The method token is case-sensitive because it might be used as a gateway to object-based systems with case-sensitive method names."*) and as `net/http.ServeMux` behaves; - `r.Method` keeps the value received on the wire, so every internal check on it — and every method-based middleware — agrees with the routing result. `serveHandlerKey` now delegates to `serveCacheKey` with the method uppercased, so both keys are built by one piece of code and differ only in that normalization. Route **registration** keeps normalizing, so `s.BindHandler("get:/api/user", ...)` still registers a GET route. | request sequence | before gogf#4812 | current master | this PR | |---|---|---|---| | 1st `get /api/user` | 404 | 200 | 404 | | 2nd `GET /api/user` | **404 (polluted)** | 200 | 200 | | 3rd `GET /api/user` | **404 (polluted)** | 200 | 200 | | 1st `OPTIONS` + `ACRM: get` | 404 | 404 | 404 | | 2nd `GET` | **404 (polluted)** | **404 (polluted)** | 200 | gogf#4812 has not been published in any release, so no released version changes behavior here. ## Note on cache cardinality The searching cache is keyed by the method as received, so `get:/x` and `GET:/x` are now separate items. This does not introduce a new way to grow `serveCache`: the key already contains the client-controlled path, and distinct method tokens (`FOO:/x`, `BAR:/x`) already produced distinct keys before this change. Uppercasing only ever folded casing variants of the same token, so this changes a constant factor, not the bound. ## Tests `Test_Issue4765` is rewritten. Its assertions were written against the gogf#4812 behavior (`get` → 200), which is precisely what this PR revisits, so they could not be kept. The new ones assert what gogf#4765 actually reported — a non-canonical request 404s, and the canonical request right after it still succeeds, repeatedly — and cover both sources of the searching method, including the preflight case that master still fails. `Test_Issue4830` is added: `r.Method` is verbatim and `GetQuery` takes its GET branch for a routed GET, and a method-based middleware cannot be bypassed by casing. Both tests fail without the `ghttp_server_router_serve.go` change and pass with it. `go test ./net/ghttp/` shows no new failures against the master baseline. closes gogf#4830
github-actions Bot
added a commit
to yuan71058/gf
that referenced
this pull request
Sep 24, 2026
…che (gogf#4833) ## Root cause gogf#4765 was not really about the case of the HTTP method — it was a mismatch between the cache key and the lookup argument. `getHandlersWithCache` built its key with `serveHandlerKey`, which uppercases `method`: ```go return strings.ToUpper(method) + ":" + path + strings.ToLower(domain) ``` but called `searchHandlers` with the method as received. So a `get /api/user` request computed the key `GET:/api/user`, searched with `get`, matched no serving handler, and cached that `serveItem == nil` result under the **canonical** key. Every subsequent well-formed `GET` read it back and 404'd until restart. The negative result reaches the cache even when nothing matches: every server registers `internalMiddlewareServerTracing` as an `ALL:/*` middleware (`ghttp_server.go:118`), so `parsedItems` is never nil. That is why the issue reproduces with no user middleware at all. ## gogf#4812 fixed one instance of this, and master still has the other gogf#4812 uppercased the searching method too, but only in the local variable holding the method of the **request line**. The searching method has a second source, assigned to that same variable a few lines later: ```go if method == http.MethodOptions { if v := r.Header.Get("Access-Control-Request-Method"); v != "" { method = v // client-supplied, not normalized } } ``` So the original gogf#4765 pollution is still live on master today, reachable cross-origin from a browser with no user code involved: ``` gogf#1 OPTIONS + Access-Control-Request-Method: get -> 404 "Not Found" gogf#2 GET -> 404 "Not Found" <- poisoned gogf#3 GET -> 404 "Not Found" ``` ## And it left the router and the parameter layer disagreeing The normalization did not reach `r.Method` either, which feeds four framework-internal branches (`ghttp_request_param_query.go:38,60`, `ghttp_request_param.go:245,351`): ```go // net/ghttp/ghttp_request_param_query.go:38 if r.Method == http.MethodGet { r.parseBody() } ``` ``` sent "GET" -> 200 r.Method="GET" GetQuery(a)=frombody sent "get" -> 200 r.Method="get" GetQuery(a)= ``` Same route, same handler, different result from `r.GetQuery`. Details in gogf#4830. ## This change Give the searching cache its own key, one that preserves the case of `method`, and search with the method as received. Key and lookup are consistent again for **both** sources of the searching method, without changing what the router matches: - gogf#4765 is fixed at its root — a non-canonical request never shares a cache item with the canonical one, whichever source the method came from; - the method token stays case-sensitive, as [RFC 9110 §9.1](https://www.rfc-editor.org/rfc/rfc9110.html#section-9.1) requires (*"The method token is case-sensitive because it might be used as a gateway to object-based systems with case-sensitive method names."*) and as `net/http.ServeMux` behaves; - `r.Method` keeps the value received on the wire, so every internal check on it — and every method-based middleware — agrees with the routing result. `serveHandlerKey` now delegates to `serveCacheKey` with the method uppercased, so both keys are built by one piece of code and differ only in that normalization. Route **registration** keeps normalizing, so `s.BindHandler("get:/api/user", ...)` still registers a GET route. | request sequence | before gogf#4812 | current master | this PR | |---|---|---|---| | 1st `get /api/user` | 404 | 200 | 404 | | 2nd `GET /api/user` | **404 (polluted)** | 200 | 200 | | 3rd `GET /api/user` | **404 (polluted)** | 200 | 200 | | 1st `OPTIONS` + `ACRM: get` | 404 | 404 | 404 | | 2nd `GET` | **404 (polluted)** | **404 (polluted)** | 200 | gogf#4812 has not been published in any release, so no released version changes behavior here. ## Note on cache cardinality The searching cache is keyed by the method as received, so `get:/x` and `GET:/x` are now separate items. This does not introduce a new way to grow `serveCache`: the key already contains the client-controlled path, and distinct method tokens (`FOO:/x`, `BAR:/x`) already produced distinct keys before this change. Uppercasing only ever folded casing variants of the same token, so this changes a constant factor, not the bound. ## Tests `Test_Issue4765` is rewritten. Its assertions were written against the gogf#4812 behavior (`get` → 200), which is precisely what this PR revisits, so they could not be kept. The new ones assert what gogf#4765 actually reported — a non-canonical request 404s, and the canonical request right after it still succeeds, repeatedly — and cover both sources of the searching method, including the preflight case that master still fails. `Test_Issue4830` is added: `r.Method` is verbatim and `GetQuery` takes its GET branch for a routed GET, and a method-based middleware cannot be bypassed by casing. Both tests fail without the `ghttp_server_router_serve.go` change and pass with it. `go test ./net/ghttp/` shows no new failures against the master baseline. closes gogf#4830
ChamHerry
pushed a commit
to ChamHerry/gf
that referenced
this pull request
Oct 21, 2026
…Cache to prevent route cache pollution (gogf#4765) (gogf#4812) **Fix gogf#4765** — ghttp: Lowercase HTTP request methods causing route cache pollution ## Root Cause getHandlersWithCache() uses r.Method directly as the cache key without normalizing to uppercase. A lowercase get request creates a cache entry with key get:/api/user, which doesn't match the registered route GET:/api/user. Subsequent normal GET requests also hit the cached get:/api/user entry (which is empty), permanently returning 404 until the server is restarted. ## Fix Normalize r.Method to uppercase at the cache entry point: Before: method = r.Method After: method = strings.ToUpper(r.Method) HTTP method names are case-insensitive per RFC 7230 section 3.1.1. The existing serveHandlerKey() already uses strings.ToUpper(method), so this fix makes the cache key consistent with the router key. ## Changed File - net/ghttp/ghttp_server_router_serve.go — 1 line: r.Method to strings.ToUpper(r.Method) ## Verification Reproducer from issue should now show all requests returning 200 without restart. --------- Co-authored-by: hailaz <739476267@qq.com>
ChamHerry
pushed a commit
to ChamHerry/gf
that referenced
this pull request
Oct 21, 2026
…che (gogf#4833) ## Root cause gogf#4765 was not really about the case of the HTTP method — it was a mismatch between the cache key and the lookup argument. `getHandlersWithCache` built its key with `serveHandlerKey`, which uppercases `method`: ```go return strings.ToUpper(method) + ":" + path + strings.ToLower(domain) ``` but called `searchHandlers` with the method as received. So a `get /api/user` request computed the key `GET:/api/user`, searched with `get`, matched no serving handler, and cached that `serveItem == nil` result under the **canonical** key. Every subsequent well-formed `GET` read it back and 404'd until restart. The negative result reaches the cache even when nothing matches: every server registers `internalMiddlewareServerTracing` as an `ALL:/*` middleware (`ghttp_server.go:118`), so `parsedItems` is never nil. That is why the issue reproduces with no user middleware at all. ## gogf#4812 fixed one instance of this, and master still has the other gogf#4812 uppercased the searching method too, but only in the local variable holding the method of the **request line**. The searching method has a second source, assigned to that same variable a few lines later: ```go if method == http.MethodOptions { if v := r.Header.Get("Access-Control-Request-Method"); v != "" { method = v // client-supplied, not normalized } } ``` So the original gogf#4765 pollution is still live on master today, reachable cross-origin from a browser with no user code involved: ``` gogf#1 OPTIONS + Access-Control-Request-Method: get -> 404 "Not Found" gogf#2 GET -> 404 "Not Found" <- poisoned gogf#3 GET -> 404 "Not Found" ``` ## And it left the router and the parameter layer disagreeing The normalization did not reach `r.Method` either, which feeds four framework-internal branches (`ghttp_request_param_query.go:38,60`, `ghttp_request_param.go:245,351`): ```go // net/ghttp/ghttp_request_param_query.go:38 if r.Method == http.MethodGet { r.parseBody() } ``` ``` sent "GET" -> 200 r.Method="GET" GetQuery(a)=frombody sent "get" -> 200 r.Method="get" GetQuery(a)= ``` Same route, same handler, different result from `r.GetQuery`. Details in gogf#4830. ## This change Give the searching cache its own key, one that preserves the case of `method`, and search with the method as received. Key and lookup are consistent again for **both** sources of the searching method, without changing what the router matches: - gogf#4765 is fixed at its root — a non-canonical request never shares a cache item with the canonical one, whichever source the method came from; - the method token stays case-sensitive, as [RFC 9110 §9.1](https://www.rfc-editor.org/rfc/rfc9110.html#section-9.1) requires (*"The method token is case-sensitive because it might be used as a gateway to object-based systems with case-sensitive method names."*) and as `net/http.ServeMux` behaves; - `r.Method` keeps the value received on the wire, so every internal check on it — and every method-based middleware — agrees with the routing result. `serveHandlerKey` now delegates to `serveCacheKey` with the method uppercased, so both keys are built by one piece of code and differ only in that normalization. Route **registration** keeps normalizing, so `s.BindHandler("get:/api/user", ...)` still registers a GET route. | request sequence | before gogf#4812 | current master | this PR | |---|---|---|---| | 1st `get /api/user` | 404 | 200 | 404 | | 2nd `GET /api/user` | **404 (polluted)** | 200 | 200 | | 3rd `GET /api/user` | **404 (polluted)** | 200 | 200 | | 1st `OPTIONS` + `ACRM: get` | 404 | 404 | 404 | | 2nd `GET` | **404 (polluted)** | **404 (polluted)** | 200 | gogf#4812 has not been published in any release, so no released version changes behavior here. ## Note on cache cardinality The searching cache is keyed by the method as received, so `get:/x` and `GET:/x` are now separate items. This does not introduce a new way to grow `serveCache`: the key already contains the client-controlled path, and distinct method tokens (`FOO:/x`, `BAR:/x`) already produced distinct keys before this change. Uppercasing only ever folded casing variants of the same token, so this changes a constant factor, not the bound. ## Tests `Test_Issue4765` is rewritten. Its assertions were written against the gogf#4812 behavior (`get` → 200), which is precisely what this PR revisits, so they could not be kept. The new ones assert what gogf#4765 actually reported — a non-canonical request 404s, and the canonical request right after it still succeeds, repeatedly — and cover both sources of the searching method, including the preflight case that master still fails. `Test_Issue4830` is added: `r.Method` is verbatim and `GetQuery` takes its GET branch for a routed GET, and a method-based middleware cannot be bypassed by casing. Both tests fail without the `ghttp_server_router_serve.go` change and pass with it. `go test ./net/ghttp/` shows no new failures against the master baseline. closes gogf#4830
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.
Fix #4765 — ghttp: Lowercase HTTP request methods causing route cache pollution
Root Cause
getHandlersWithCache() uses r.Method directly as the cache key without normalizing to uppercase. A lowercase get request creates a cache entry with key get:/api/user, which doesn't match the registered route GET:/api/user. Subsequent normal GET requests also hit the cached get:/api/user entry (which is empty), permanently returning 404 until the server is restarted.
Fix
Normalize r.Method to uppercase at the cache entry point:
Before:
method = r.Method
After:
method = strings.ToUpper(r.Method)
HTTP method names are case-insensitive per RFC 7230 section 3.1.1. The existing serveHandlerKey() already uses strings.ToUpper(method), so this fix makes the cache key consistent with the router key.
Changed File
Verification
Reproducer from issue should now show all requests returning 200 without restart.