8000 fix(net/ghttp): normalize HTTP method to uppercase in getHandlersWithCache to prevent route cache pollution (#4765) by waterWang · Pull Request #4812 · gogf/gf · GitHub
[go: up one dir, main page]

Skip to content

fix(net/ghttp): normalize HTTP method to uppercase in getHandlersWithCache to prevent route cache pollution (#4765) - #4812

Merged
hailaz merged 2 commits into
gogf:masterfrom
waterWang:fix-http-method-case
Jul 30, 2026
Merged

hailaz merged 2 commits into
gogf:masterfrom
waterWang:fix-http-method-case

Conversation

@waterWang
Copy link
Copy Markdown

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

  • 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.

@hailaz hailaz changed the title fix: normalize HTTP method to uppercase in getHandlersWithCache to prevent route cache pollution (#4765) fix(net/ghttp): normalize HTTP method to uppercase in getHandlersWithCache to prevent route cache pollution (#4765) Jul 30, 2026
@hailaz hailaz added the ready to merge Used in PR, which means this PR is reviewed. label Jul 30, 2026
@hailaz
hailaz merged commit 739921d into gogf:master Jul 30, 2026
18 checks passed
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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ready to merge Used in PR, which means this PR is reviewed.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

ghttp: Lowercase HTTP request methods causing route cache pollution

2 participants

0