gobackendgorilla

Go (GoLang) Gorilla/Mux server returns 404 when there is an empty string between two slashes in http path


This is my web server code ::

package main

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

    "github.com/gorilla/mux"
)

func FooHandler(w http.ResponseWriter, r *http.Request) {

    vars := mux.Vars(r)

    pathVal := vars["item"]

    fmt.Printf("\n item :: %s \n", pathVal)

    w.Write([]byte("Gorilla!\n"))
}

func main() {
    router := mux.NewRouter()

    routes := router.PathPrefix("/{item}").Subrouter()

    routes.Methods(http.MethodGet).
        Path("/foo").
        HandlerFunc(FooHandler)

    log.Fatal(http.ListenAndServe(":8000", router))
}

Everything works perfectly when I send requests via Postman to URLs like 127.0.0.1:8000/itemvalue/foo - the request reaches FooHandler, i get the correct value in pathVal.

What i need is for requests like 127.0.0.1:8000//foo (an empty string in place of item in URL path, so there are two slashes) to behave exactly the same. I want the request to reach the FooHandler, and pathVal to be empty string. However, I am currently getting a 404 page not found error; the request doesn't reach FooHandler.

I tried this ::

routes := router.PathPrefix("/{item:^$|.+}").Subrouter()

This regular expression should match an empty string or any string, but i still get "404 page not found" error.


Solution

  • The problem is, as @BMitch correctly pointed out, that the default mux as well as Gorilla normalize paths and will send you a "301 Moved Permanently" response.

    So, what you have to do is to move the error handling to a point where neither of them is involved. You can do this by wrapping the actual mux in a handler which deals with the invalid URL the way you want:

    package main
    
    import (
        "fmt"
        "log"
        "net/http"
        "regexp"
    
        "github.com/gorilla/mux"
    )
    
    var (
        // doubleSlashInFoo is a regex to match the URL path with double slashes in
        // the beginning and Ending with "//foo"
        // This, of course, can be adjusted to match any other pattern you want.
        doubleSlashInFoo = regexp.MustCompile(`^//foo$`)
    )
    
    func FooHandler(w http.ResponseWriter, r *http.Request) {
    
        vars := mux.Vars(r)
    
        pathVal := vars["item"]
    
        fmt.Printf("\n item :: %s \n", pathVal)
    
        w.Write([]byte("Gorilla!\n"))
    }
    
    // specialNotFoundHandler is a custom handler that checks for double slashes in
    // the URL path and returns a 400 Bad Request error if found. It also serves
    // the request to the underlying mux router. Only there the double slashes will cause
    // a "301 Moved Permanently" reponse.
    type specialNotFoundHandler struct {
        mux http.Handler
    }
    
    // ServeHTTP implements the http.Handler interface for specialNotFoundHandler.
    func (h *specialNotFoundHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
        if doubleSlashInFoo.MatchString(r.URL.Path) {
            http.Error(w, "The specified bucket is not valid.", http.StatusBadRequest)
            return
        }
        h.mux.ServeHTTP(w, r)
    }
    
    func main() {
        router := mux.NewRouter()
    
        routes := router.PathPrefix("/{item}").Subrouter()
        routes.Methods(http.MethodGet).
            Path("/foo").
            HandlerFunc(FooHandler)
    
        log.Fatal(http.ListenAndServe(":8000", &specialNotFoundHandler{mux: router}))
    }
    

    Calling this returns the expected response:

    $ curl -v http://localhost:8000//foo
    * Host localhost:8000 was resolved.
    * IPv6: ::1
    * IPv4: 127.0.0.1
    *   Trying [::1]:8000...
    * Connected to localhost (::1) port 8000
    > GET //foo HTTP/1.1
    > Host: localhost:8000
    > User-Agent: curl/8.7.1
    > Accept: */*
    > 
    * Request completely sent off
    < HTTP/1.1 400 Bad Request
    < Content-Type: text/plain; charset=utf-8
    < X-Content-Type-Options: nosniff
    < Date: Thu, 27 Mar 2025 17:26:36 GMT
    < Content-Length: 35
    < 
    The specified bucket is not valid.
    

    You might also want to have a look at @Thundercat's answer to a similar question where they describe a normalizer to prevent a "301 Moved Permanently" response.