Files
humanlayer/hld/daemon/http_server.go
Allison Durham 5454e7b690 feat(daemon): add dynamic port allocation support
- Always create HTTP server regardless of port configuration
- Support port 0 for dynamic allocation
- Output actual port to stdout when using dynamic allocation
- Enable WUI to start daemon on any available port
2025-07-30 10:20:06 -07:00

144 lines
4.0 KiB
Go

package daemon
import (
"context"
"fmt"
"log/slog"
"net"
"net/http"
"time"
"github.com/gin-contrib/cors"
"github.com/gin-gonic/gin"
"github.com/humanlayer/humanlayer/hld/api"
"github.com/humanlayer/humanlayer/hld/api/handlers"
"github.com/humanlayer/humanlayer/hld/approval"
"github.com/humanlayer/humanlayer/hld/bus"
"github.com/humanlayer/humanlayer/hld/config"
"github.com/humanlayer/humanlayer/hld/session"
"github.com/humanlayer/humanlayer/hld/store"
)
// HTTPServer manages the REST API server
type HTTPServer struct {
config *config.Config
router *gin.Engine
sessionHandlers *handlers.SessionHandlers
approvalHandlers *handlers.ApprovalHandlers
sseHandler *handlers.SSEHandler
server *http.Server
}
// NewHTTPServer creates a new HTTP server instance
func NewHTTPServer(
cfg *config.Config,
sessionManager session.SessionManager,
approvalManager approval.Manager,
conversationStore store.ConversationStore,
eventBus bus.EventBus,
) *HTTPServer {
// Set Gin mode to release
gin.SetMode(gin.ReleaseMode)
router := gin.New()
// Add middleware
router.Use(gin.Recovery())
router.Use(gin.LoggerWithConfig(gin.LoggerConfig{
SkipPaths: []string{"/api/v1/health"}, // Skip health check logs
}))
router.Use(handlers.RequestIDMiddleware())
router.Use(handlers.CompressionMiddleware())
// Add CORS middleware for browser clients
router.Use(cors.New(cors.Config{
AllowOrigins: []string{"*"}, // TODO: Configure allowed origins
AllowMethods: []string{"GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"},
AllowHeaders: []string{"Origin", "Content-Type", "Accept", "Authorization", "X-Client", "X-Client-Version"},
ExposeHeaders: []string{"X-Request-ID"},
AllowCredentials: false,
MaxAge: 12 * time.Hour,
}))
// Create handlers
sessionHandlers := handlers.NewSessionHandlers(sessionManager, conversationStore, approvalManager)
approvalHandlers := handlers.NewApprovalHandlers(approvalManager, sessionManager)
sseHandler := handlers.NewSSEHandler(eventBus)
return &HTTPServer{
config: cfg,
router: router,
sessionHandlers: sessionHandlers,
approvalHandlers: approvalHandlers,
sseHandler: sseHandler,
}
}
// Start starts the HTTP server
func (s *HTTPServer) Start(ctx context.Context) error {
// Create server implementation combining all handlers
serverImpl := handlers.NewServerImpl(s.sessionHandlers, s.approvalHandlers, s.sseHandler)
// Create strict handler with middleware
strictHandler := api.NewStrictHandler(serverImpl, nil)
// Create API v1 route group
v1 := s.router.Group("/api/v1")
// Register OpenAPI handlers under the v1 group
api.RegisterHandlers(v1, strictHandler)
// Register SSE endpoint directly (not part of strict interface)
v1.GET("/stream/events", s.sseHandler.StreamEvents)
// Create listener first to handle port 0
addr := fmt.Sprintf("%s:%d", s.config.HTTPHost, s.config.HTTPPort)
listener, err := net.Listen("tcp", addr)
if err != nil {
return fmt.Errorf("failed to listen on %s: %w", addr, err)
}
// Get actual port after binding
actualAddr := listener.Addr().(*net.TCPAddr)
actualPort := actualAddr.Port
// If port 0 was used, output actual port to stdout
if s.config.HTTPPort == 0 {
fmt.Printf("HTTP_PORT=%d\n", actualPort)
}
slog.Info("Starting HTTP server",
"configured_port", s.config.HTTPPort,
"actual_address", actualAddr.String())
// Create HTTP server
s.server = &http.Server{
Handler: s.router,
}
// Start server in goroutine
go func() {
// Use the existing listener
if err := s.server.Serve(listener); err != nil && err != http.ErrServerClosed {
slog.Error("HTTP server error", "error", err)
}
}()
// Wait for context cancellation
<-ctx.Done()
return s.Shutdown()
}
// Shutdown gracefully shuts down the HTTP server
func (s *HTTPServer) Shutdown() error {
if s.server == nil {
return nil
}
slog.Info("Shutting down HTTP server")
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
return s.server.Shutdown(ctx)
}