charmbracelet/crush · error

failed to get workspace: %w

Error message

failed to get workspace: %w

What it means

GetWorkspace wraps an error returned by the client's underlying c.get transport call (building/sending the HTTP GET /workspaces/{id}). This fires before any status checking, so it indicates the request itself failed at the network/transport layer, not that the server rejected it. The original error (e.g. connection refused, context canceled) is preserved via %w.

Source

Thrown at internal/client/proto.go:62

	if err != nil {
		return nil, fmt.Errorf("failed to create workspace: %w", err)
	}
	defer rsp.Body.Close()
	if err := checkStatus(rsp); err != nil {
		return nil, fmt.Errorf("failed to create workspace: %w", err)
	}
	var created proto.Workspace
	if err := json.NewDecoder(rsp.Body).Decode(&created); err != nil {
		return nil, fmt.Errorf("failed to decode workspace: %w", err)
	}
	return &created, nil
}

// GetWorkspace retrieves a workspace from the server.
func (c *Client) GetWorkspace(ctx context.Context, id string) (*proto.Workspace, error) {
	rsp, err := c.get(ctx, fmt.Sprintf("/workspaces/%s", id), nil, nil)
	if err != nil {
		return nil, fmt.Errorf("failed to get workspace: %w", err)
	}
	defer rsp.Body.Close()
	if err := checkStatus(rsp); err != nil {
		return nil, fmt.Errorf("failed to get workspace: %w", err)
	}
	var ws proto.Workspace
	if err := json.NewDecoder(rsp.Body).Decode(&ws); err != nil {
		return nil, fmt.Errorf("failed to decode workspace: %w", err)
	}
	return &ws, nil
}

// DeleteWorkspace deletes a workspace on the server.
func (c *Client) DeleteWorkspace(ctx context.Context, id string) error {
	q := url.Values{"client_id": []string{c.clientID}}
	rsp, err := c.delete(ctx, fmt.Sprintf("/workspaces/%s", id), q, nil)
	if err != nil {
		return fmt.Errorf("failed to delete workspace: %w", err)

View on GitHub (pinned to 7944b8e522)

Solutions

  1. Confirm the crush server is running and reachable (curl the /workspaces endpoint).
  2. Check the client's configured server URL/port matches the server's actual listen address.
  3. Use errors.Is(err, context.DeadlineExceeded) to distinguish timeout vs connectivity and retry with a longer deadline.
  4. Retry the request; transient network errors often resolve.
  5. Verify DNS resolution and any proxy environment variables (HTTP_PROXY/HTTPS_PROXY) are correct.

Example fix

// before
ws, err := client.GetWorkspace(ctx, id)
if err != nil { return err }
// after: classify and retry transport errors
ws, err := client.GetWorkspace(ctx, id)
if err != nil {
    if errors.Is(err, context.DeadlineExceeded) || errors.Is(err, context.Canceled) {
        return err
    }
    time.Sleep(time.Second)
    ws, err = client.GetWorkspace(ctx, id)
    if err != nil { return fmt.Errorf("get workspace: %w", err) }
}
Defensive patterns

Strategy: retry

Validate before calling

// probe reachability before the call
conn, err := net.DialTimeout("tcp", host, 2*time.Second)
if err != nil { return fmt.Errorf("server unreachable: %w", err) }
conn.Close()

Try / catch

ws, err := client.GetWorkspace(ctx, id)
if err != nil {
    if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) {
        return err
    }
    var ne net.Error
    if errors.As(err, &ne) && ne.Timeout() {
        return retryWithBackoff()
    }
    return err
}

Prevention

When it happens

Trigger: Calling client.GetWorkspace(ctx, id) when the crush server is unreachable: wrong URL/port, server process down, TLS handshake failure, or the passed context is canceled/expired mid-request.

Common situations: crush serve not running or crashed; client configured with a stale or mistyped server address; firewall/network partition; a long-running operation whose context deadline exceeded before the GET completed.

Related errors


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