Unexpected 301 Redirect with http.StripPrefix + nested http.ServeMux in Go 1.24.1?

147 views
Skip to first unread message

Alexander Ertli

unread,
Apr 4, 2025, 8:52:18 AMApr 4
to golang-nuts

Hello,

I'm encountering unexpected behavior with net/http routing in Go 1.24.1 (amd64, ubuntu linux) when using http.StripPrefix to delegate to a nested http.ServeMux which uses the Go 1.22+ METHOD /path routing syntax.

Instead of the nested ServeMux executing its registered handlers for the stripped path, it consistently returns a 301 Moved Permanently redirect to the stripped path itself.

```go
package main

import (
"fmt"
"log"
"net/http"
"runtime"
)

func handleInnerTest(w http.ResponseWriter, r *http.Request) {
log.Printf("HANDLER HIT: handleInnerTest (GET /test) | Received Path: %s\n", r.URL.Path)
fmt.Fprintln(w, "OK - GET /test")
}

func handleInnerLogin(w http.ResponseWriter, r *http.Request) {
log.Printf("HANDLER HIT: handleInnerLogin (POST /login) | Received Path: %s\n", r.URL.Path)
if r.Method != http.MethodPost {
http.Error(w, "Method Not Allowed (Handler expected POST)", http.StatusMethodNotAllowed)
return
}
fmt.Fprintln(w, "OK - POST /login")
}

func main() {
log.Printf("Go Version: %s\n", runtime.Version())

innerMux := http.NewServeMux()
innerMux.HandleFunc("GET /test", handleInnerTest)
innerMux.HandleFunc("POST /login", handleInnerLogin)
innerMux.HandleFunc("GET /api2/test2", handleInnerTest)

outerMux := http.NewServeMux()

// This should remove "/api/" before innerMux sees the request.
outerMux.Handle("/api/", http.StripPrefix("/api/", innerMux))

outerMux.Handle("/api2/", innerMux)

outerMux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
log.Printf("HANDLER HIT: Outer Root Handler | Received Path: %s\n", r.URL.Path)
http.NotFound(w, r)
})

port := ":8090"
log.Printf("Starting server on %s...\n", port)
log.Println("----\nEXPECTED BEHAVIOR:")
log.Println("  GET /api/test       -> 200 OK ('OK - GET /test')")
log.Println("  GET /api/nonexistent -> 404 Not Found (from innerMux)")
log.Println("  POST /api/login      -> 200 OK ('OK - POST /login')")
log.Println("----")
log.Println("Run tests like:")
log.Println("  curl -v http://localhost:8090/api/test")          // is HTTP/1.1 301 Moved Permanently && no log print
log.Println("  curl -v http://localhost:8090/api/nonexistent")   // is HTTP/1.1 301 Moved Permanently && no log print
log.Println("  curl -v -X POST http://localhost:8090/api/login") // is HTTP/1.1 301 Moved Permanently && no log print
log.Println("  curl -v http://localhost:8090/test/")             // is HTTP/1.1 404 Not Found && prints HANDLER HIT: Outer Root Handler | Received Path: /test/
log.Println("  curl -v http://localhost:8090/api2/test2")        // is HTTP/1.1 200 OK && prints HANDLER HIT: handleInnerTest (GET /test) | Received Path: /api2/test2

log.Println("----")

err := http.ListenAndServe(port, outerMux)
if err != nil {
log.Fatalf("Server failed: %v\n", err)
}
}
```

It seems counter-intuitive that after http.StripPrefix modifies the path, the inner ServeMux doesn't seem to use that stripped path to match its handlers, instead issuing a redirect, like curl -v http://localhost:8090/api/test -> `<a href="/test">Moved Permanently</a>`

Have I misunderstood how these components should interact? 

Thanks for any insights.

Reto

unread,
Apr 5, 2025, 5:11:05 AMApr 5
to Alexander Ertli, golang-nuts
On Fri, Apr 04, 2025 at 04:01:56AM -0700, 'Alexander Ertli' via golang-nuts wrote:
> Instead of the nested ServeMux executing its registered handlers for the
> stripped path, it consistently returns a 301 Moved Permanently redirect to
> the stripped path itself.

> // This should remove "/api/" before innerMux sees the request.
> outerMux.Handle("/api/", http.StripPrefix("/api/", innerMux))

You are stripping too much though.

Your request going in looks like `GET /api/test`, after stripping however you
end up with `GET test`, note the lack of an initial root slash.

That's normally ok if you directly pass it to the handler, but you have a mux there
that expects to be at the root. Meaning sane requests must start with a root.

Removing the trailing slash from the stripPrefix makes it work, or just stop chaining muxers I guess.
> outerMux.Handle("/api/", http.StripPrefix("/api", innerMux))
Reply all
Reply to author
Forward
0 new messages