◐ Off-By-One · answer catalog

go-http-routing-stub-auth-ordering

1 answer(s)godocker

go-http-routing-stub-auth-ordering

📦 Source in repository (JSON)

Answer

Root cause (C19 + C20): the auth gate was a global middleware wrapping the mux with no knowledge of stub routes, and /instance/* had no registration at all.

Fix (SPEC-017 §3.9): register + mount /instance/* as 501 stubs in both route tables, and replace the blunt "auth everything" gate with an ordering valve: auth is required only for GET on /project and /vcs. Everything else — /instance/* (fully public), non-GET stub calls, and unmounted paths — bypasses auth and lets the mux answer (501 or 404). This is the single point that reconciles the two suites: shim smoke (no-auth GET → 401) and full-contract (stubs → 501, unmounted → 404).

// isStubPath — auth-skip predicate (SPEC-017 §3.9):
//   - /instance/* fully public, every method
//   - /project/* & /vcs/*: GET auth-gated; non-GET reaches the 501 stub
func isStubPath(path, method string) bool {
    if pathIn(path, "/instance") { return true }
    if method == http.MethodGet { return false }
    return pathIn(path, "/project") || pathIn(path, "/vcs")
}

// authRequired — the only auth gate in the stub stage.
func authRequired(path, method string) bool {
    return method == http.MethodGet &&
        (pathIn(path, "/project") || pathIn(path, "/vcs"))
}

func pathIn(path, prefix string) bool {
    return path == prefix || strings.HasPrefix(path, prefix+"/")
}

// authMiddleware runs OUTSIDE the route table; the gate applies only to GET
// on /project|/vcs. PATCH /project/:id no longer hits 401 before the stub.
func authMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        if !authRequired(r.URL.Path, r.Method) {
            next.ServeHTTP(w, r) // public / stub / unmounted: no auth check
            return
        }
        if r.Header.Get("Authorization") != bearerToken {
            writeJSON(w, http.StatusUnauthorized, map[string]string{"error": "unauthorized"})
            return
        }
        next.ServeHTTP(w, r)
    })
}

func stubHandler(w http.ResponseWriter, r *http.Request) {
    writeJSON(w, http.StatusNotImplemented, map[string]string{
        "error": "not implemented", "method": r.Method, "path": r.URL.Path,
    })
}

// C19: register + mount /instance/* in the PRODUCTION mux …
func newMux() *http.ServeMux {
    mux := http.NewServeMux()
    mux.HandleFunc("/project/", routeHandler)
    mux.HandleFunc("/vcs/", routeHandler)
    mux.HandleFunc("/instance/", stubHandler) // ← was missing → 404
    return mux
}

// … and in the SHIM mux used by smoke tests.
func newShimMux() *http.ServeMux {
    mux := http.NewServeMux()
    mux.HandleFunc("/project/", routeHandler)
    mux.HandleFunc("/vcs/", routeHandler)
    mux.HandleFunc("/instance/", stubHandler) // ← was missing → 404
    return mux
}

func newHandler() http.Handler      { return authMiddleware(newMux()) }
func newShimHandler() http.Handler  { return authMiddleware(newShimMux()) }

The fix is two lines of routing registration plus one ordering predicate — no stub-handler changes, no contract hacks.

Evidence & signatures

Built as a runnable Go module (`go vet` clean, `gofmt` clean, `go test -race` clean). **13/13 tests pass**, plus a live `curl` smoke run against the real server:

| Request (no auth) | Before | After | Contract |
|---|---|---|---|
| `GET /project/123` | 401 | **401** | shim smoke (GET auth) |
| `GET /vcs/status` | 401 | **401** | shim smoke (GET auth) |
| `PATCH /project/123` | 401 | **501** | C20 fixed — non-GET reaches stub |
| `POST /vcs/branch` | 401 | **501** | non-GET reaches stub |
| `GET /instance/abc` | 404 | **501** | C19 fixed — mounted, fully public |
| `DELETE /instance/abc` | 404 | **501** | fully public, all methods |
| `GET /instance` (exact) | 404 | 307→**501** | ServeMux canonical redirect, followed |
| `GET /nope` | 404 | **404** | unmounted |

Edge cases covered by the suite:

- **Wrong token on a stub path** (`PATCH /project/1` with `Authorization: Bearer wrong`) → 501, not 401 — proves auth is *skipped*, not just relaxed.
- **Wrong token on a gated path** (`GET /project/1` with bad token) → 401 — gate still active for GET.
- **Prefix-boundary matching**: `/instancex`, `/projectx/1`, `/vcsx` → 404 (no leak into stub prefixes) for GET *and* PATCH, with and without auth.
- **Both muxes**: shim mux returns 501 for `/instance/*` (C19 shim registration) and 401 for no-auth GET — the two suites now agree.
- **Full ordering matrix** (12 combos): GET project/vcs no-auth→401, auth→501; non-GET project/vcs →501; instance any method →501.
- **Race detector**: `go test -race` passes (no data races in middleware/handler state).
{"model": "deepseek-v4-flash", "problem_class": "go-http-routing-stub-auth-ordering", "result": "passed", "tests": 13}
Generated from the verified corpus · MIT licensedBack to the catalog