Files
descrybe/apps/api/cmd/api/main.go
T

117 lines
3.9 KiB
Go
Raw Normal View History

package main
import (
"context"
"log"
"log/slog"
"net"
"net/http"
"os"
"os/signal"
"syscall"
"time"
"github.com/descrybe/descrybe-v2/apps/api/internal/auth"
"github.com/descrybe/descrybe-v2/apps/api/internal/config"
"github.com/descrybe/descrybe-v2/apps/api/internal/db"
"github.com/descrybe/descrybe-v2/apps/api/internal/httpapi"
"github.com/descrybe/descrybe-v2/apps/api/internal/logredact"
)
func main() {
log.SetOutput(logredact.Writer(os.Stderr))
slog.SetDefault(slog.New(logredact.NewJSONHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelInfo})))
cfg, err := config.Load()
if err != nil {
log.Fatalf("config: %v", err)
}
2026-08-16 17:38:15 +02:00
if cfg.InsecureLocalProductionActive() {
slog.Warn("ALLOW_INSECURE_LOCAL_PRODUCTION=1 with loopback WEB_ORIGIN — not for public deploy",
"web_origin", cfg.WebOrigin,
"http_addr", cfg.HTTPAddr,
)
}
if cfg.ShouldWarnRateLimits() {
slog.Warn(cfg.RateLimitWarningMessage(),
"rate_limit_replicas", cfg.RateLimitReplicas,
"rate_limit_multi_replica", cfg.RateLimitMultiReplica,
"rate_limit_backend", cfg.RateLimitBackend,
"rate_limit_backend_requested", cfg.RateLimitBackendRequested,
)
}
ctx := context.Background()
pool, err := db.NewPool(ctx, cfg.DatabaseURL, db.PoolOptions{
MaxConns: int32(cfg.DBMaxConns),
MinConns: int32(cfg.DBMinConns),
MaxConnLifetime: cfg.DBMaxConnLifetime,
MaxConnLifetimeJitter: cfg.DBMaxConnLifetimeJitter,
MaxConnIdleTime: cfg.DBMaxConnIdleTime,
HealthCheckPeriod: cfg.DBHealthCheckPeriod,
StatementTimeout: cfg.DBStatementTimeout,
})
if err != nil {
log.Fatalf("db: %v", err)
}
defer pool.Close()
sessions := auth.NewSessionManager(pool, cfg.SessionCookieName, cfg.SessionCookieDomain, cfg.CookieSecure(), cfg.SessionIdleHours)
srv := httpapi.NewServer(cfg, pool, sessions)
runCtx, runCancel := context.WithCancel(context.Background())
defer runCancel()
// Lightweight AI fallback poller so FAQ-miss tickets drain without a separate worker.
if srv.Support != nil {
go srv.Support.RunAutoJobsLoop(runCtx, 2*time.Second, 3)
}
httpServer := newHTTPServer(cfg.HTTPAddr, srv.Router())
ln, err := net.Listen("tcp", cfg.HTTPAddr)
if err != nil {
log.Fatalf("listen: %v", err)
}
slog.Info("api_listening", "addr", cfg.HTTPAddr, "maintenance", cfg.MaintenanceMode, "read_only", cfg.ReadOnlyMode)
go func() {
if err := httpServer.Serve(ln); err != nil && err != http.ErrServerClosed {
log.Fatalf("serve: %v", err)
}
}()
stop := make(chan os.Signal, 1)
signal.Notify(stop, syscall.SIGINT, syscall.SIGTERM)
<-stop
runCancel()
shutdownCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
_ = httpServer.Shutdown(shutdownCtx)
}
// newHTTPServer configures net/http timeouts for the API listener.
//
// Tradeoff — WriteTimeout vs long sync/export:
// WriteTimeout bounds the whole ServeHTTP + response write. Feed export
// streams and sync-style handlers can run for many minutes; a short
// WriteTimeout aborts them mid-stream (clients see truncated/hanging
// responses). We use a long WriteTimeout ceiling instead of 0 (unlimited)
// so wedged handlers still release connections eventually. Finer per-route
// deadlines belong on request contexts / middleware for normal JSON APIs.
// Leaving WriteTimeout unset (0) would never reclaim a stuck writer.
func newHTTPServer(addr string, handler http.Handler) *http.Server {
return &http.Server{
Addr: addr,
Handler: handler,
// Headers-only Slowloris guard (independent of ReadTimeout).
ReadHeaderTimeout: 10 * time.Second,
// Full request read (headers + body). Above typical API JSON uploads.
ReadTimeout: 60 * time.Second,
// Long ceiling so streaming exports/sync can finish; see comment above.
WriteTimeout: 15 * time.Minute,
// Close keep-alive connections idle between requests.
IdleTimeout: 120 * time.Second,
}
}