102 lines
2.9 KiB
Go
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)
|
|
})
|
|
}
|