diff --git a/internal/agent/client.go b/internal/agent/client.go index 7da6fcd..b437e9e 100644 --- a/internal/agent/client.go +++ b/internal/agent/client.go @@ -246,14 +246,14 @@ func (c *Client) handleResponse(resp *http.Response, dst any) error { // Try to parse as JSON error var errResp ErrorResponse if json.Unmarshal(body, &errResp) == nil && errResp.Error != "" { - return fmt.Errorf("API error %d: %s", resp.StatusCode, errResp.Error) + return &HTTPError{StatusCode: resp.StatusCode, Message: errResp.Error} } // Non-JSON response (e.g. HTML error page) — truncate to something readable msg := string(body) if len(msg) > 120 || strings.Contains(msg, "= 500 + } + // Fallback: network-level errors (no HTTP response received) + lower := strings.ToLower(err.Error()) + for _, keyword := range []string{"connection refused", "no such host", "timeout", "request failed"} { + if strings.Contains(lower, keyword) { + return true + } + } + return false +} + func (d *Daemon) poll(ctx context.Context) { resp, err := d.transport.ClaimTasks(ctx, d.cfg.AgentID) if err != nil { diff --git a/internal/agent/transport.go b/internal/agent/transport.go index 4bae6d7..5e223fb 100644 --- a/internal/agent/transport.go +++ b/internal/agent/transport.go @@ -6,6 +6,7 @@ import "context" // Both WebSocket (via CF Durable Object) and HTTP (direct to origin) implement this. type Transport interface { // Connect establishes the transport connection. + // Called internally by Daemon.Run — callers must NOT call Connect separately. Connect(ctx context.Context) error // Close tears down the connection gracefully. diff --git a/internal/agent/types.go b/internal/agent/types.go index 94e4751..dad1ddb 100644 --- a/internal/agent/types.go +++ b/internal/agent/types.go @@ -1,6 +1,9 @@ package agent -import "time" +import ( + "fmt" + "time" +) // RegisterRequest is sent by the CLI on startup to register itself. type RegisterRequest struct { @@ -147,6 +150,17 @@ type ErrorResponse struct { Details any `json:"details,omitempty"` } +// HTTPError represents an HTTP API error with a status code. +// Use errors.As to extract the status code for retry decisions. +type HTTPError struct { + StatusCode int + Message string +} + +func (e *HTTPError) Error() string { + return fmt.Sprintf("API error %d: %s", e.StatusCode, e.Message) +} + // AgentInfo holds metadata about the running agent for display. type AgentInfo struct { ID string