Files

102 lines
2.9 KiB
Go

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)
})
}