charmbracelet/crush · error

failed to decode workspaces: %w

Error message

failed to decode workspaces: %w

What it means

ListWorkspaces received a 200 response but the body could not be decoded into []proto.Workspace. This wraps the JSON decoding error and indicates the payload shape or encoding deviates from the expected array of workspace objects.

Source

Thrown at internal/client/proto.go:35

	"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)
	}
	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)

View on GitHub (pinned to 7944b8e522)

Solutions

  1. Read the wrapped json error: *json.UnmarshalTypeError signals schema drift between client and server.
  2. Align client and server versions so proto.Workspace matches.
  3. Capture the raw response body to confirm the actual payload shape.
  4. Check for intermediate proxies altering or truncating the response.

Example fix

// before
var workspaces []proto.Workspace
if err := json.NewDecoder(rsp.Body).Decode(&workspaces); err != nil {
	return nil, fmt.Errorf("failed to decode workspaces: %w", err)
}
// after: tolerate null/absent array
body, _ := io.ReadAll(rsp.Body)
var workspaces []proto.Workspace
if len(bytes.TrimSpace(body)) > 0 {
	if err := json.Unmarshal(body, &workspaces); err != nil {
		return nil, fmt.Errorf("failed to decode workspaces: %w (body: %s)", err, truncate(body, 256))
	}
}
Defensive patterns

Strategy: validation

Validate before calling

// Verify the endpoint returns a JSON array before relying on decode
resp, err := http.Get(baseURL + "/workspaces")
if err == nil {
	ct := resp.Header.Get("Content-Type")
	if !strings.Contains(ct, "application/json") {
		return fmt.Errorf("unexpected content type %q from server", ct)
	}
	resp.Body.Close()
}

Type guard

func isWorkspaceSchemaMismatch(err error) bool {
	var typeErr *json.UnmarshalTypeError
	return errors.As(err, &typeErr)
}

Try / catch

workspaces, err := client.ListWorkspaces(ctx)
if err != nil {
	var typeErr *json.UnmarshalTypeError
	if errors.As(err, &typeErr) {
		// Schema drift: refresh server or fall back to empty list
		log.Printf("workspace schema mismatch at offset %d; restarting server", typeErr.Offset)
		return restartServerAndList(ctx)
	}
	return err
}

Prevention

When it happens

Trigger: Calling ListWorkspaces when the server returns 200 with a malformed or differently-shaped JSON body — a single object instead of an array, changed Workspace field types, truncated body, or non-JSON content.

Common situations: Client and server built from different versions (Workspace schema drift); a proxy injecting content into the body; server bug returning null instead of an array; manual server-side modifications.

Understand the failure class

Related errors


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