Skip to content

ghttp: Lowercase HTTP request methods causing route cache pollution #4765

Description

@Xing-Fax

Go version

go version go1.26.1 linux/amd64

GoFrame version

v2.10.0

Can this bug be reproduced with the latest release?

Option Yes

What did you do?

Using a lowercase HTTP method (e.g., get) to send a request via http.NewRequest("get", ...) to a route that is registered with an exact method match (e.g., GET:/api/user):

req, _ := http.NewRequest("get", "http://127.0.0.1:8000/api/user", nil)
resp, _ := http.DefaultClient.Do(req)

What did you see happen?

The first request using a lowercase get returns 404 Not Found.
Subsequent normal GET requests to the same path will permanently return 404, even when using other HTTP clients (such as curl or Postman).
The issue can only be resolved by restarting the service.

package main

import (
	"fmt"
	"io"
	"net/http"
	"time"

	"github.com/gogf/gf/v2/net/ghttp"
)

func main() {
	s := ghttp.GetServer()
	s.BindHandler("GET:/api/user", func(r *ghttp.Request) {
		r.Response.Write("hello")
	})
	s.SetPort(8002)
	go s.Run()

	time.Sleep(2 * time.Second)

	addr := "http://127.0.0.1:8002/api/user"

	fmt.Println("=== Request 1: lowercase get ===")
	req1, _ := http.NewRequest("get", addr, nil)
	resp1, _ := http.DefaultClient.Do(req1)
	body1, _ := io.ReadAll(resp1.Body)
	fmt.Printf("Status: %d, Body: %s\n", resp1.StatusCode, string(body1))
	resp1.Body.Close()

	fmt.Println("\n=== Request 2: uppercase GET ===")
	req2, _ := http.NewRequest("GET", addr, nil)
	resp2, _ := http.DefaultClient.Do(req2)
	body2, _ := io.ReadAll(resp2.Body)
	fmt.Printf("Status: %d, Body: %s\n", resp2.StatusCode, string(body2))
	resp2.Body.Close()

	fmt.Println("\n=== Request 3: uppercase GET (again) ===")
	req3, _ := http.NewRequest("GET", addr, nil)
	resp3, _ := http.DefaultClient.Do(req3)
	body3, _ := io.ReadAll(resp3.Body)
	fmt.Printf("Status: %d, Body: %s\n", resp3.StatusCode, string(body3))
	resp3.Body.Close()
}
output:
2026-04-30T14:53:29.446+08:00 [INFO] pid[13742]: http server started listening on [:8002]
2026-04-30T14:53:29.446+08:00 [INFO] openapi specification is disabled

----------|--------|-----------|-----------------|-------------
| ADDRESS | METHOD |   ROUTE   |     HANDLER     | MIDDLEWARE |
----------|--------|-----------|-----------------|-------------
| :8002   | GET    | /api/user | main.main.func1 |            |
----------|--------|-----------|-----------------|-------------

=== Request 1: lowercase get ===
Status: 404, Body: Not Found

=== Request 2: uppercase GET ===
Status: 404, Body: Not Found

=== Request 3: uppercase GET (again) ===
Status: 404, Body: Not Found
package main

import (
	"fmt"
	"io"
	"net/http"
	"time"

	"github.com/gogf/gf/v2/net/ghttp"
)

func main() {
	s := ghttp.GetServer()
	s.BindHandler("GET:/api/user", func(r *ghttp.Request) {
		r.Response.Write("hello")
	})
	s.SetPort(8002)
	go s.Run()

	time.Sleep(2 * time.Second)

	addr := "http://127.0.0.1:8002/api/user"

	fmt.Println("=== Request 1: lowercase get ===")
	req1, _ := http.NewRequest("GET", addr, nil)
	resp1, _ := http.DefaultClient.Do(req1)
	body1, _ := io.ReadAll(resp1.Body)
	fmt.Printf("Status: %d, Body: %s\n", resp1.StatusCode, string(body1))
	resp1.Body.Close()

	fmt.Println("\n=== Request 2: uppercase GET ===")
	req2, _ := http.NewRequest("get", addr, nil)
	resp2, _ := http.DefaultClient.Do(req2)
	body2, _ := io.ReadAll(resp2.Body)
	fmt.Printf("Status: %d, Body: %s\n", resp2.StatusCode, string(body2))
	resp2.Body.Close()

	fmt.Println("\n=== Request 3: uppercase GET (again) ===")
	req3, _ := http.NewRequest("GET", addr, nil)
	resp3, _ := http.DefaultClient.Do(req3)
	body3, _ := io.ReadAll(resp3.Body)
	fmt.Printf("Status: %d, Body: %s\n", resp3.StatusCode, string(body3))
	resp3.Body.Close()
}
output:
2026-04-30T14:54:46.087+08:00 [INFO] pid[13890]: http server started listening on [:8002]
2026-04-30T14:54:46.087+08:00 [INFO] openapi specification is disabled

----------|--------|-----------|-----------------|-------------
| ADDRESS | METHOD |   ROUTE   |     HANDLER     | MIDDLEWARE |
----------|--------|-----------|-----------------|-------------
| :8002   | GET    | /api/user | main.main.func1 |            |
----------|--------|-----------|-----------------|-------------

=== Request 1: lowercase get ===
Status: 200, Body: hello

=== Request 2: uppercase GET ===
Status: 200, Body: hello

=== Request 3: uppercase GET (again) ===
Status: 200, Body: hello

What did you expect to see?

A lowercase get should be treated as equivalent to GET, correctly matching the route and returning 200 (HTTP method names are case-insensitive per RFC specifications).

Even if lowercase methods are not supported, they should not pollute the cache and cause subsequent valid requests to permanently return 404.

The best fix is to normalize the HTTP method at the entry point of getHandlersWithCache:

var (
    ctx    = r.Context()
    method = r.Method
    path   = r.URL.Path
    host   = r.GetHost()
)

Change it to:

var (
    ctx    = r.Context()
    method = strings.ToUpper(r.Method)
    path   = r.URL.Path
    host   = r.GetHost()
)

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugIt is confirmed a bug, but don't worry, we'll handle it.

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions