Troubleshooting Routing
This guide covers common routing issues in Tracks applications and how to debug them. All Tracks applications use the Chi router, which provides a lightweight, idiomatic router for building Go HTTP services.
Route Not Found (404)
When you get a 404 error, the route isn't registered or the pattern doesn't match.
Debug with chi.Walk()
The chi.Walk() function is your best friend for debugging routing issues. It prints all registered routes:
package main
import (
"fmt"
"net/http"
"github.com/go-chi/chi/v5"
)
func main() {
r := chi.NewRouter()
// Register your routes...
// Print all registered routes for debugging
chi.Walk(r, func(method string, route string, handler http.Handler, middlewares ...func(http.Handler) http.Handler) error {
fmt.Printf("[%s] %s\n", method, route)
return nil
})
http.ListenAndServe(":8080", r)
}
Common causes:
- Route not registered - Check
internal/http/routes.goto ensure the route is registered - Typo in route constant - Verify the constant value matches the expected pattern
- Middleware blocking request - A middleware earlier in the chain may be returning early
- Route order matters - More specific routes must be registered before catch-all patterns
Route Registration Checklist
// 1. Define constant in internal/http/routes/users.go
const UserShow = "/users/:username"
// 2. Register in internal/http/routes.go
s.router.Get(routes.UserShow, userHandler.HandleShow)
// 3. Verify handler exists
func (h *UserHandler) HandleShow(w http.ResponseWriter, r *http.Request) {
// ...
}
Testing Routes with curl
# Test route exists
curl -i http://localhost:8080/users/johndoe
# Expected: 200 OK or appropriate response
# If 404: Route not registered or pattern mismatch
Middleware Not Executing
Middleware must be registered in the correct order and must call next.ServeHTTP() to continue the chain.
Middleware Registration Order
Middleware executes in the order it's registered:
func (s *Server) routes() {
// Global middleware (runs for ALL routes)
s.router.Use(middleware.RequestID) // 1. Runs first
s.router.Use(middleware.RealIP) // 2. Then this
s.router.Use(httpmiddleware.Logging(s.logger)) // 3. Then logging
s.router.Use(middleware.Recoverer) // 4. Finally recoverer
// Routes registered after middleware
s.router.Get("/users", handler.HandleIndex)
}
Common mistakes:
- Registering routes before middleware - Routes registered before
.Use()won't have that middleware - Forgetting to call next.ServeHTTP() - Breaks the middleware chain
- Middleware panic without recovery - Use
middleware.Recovererto catch panics
Correct Middleware Pattern
func MyMiddleware() func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Do something before handler
// CRITICAL: Call next handler in chain
next.ServeHTTP(w, r)
// Do something after handler (optional)
})
}
}
Debugging Middleware Execution
Add logging to verify middleware is running:
func MyMiddleware(logger *zerolog.Logger) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
logger.Debug().
Str("path", r.URL.Path).
Msg("MyMiddleware executing")
next.ServeHTTP(w, r)
logger.Debug().
Str("path", r.URL.Path).
Msg("MyMiddleware complete")
})
}
}
Route Parameters Not Found
Chi uses URL parameters with :param syntax. If chi.URLParam() returns an empty string, the parameter name doesn't match.
Correct Parameter Extraction
// Route definition in internal/http/routes/users.go
const (
UserSlugParam = "username"
UserShow = "/users/:username" // Must use :username
)
// Handler in internal/http/handlers/user_handler.go
func (h *UserHandler) HandleShow(w http.ResponseWriter, r *http.Request) {
// Extract parameter - use the constant to avoid typos
username := chi.URLParam(r, routes.UserSlugParam)
if username == "" {
http.Error(w, "Username parameter missing", http.StatusBadRequest)
return
}
// Use username...
}
Common mistakes:
- Typo in parameter name -
chi.URLParam(r, "user")when route uses:username - Missing colon in route -
/users/usernameinstead of/users/:username - Wrong route pattern - Using
chi.URLParam(r, "id")on a route with:username
Always Use Route Constants
Never use magic strings for parameter names:
// Good - uses constant
username := chi.URLParam(r, routes.UserSlugParam)
// Bad - magic string (typo-prone)
username := chi.URLParam(r, "username")
Route Conflicts
Chi matches routes in the order they're registered. More specific routes must come before catch-all patterns.
Route Specificity
func (s *Server) routes() {
// Specific routes FIRST
s.router.Get("/users/new", handler.HandleNew) // Must be first
s.router.Get("/users/:username", handler.HandleShow) // Then parameterized
// This won't work if reversed - /:username would catch /new
}
Rule: Static segments beat parameters, so register static routes first.
Debugging Route Conflicts
Use chi.Walk() to see the order routes are registered:
chi.Walk(r, func(method string, route string, handler http.Handler, middlewares ...func(http.Handler) http.Handler) error {
fmt.Printf("[%s] %s\n", method, route)
return nil
})
// Output shows registration order:
// [GET] /users/new
// [GET] /users/:username
// ✓ Correct order
CORS and Preflight Issues
CORS requires proper middleware configuration and understanding of preflight requests.
CORS Middleware Setup
import (
"github.com/go-chi/cors"
)
func (s *Server) routes() {
// CORS middleware must be registered early
s.router.Use(cors.Handler(cors.Options{
AllowedOrigins: []string{"https://app.example.com"},
AllowedMethods: []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"},
AllowedHeaders: []string{"Accept", "Authorization", "Content-Type"},
ExposedHeaders: []string{"Link"},
AllowCredentials: true,
MaxAge: 300,
}))
// Routes...
}
Preflight Requests (OPTIONS)
Browsers send OPTIONS requests before POST/PUT/DELETE. Chi handles this automatically if you register the route with POST/PUT/DELETE methods.
Common mistake:
// Bad - only handles POST, OPTIONS requests will 404
s.router.Post("/users", handler.HandleCreate)
// Good - Chi automatically handles OPTIONS for registered methods
s.router.Post("/users", handler.HandleCreate) // OPTIONS automatically allowed
Debugging CORS with curl
# Test preflight request
curl -i -X OPTIONS http://localhost:8080/users \
-H "Origin: https://app.example.com" \
-H "Access-Control-Request-Method: POST"
# Expected headers in response:
# Access-Control-Allow-Origin: https://app.example.com
# Access-Control-Allow-Methods: POST, OPTIONS
# Access-Control-Allow-Headers: Content-Type, ...
Server Won't Shutdown Gracefully
Graceful shutdown requires proper context handling and timeout configuration.
Correct Graceful Shutdown Pattern
func (s *Server) Start(ctx context.Context) error {
srv := &http.Server{
Addr: s.addr,
Handler: s.router,
}
// Channel to signal server errors
serverErrors := make(chan error, 1)
// Start server in goroutine
go func() {
s.logger.Info().Str("addr", s.addr).Msg("Starting server")
serverErrors <- srv.ListenAndServe()
}()
// Wait for interrupt signal or server error
select {
case err := <-serverErrors:
return fmt.Errorf("server error: %w", err)
case <-ctx.Done():
s.logger.Info().Msg("Shutdown signal received")
// Give outstanding requests time to complete
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
defer cancel()
if err := srv.Shutdown(ctx); err != nil {
s.logger.Error().Err(err).Msg("Graceful shutdown failed")
return srv.Close() // Force close
}
s.logger.Info().Msg("Server stopped gracefully")
return nil
}
}
Common Shutdown Issues
- Blocked goroutines - Background tasks not respecting context cancellation
- Short timeout - Increase shutdown timeout if requests take longer
- Database connection not closed - Ensure DB cleanup in shutdown
Debugging Shutdown Hangs
Add logging to identify what's blocking:
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
defer cancel()
s.logger.Info().Msg("Starting graceful shutdown")
if err := srv.Shutdown(ctx); err != nil {
s.logger.Error().
Err(err).
Msg("Shutdown timeout - some requests didn't complete")
return srv.Close()
}
s.logger.Info().Msg("All requests completed, server stopped")
Common Chi Router Gotchas
1. Trailing Slashes
Chi treats /users and /users/ as different routes by default.
// These are DIFFERENT routes
s.router.Get("/users", handler.HandleIndex) // /users
s.router.Get("/users/", handler.HandleIndex) // /users/ (different!)
// Solution: Use middleware to handle trailing slashes
import "github.com/go-chi/chi/v5/middleware"
s.router.Use(middleware.StripSlashes) // Strips trailing slashes
2. Method Mismatch
Chi routes are method-specific. GET request to a POST route will 404.
// Route only accepts POST
s.router.Post("/users", handler.HandleCreate)
// GET /users will return 404, not 405 Method Not Allowed
3. Middleware Scope
Middleware can be global, group-scoped, or route-specific:
// Global - applies to all routes
s.router.Use(middleware.Logger)
// Group-scoped - applies to routes in this group
s.router.Group(func(r chi.Router) {
r.Use(AuthMiddleware)
r.Get("/admin", handler.HandleAdmin)
})
// Route-specific - only this route
s.router.With(RateLimitMiddleware).Get("/api/heavy", handler.HandleHeavy)
Debugging Checklist
When debugging routing issues, check in this order:
-
Is the route registered?
- Use
chi.Walk()to list all routes - Check
internal/http/routes.gofor registration
- Use
-
Does the pattern match?
- Verify route constant value
- Check for typos in parameter names
- Test with curl:
curl -i http://localhost:8080/your/path
-
Is middleware blocking?
- Add logging to each middleware
- Verify
next.ServeHTTP()is called - Check middleware order
-
Are parameters extracted correctly?
- Use route constants for parameter names
- Check for empty string returns from
chi.URLParam() - Verify
:paramsyntax in route pattern
-
Is the method correct?
- GET vs POST vs PUT vs DELETE
- Check browser dev tools for actual method sent
-
Check response headers
- Use curl with
-iflag to see headers - Verify Content-Type is set correctly
- Check for CORS headers if cross-origin
- Use curl with
Useful Tools
curl Examples
# View full request/response including headers
curl -i http://localhost:8080/users
# Follow redirects
curl -L http://localhost:8080/users/new
# Send POST with form data
curl -X POST http://localhost:8080/users \
-d "username=john&email=john@example.com"
# Send JSON
curl -X POST http://localhost:8080/api/webhooks \
-H "Content-Type: application/json" \
-d '{"event":"user.created"}'
# Test CORS preflight
curl -i -X OPTIONS http://localhost:8080/users \
-H "Origin: https://app.example.com" \
-H "Access-Control-Request-Method: POST"
Chi Debugging Functions
// Print all registered routes
chi.Walk(router, func(method string, route string, handler http.Handler, middlewares ...func(http.Handler) http.Handler) error {
fmt.Printf("[%s] %s (middlewares: %d)\n", method, route, len(middlewares))
return nil
})
// Get current route pattern in handler
routePattern := chi.RouteContext(r.Context()).RoutePattern()
fmt.Printf("Matched route: %s\n", routePattern)
Next Steps
- Routing Guide - Comprehensive routing patterns and best practices
- Architecture Overview - High-level system design
- Patterns - Common implementation patterns
- Testing Guide - Testing strategies for routes and handlers