charmbracelet/crush · error

failed to list workspaces: %w

Error message

failed to list workspaces: %w

What it means

ListWorkspaces calls GET /workspaces; this error wraps any transport-level failure from the HTTP client (connection refused, timeout, canceled context) before a response is obtained. It is a request-sending failure, not a server-reported status.

Source

Thrown at internal/client/proto.go:27

	"fmt"
	"io"
	"log/slog"
	"net/http"
	"net/url"
	"time"

	"github.com/charmbracelet/crush/internal/config"
	"github.com/charmbracelet/crush/internal/message"
	"github.com/charmbracelet/crush/internal/proto"
	"github.com/charmbracelet/crush/internal/pubsub"
	"github.com/charmbracelet/x/powernap/pkg/lsp/protocol"
)

// ListWorkspaces retrieves all workspaces from the server.
func (c *Client) ListWorkspaces(ctx context.Context) ([]proto.Workspace, error) {
	rsp, err := c.get(ctx, "/workspaces", nil, nil)
	if err != nil {
		return nil, fmt.Errorf("failed to list workspaces: %w", err)
	}
	defer rsp.Body.Close()
	if rsp.StatusCode != http.StatusOK {
		return nil, fmt.Errorf("failed to list workspaces: status code %d", rsp.StatusCode)
	}
	var workspaces []proto.Workspace
	if err := json.NewDecoder(rsp.Body).Decode(&workspaces); err != nil {
		return nil, fmt.Errorf("failed to decode workspaces: %w", err)
	}
	return workspaces, nil
}

// CreateWorkspace creates a new workspace on the server.
func (c *Client) CreateWorkspace(ctx context.Context, ws proto.Workspace) (*proto.Workspace, error) {
	ws.ClientID = c.clientID
	rsp, err := c.post(ctx, "/workspaces", nil, jsonBody(ws), http.Header{"Content-Type": []string{"application/json"}})
	if err != nil {
		return nil, fmt.Errorf("failed to create workspace: %w", err)

View on GitHub (pinned to 7944b8e522)

Solutions

  1. Check the wrapped error: connection refused means the daemon is not running — start it.
  2. Verify the server address/port configured for the client.
  3. Check for context cancellation/deadline and increase the timeout if the server is slow to start.
  4. Ensure no firewall or proxy blocks the connection to the daemon.

Example fix

// before
workspaces, err := client.ListWorkspaces(ctx)
if err != nil {
	return err
}
// after
workspaces, err := client.ListWorkspaces(ctx)
if err != nil {
	if isConnectionRefused(err) {
		if startErr := startServer(ctx); startErr != nil {
			return startErr
		}
		workspaces, err = client.ListWorkspaces(ctx)
	}
	if err != nil {
		return err
	}
}
Defensive patterns

Strategy: retry

Validate before calling

// Verify the daemon is listening before calling
conn, err := net.DialTimeout("tcp", serverAddr, 2*time.Second)
if err != nil {
	return fmt.Errorf("daemon not reachable at %s: %w", serverAddr, err)
}
conn.Close()

Type guard

func isTransportError(err error) bool {
	// Wrapped transport failure: no "status code" component means the request never completed
	s := err.Error()
	return !strings.Contains(s, "status code")
}

Try / catch

workspaces, err := client.ListWorkspaces(ctx)
if err != nil {
	var netErr net.Error
	if errors.As(err, &netErr) || strings.Contains(err.Error(), "connection refused") {
		if startErr := startDaemon(ctx); startErr == nil {
			workspaces, err = client.ListWorkspaces(ctx)
		}
	}
	if err != nil {
		return err
	}
}

Prevention

When it happens

Trigger: Calling ListWorkspaces when the daemon is not running or unreachable, the context is canceled, the connection is refused/reset, or DNS/TCP fails for the configured server address.

Common situations: The background server died or was never started; wrong port/address in configuration; firewall blocking localhost port; context deadline exceeded under slow startup; shutdownLegacyStaleServer probing a server that already exited.

Related errors


AI-assisted analysis of charmbracelet/crush@7944b8e522 (2026-08-29). Data as JSON: /api/errors/3a070d7f5f71dbac. Report an issue: GitHub.