Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions caddy/health.go
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,11 @@ func RepairRoutes(result *HealthCheckResult, sessions map[string]*SessionInfo) e
return fmt.Errorf("Caddy is not running")
}

// Ensure routes array exists (Caddy can't append to null)
if err := client.EnsureRoutesArray(); err != nil {
return fmt.Errorf("failed to initialize routes array: %w", err)
}

// If catch-all is first, we need to reorder all routes
if result.CatchAllFirst {
fmt.Println("Fixing route order (catch-all route is blocking specific routes)...")
Expand Down
5 changes: 5 additions & 0 deletions caddy/provisioning.go
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,11 @@ func ProvisionSessionRoutesWithProject(sessionName string, services map[string]i
return make(map[string]string), nil
}

// Ensure routes array exists (Caddy can't append to null)
if err := client.EnsureRoutesArray(); err != nil {
return nil, fmt.Errorf("failed to initialize routes array: %w", err)
}

// Check if there are any catch-all routes that need to be moved to the end
existingRoutes, err := client.GetAllRoutes()
if err == nil {
Expand Down
101 changes: 94 additions & 7 deletions caddy/routes.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,8 +42,9 @@ type RouteResponse struct {

// CaddyClient manages communication with Caddy's admin API
type CaddyClient struct {
client *resty.Client
baseURL string
client *resty.Client
baseURL string
serverName string
}

// NewCaddyClient creates a new Caddy API client
Expand All @@ -56,10 +57,48 @@ func NewCaddyClient() *CaddyClient {
client := resty.New()
client.SetTimeout(10 * time.Second)

return &CaddyClient{
c := &CaddyClient{
client: client,
baseURL: caddyAPI,
}
c.discoverServerName()
return c
}

// discoverServerName finds the HTTP server listening on :80.
// Falls back to "srv1" on any failure.
func (c *CaddyClient) discoverServerName() {
c.serverName = "srv1" // default fallback

resp, err := c.client.R().Get(c.baseURL + "/config/apps/http/servers")
if err != nil || resp.StatusCode() != http.StatusOK {
return
}

var servers map[string]json.RawMessage
if err := json.Unmarshal(resp.Body(), &servers); err != nil {
return
}

for name, raw := range servers {
var srv struct {
Listen []string `json:"listen"`
}
if err := json.Unmarshal(raw, &srv); err != nil {
continue
}
for _, addr := range srv.Listen {
if strings.Contains(addr, ":80") {
c.serverName = name
return
}
}
}
}
Comment on lines +70 to +97
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Port-matching logic can produce false positives for ports like :8080, :800, :8000.

strings.Contains(addr, ":80") matches any address containing the substring :80, not just port 80. A server listening on :8080 or :8000 would be incorrectly selected.

🐛 Proposed fix: use a stricter match
 		for _, addr := range srv.Listen {
-			if strings.Contains(addr, ":80") {
+			if addr == ":80" || strings.HasSuffix(addr, ":80") {
 				c.serverName = name
 				return
 			}

This covers both :80 and 0.0.0.0:80 / [::]:80 while avoiding false matches on :8080, :800, etc.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
func (c *CaddyClient) discoverServerName() {
c.serverName = "srv1" // default fallback
resp, err := c.client.R().Get(c.baseURL + "/config/apps/http/servers")
if err != nil || resp.StatusCode() != http.StatusOK {
return
}
var servers map[string]json.RawMessage
if err := json.Unmarshal(resp.Body(), &servers); err != nil {
return
}
for name, raw := range servers {
var srv struct {
Listen []string `json:"listen"`
}
if err := json.Unmarshal(raw, &srv); err != nil {
continue
}
for _, addr := range srv.Listen {
if strings.Contains(addr, ":80") {
c.serverName = name
return
}
}
}
}
func (c *CaddyClient) discoverServerName() {
c.serverName = "srv1" // default fallback
resp, err := c.client.R().Get(c.baseURL + "/config/apps/http/servers")
if err != nil || resp.StatusCode() != http.StatusOK {
return
}
var servers map[string]json.RawMessage
if err := json.Unmarshal(resp.Body(), &servers); err != nil {
return
}
for name, raw := range servers {
var srv struct {
Listen []string `json:"listen"`
}
if err := json.Unmarshal(raw, &srv); err != nil {
continue
}
for _, addr := range srv.Listen {
if addr == ":80" || strings.HasSuffix(addr, ":80") {
c.serverName = name
return
}
}
}
}
🤖 Prompt for AI Agents
In `@caddy/routes.go` around lines 70 - 97, The port-matching in
discoverServerName uses strings.Contains(addr, ":80") which false-positives on
ports like :8080; update the loop that iterates srv.Listen (the addr variable)
to reliably extract the port (use net.SplitHostPort or handle leading ":" cases)
and compare the port string exactly to "80" before setting c.serverName; ensure
IPv6 forms like "[::]:80" and short forms like ":80" are handled and skip
entries that don’t parse to a host:port pair.


// serverPath returns the Caddy config path for the discovered HTTP server.
func (c *CaddyClient) serverPath() string {
return "/config/apps/http/servers/" + c.serverName
}

// CreateRoute creates a route for a service
Expand Down Expand Up @@ -116,7 +155,7 @@ func (c *CaddyClient) CreateRouteWithProject(sessionName, serviceName string, po
resp, err := c.client.R().
SetHeader("Content-Type", "application/json").
SetBody(routeJSON).
Post(c.baseURL + "/config/apps/http/servers/srv1/routes/-")
Post(c.baseURL + c.serverPath() + "/routes/-")

if err != nil {
return "", fmt.Errorf("failed to create route: %w", err)
Expand Down Expand Up @@ -183,15 +222,26 @@ func (c *CaddyClient) DeleteSessionRoutes(sessionName string) error {

// GetAllRoutes retrieves all routes from Caddy
func (c *CaddyClient) GetAllRoutes() ([]Route, error) {
resp, err := c.client.R().Get(c.baseURL + "/config/apps/http/servers/srv1/routes")
resp, err := c.client.R().Get(c.baseURL + c.serverPath() + "/routes")
if err != nil {
return nil, fmt.Errorf("failed to list routes: %w", err)
}

// Routes path doesn't exist yet — treat as empty
if resp.StatusCode() == http.StatusNotFound {
return []Route{}, nil
}

if resp.StatusCode() != http.StatusOK {
return nil, fmt.Errorf("caddy API returned status %d: %s", resp.StatusCode(), resp.String())
}

// Handle null or empty body
body := strings.TrimSpace(string(resp.Body()))
if body == "null" || body == "" {
return []Route{}, nil
}

// Parse routes response
var routes []Route
if err := json.Unmarshal(resp.Body(), &routes); err != nil {
Expand All @@ -201,6 +251,43 @@ func (c *CaddyClient) GetAllRoutes() ([]Route, error) {
return routes, nil
}

// EnsureRoutesArray initializes the server's routes array if it is null or missing.
// Caddy cannot append a route to a null RouteList, so this must be called before
// any route-creation batch.
func (c *CaddyClient) EnsureRoutesArray() error {
resp, err := c.client.R().Get(c.baseURL + c.serverPath())
if err != nil {
return fmt.Errorf("failed to read server config: %w", err)
}
if resp.StatusCode() != http.StatusOK {
return fmt.Errorf("caddy API returned status %d reading server config: %s", resp.StatusCode(), resp.String())
}

var serverCfg map[string]json.RawMessage
if err := json.Unmarshal(resp.Body(), &serverCfg); err != nil {
return fmt.Errorf("failed to parse server config: %w", err)
}

raw, exists := serverCfg["routes"]
if exists && strings.TrimSpace(string(raw)) != "null" {
return nil // routes array already present
}

// PATCH with an empty routes array — merges into existing config
resp, err = c.client.R().
SetHeader("Content-Type", "application/json").
SetBody([]byte(`{"routes":[]}`)).
Patch(c.baseURL + c.serverPath())
if err != nil {
return fmt.Errorf("failed to initialize routes array: %w", err)
}
if resp.StatusCode() != http.StatusOK {
return fmt.Errorf("caddy API returned status %d initializing routes: %s", resp.StatusCode(), resp.String())
}

return nil
}

// CheckCaddyConnection verifies that Caddy is running and accessible
func (c *CaddyClient) CheckCaddyConnection() error {
resp, err := c.client.R().Get(c.baseURL + "/config/")
Expand Down Expand Up @@ -240,7 +327,7 @@ func GetServiceMapping(portName string) string {
// ReplaceAllRoutes deletes all current routes and creates new ones in the specified order
func (c *CaddyClient) ReplaceAllRoutes(routes []Route) error {
// First, delete all existing routes
resp, err := c.client.R().Delete(c.baseURL + "/config/apps/http/servers/srv1/routes")
resp, err := c.client.R().Delete(c.baseURL + c.serverPath() + "/routes")
if err != nil {
return fmt.Errorf("failed to delete existing routes: %w", err)
}
Expand All @@ -258,7 +345,7 @@ func (c *CaddyClient) ReplaceAllRoutes(routes []Route) error {
resp, err = c.client.R().
SetHeader("Content-Type", "application/json").
SetBody(routesJSON).
Post(c.baseURL + "/config/apps/http/servers/srv1/routes")
Post(c.baseURL + c.serverPath() + "/routes")

if err != nil {
return fmt.Errorf("failed to create routes: %w", err)
Expand Down
Loading
Loading