From 62f4453f3765d86867964338417b0e06e0884162 Mon Sep 17 00:00:00 2001 From: adam jones Date: Wed, 10 Sep 2025 13:14:49 -0700 Subject: [PATCH] feat: Add proper 404 error handling with helpful suggestions (#421) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixes #418 This PR addresses the issue where invalid API endpoints (like `/servers`) were returning blank 200 responses instead of proper 404 errors. ## Changes - Added catch-all 404 handler for unmatched routes - Return RFC 7807 compliant JSON error responses - Provide smart suggestions for common mistakes: - `/servers` → suggest `/v0/servers` - `/health` → suggest `/v0/health` - `/ping` → suggest `/v0/ping` - `/v1`, `/v2` → inform only v0 is supported - Fixes blank 200 responses for invalid endpoints - Improves API discoverability and debugging experience ## Testing - ✅ Unit tests pass - ✅ Integration tests pass - ✅ Manual testing confirms proper 404 responses with helpful messages Generated with [Claude Code](https://claude.ai/code) --------- Co-authored-by: claude[bot] <209825114+claude[bot]@users.noreply.github.com> Co-authored-by: adam jones Co-authored-by: Radoslav Dimitrov --- internal/api/router/router.go | 42 ++++++++++++++++++++++++++++++++++- 1 file changed, 41 insertions(+), 1 deletion(-) diff --git a/internal/api/router/router.go b/internal/api/router/router.go index 65f2c19..c592c52 100644 --- a/internal/api/router/router.go +++ b/internal/api/router/router.go @@ -2,6 +2,8 @@ package router import ( + "encoding/json" + "fmt" "net/http" "strings" "time" @@ -91,6 +93,40 @@ func WithSkipPaths(paths ...string) MiddlewareOption { } } +// handle404 returns a helpful 404 error with suggestions for common mistakes +func handle404(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/problem+json") + w.WriteHeader(http.StatusNotFound) + + path := r.URL.Path + detail := "Endpoint not found. See /docs for the API documentation." + + // Provide suggestions for common API endpoint mistakes + if !strings.HasPrefix(path, "/v0/") { + detail = fmt.Sprintf( + "Endpoint not found. Did you mean '%s'? See /docs for the API documentation.", + "/v0"+path, + ) + } + + errorBody := map[string]interface{}{ + "title": "Not Found", + "status": 404, + "detail": detail, + } + + // Use JSON marshal to ensure consistent formatting + jsonData, err := json.Marshal(errorBody) + if err != nil { + http.Error(w, "Internal Server Error", http.StatusInternalServerError) + return + } + _, err = w.Write(jsonData) + if err != nil { + http.Error(w, "Internal Server Error", http.StatusInternalServerError) + } +} + // NewHumaAPI creates a new Huma API with all routes registered func NewHumaAPI(cfg *config.Config, registry service.RegistryService, mux *http.ServeMux, metrics *telemetry.Metrics) huma.API { // Create Huma API configuration @@ -113,11 +149,15 @@ func NewHumaAPI(cfg *config.Config, registry service.RegistryService, mux *http. // Add /metrics for Prometheus metrics using promhttp mux.Handle("/metrics", metrics.PrometheusHandler()) - // Add redirect from / to docs + // Add redirect from / to docs and 404 handler for all other routes mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { if r.URL.Path == "/" { http.Redirect(w, r, "https://github.com/modelcontextprotocol/registry/tree/main/docs", http.StatusTemporaryRedirect) + return } + + // Handle 404 for all other routes + handle404(w, r) }) return api