package httpx import ( "context" "errors" "log/slog" "net" "net/http" "strings" "time" ) // NewServer создаёт http.Server с общими таймаутами. WriteTimeout не задаётся: // стриминг аудио и загрузки держат ответ открытым дольше любого разумного предела. func NewServer(addr string, h http.Handler) *http.Server { return &http.Server{ Addr: addr, Handler: h, ReadHeaderTimeout: 10 * time.Second, IdleTimeout: 120 * time.Second, } } // Serve запускает сервер и корректно останавливает его при отмене ctx. func Serve(ctx context.Context, log *slog.Logger, srv *http.Server, shutdownTimeout time.Duration) error { errCh := make(chan error, 1) go func() { log.Info("listening", "addr", srv.Addr) if err := srv.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) { errCh <- err } close(errCh) }() select { case err := <-errCh: return err case <-ctx.Done(): } log.Info("shutting down") shutdownCtx, cancel := context.WithTimeout(context.Background(), shutdownTimeout) defer cancel() return srv.Shutdown(shutdownCtx) } // RealIP подставляет в RemoteAddr адрес клиента из X-Real-IP, который выставляет nginx. // В отличие от middleware.RealIP из chi, не доверяет True-Client-IP и X-Forwarded-For: // их клиент может прислать сам, а nginx пропускает их дальше без изменений. func RealIP(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if ip := strings.TrimSpace(r.Header.Get("X-Real-IP")); net.ParseIP(ip) != nil { r.RemoteAddr = ip } next.ServeHTTP(w, r) }) }