Skip to content

net/http: Handling Nested Groups #72789

Closed as not planned
Closed as not planned
@gavin-iru

Description

@gavin-iru

Go version

go 1.24.1

Output of go env in your module/workspace:

GOGCCFLAGS='-fPIC -arch arm64 -pthread -fno-caret-diagnostics -Qunused-arguments -fmessage-length=0 -ffile-prefix-map=/var/folders/hb/1n61v0yj11zc1rnwkbm974tm0000gn/T/go-build1330795007=/tmp/go-build -gno-record-gcc-switches -fno-common'
GOHOSTARCH='arm64'
GOHOSTOS='darwin'
GOINSECURE=''
GOMOD='/Users/***/go.mod'
GOMODCACHE='/Users/**/go/pkg/mod'
GONOPROXY=''
GONOSUMDB=''
GOOS='darwin'
GOPATH='/Users/**/go'
GOPRIVATE=''
GOPROXY='https://proxy.golang.org,direct'
GOROOT='/Users/**/go/pkg/mod/golang.org/toolchain@v0.0.1-go1.24.1.darwin-arm64'
GOSUMDB='sum.golang.org'
GOTELEMETRY='local'
GOTELEMETRYDIR='/Users/***/Library/Application Support/go/telemetry'
GOTMPDIR=''
GOTOOLCHAIN='auto'
GOTOOLDIR='/Users/chin/go/pkg/mod/golang.org/toolchain@v0.0.1-go1.24.1.darwin-arm64/pkg/tool/darwin_arm64'
GOVCS=''
GOVERSION='go1.24.1'
GOWORK=''
PKG_CONFIG='pkg-config'

What did you do?

ServeMux Pattern Matching Behavior with Grouped Routes

Problem Description

I'm trying to organize my HTTP routes into logical groups using nested ServeMux, but I'm encountering unexpected behavior with pattern matching.

API Structure

I have the following API endpoints:

GET    /v1/users
POST   /v1/users
GET    /v1/users/attributes
GET    /v1/users/{id}

Implementation Attempt

I'm trying to organize the code by grouping user-related handlers:

func mainHandler() {
    mux := http.NewServeMux()
    userHandler := userHandler(mux)
}

func userHandler(mux *http.ServeMux) {
    userMux := http.NewServeMux()
    
    userMux.Handle("GET /users", getUserLogic())
    userMux.Handle("POST /users", postUserLogic())
    userMux.Handle("GET /users/attributes", getUserAttributesLogic())
    userMux.Handle("GET /users/{id}", getUserByIdLogic())
}

What did you see happen?

Observed Behavior

I've tried three different approaches to register the user routes:

Scenario 1: Without trailing slash

mux.Handle("/v1/users", userMux)

Result: Only matches /v1/users endpoints, returns 404 for /v1/users/attributes and /v1/users/{id}

Scenario 2: With trailing slash

mux.Handle("/v1/users/", userMux)

Result: Only matches /v1/users/attributes and /v1/users/{id}, returns 404 for /v1/users endpoints

Scenario 3: Using StripPrefix

mux.Handle("/v1/users/", http.StripPrefix("/v1/users", userMux))

With modified handlers:

userMux.Handle("GET ", getUserLogic())
userMux.Handle("POST ", postUserLogic())
userMux.Handle("GET /attributes", getUserAttributesLogic())
userMux.Handle("GET /{id}", getUserByIdLogic())

Result: Error - "Pattern matching fails because host/path missing /"

What did you expect to see?

Question

Why does ServeMux exhibit this behavior with grouped routes, and is there a way to achieve the desired routing structure without requiring trailing slashes? The current pattern matching seems to force us to choose between handling root-level routes (/v1/users) OR nested routes (/v1/users/*), but not both simultaneously.

I'd prefer not to add trailing slashes to all my routes. Is this a limitation of ServeMux, or am I missing something in the implementation?

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions