package web import ( "crypto/rand" "encoding/hex" "log" "net/http" "net/url" "path/filepath" "runtime/debug" "strings" ) // registerRoutes is set by internal/web/handlers's init() (via RegisterRoutes), // avoiding an import cycle: handlers imports web for *App, so web cannot import // handlers back — it calls into this hook instead. var registerRoutes func(*App, *http.ServeMux) // RegisterRoutes installs the application's route-registration callback. Called // once from the handlers package's init(). func RegisterRoutes(fn func(*App, *http.ServeMux)) { registerRoutes = fn } // Handler builds the full HTTP handler: routing, static assets, session // load/save, panic recovery, request-id tagging and base-URL prefix handling. func (a *App) Handler() http.Handler { mux := http.NewServeMux() if registerRoutes != nil { registerRoutes(a, mux) } staticDir := filepath.Join(a.Cfg.WebDir, "static") mux.Handle("GET /static/", http.StripPrefix("/static/", http.FileServer(http.Dir(staticDir)))) var h http.Handler = mux h = a.recoverer(h) h = a.requestID(h) h = a.Sessions.LoadAndSave(h) h = a.stripBasePrefix(h) return h } // recoverer converts panics in handlers into a 500 response instead of crashing // the server. func (a *App) recoverer(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { defer func() { if rec := recover(); rec != nil { log.Printf("panic: %v\n%s", rec, debug.Stack()) http.Error(w, "Внутренняя ошибка сервера.", http.StatusInternalServerError) } }() next.ServeHTTP(w, r) }) } // requestID tags every response with a short correlation id (X-Request-Id), // useful for grepping logs; purely optional/best-effort. func (a *App) requestID(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { id := r.Header.Get("X-Request-Id") if id == "" { b := make([]byte, 8) _, _ = rand.Read(b) id = hex.EncodeToString(b) } w.Header().Set("X-Request-Id", id) next.ServeHTTP(w, r) }) } // stripBasePrefix strips the configured APP_BASE_URL path prefix (if any) from // incoming requests before they reach the router, so route patterns stay // prefix-free. Requests that arrive without the prefix (e.g. an internal // container health check hitting 127.0.0.1 directly) are passed through // unchanged rather than 404ing. func (a *App) stripBasePrefix(next http.Handler) http.Handler { prefix := strings.Trim(a.Cfg.BaseURL, "/") if prefix == "" { return next } p := "/" + prefix return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.URL.Path == p || strings.HasPrefix(r.URL.Path, p+"/") { trimmed := strings.TrimPrefix(r.URL.Path, p) if trimmed == "" { trimmed = "/" } r2 := new(http.Request) *r2 = *r u2 := new(url.URL) *u2 = *r.URL u2.Path = trimmed r2.URL = u2 next.ServeHTTP(w, r2) return } next.ServeHTTP(w, r) }) }