charmbracelet/crush · error

failed to decode workspace: %w

Error message

failed to decode workspace: %w

What it means

CreateWorkspace performed a successful HTTP POST to /workspaces (status passed checkStatus), but the response body could not be decoded into proto.Workspace via encoding/json. This means the server returned a 2xx response whose body is not the expected workspace JSON shape. The library wraps the decode error with %w so the underlying json error (syntax or type mismatch) is preserved.

Source

Thrown at internal/client/proto.go:53

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

View on GitHub (pinned to 7944b8e522)

Solutions

  1. Verify client and server are the same crush version (upgrade or rebuild both).
  2. Log the raw response body (e.g. io.ReadAll before decode) to see what the server actually returned.
  3. Check that the server URL points at the real crush server, not a proxy/other service that returns HTML with 200.
  4. Retry the create; a transient truncation may succeed on a second attempt.
  5. Inspect the server logs for errors producing a malformed response.

Example fix

// before: decode blindly
var created proto.Workspace
if err := json.NewDecoder(rsp.Body).Decode(&created); err != nil {
    return nil, fmt.Errorf("failed to decode workspace: %w", err)
}
// after: surface the body for diagnosis
body, _ := io.ReadAll(rsp.Body)
var created proto.Workspace
if err := json.Unmarshal(body, &created); err != nil {
    return nil, fmt.Errorf("failed to decode workspace: %w (body: %s)", err, body)
}
Defensive patterns

Strategy: try-catch

Validate before calling

// pre-flight: confirm the endpoint returns JSON
rsp, err := http.Get(serverURL + "/workspaces")
if err != nil || !strings.Contains(rsp.Header.Get("Content-Type"), "application/json") {
    return fmt.Errorf("server does not speak JSON at %s", serverURL)
}

Try / catch

created, err := client.CreateWorkspace(ctx, ws)
if err != nil {
    var urlErr *url.Error
    switch {
    case errors.As(err, &urlErr):
        return fmt.Errorf("transport failure: %w", urlErr)
    case strings.Contains(err.Error(), "failed to decode"):
        return fmt.Errorf("server/client version mismatch or bad response: %w", err)
    default:
        return err
    }
}

Prevention

When it happens

Trigger: Calling client.CreateWorkspace when the server returns a non-JSON or structurally different 2xx body: empty body, HTML error page from a proxy, truncated response, or a server version whose Workspace JSON fields have incompatible types for the client's proto.Workspace struct.

Common situations: Client and server (crush serve) built from different versions where the Workspace schema drifted; a reverse proxy or captive portal intercepting requests; server crashing mid-response leaving a truncated body; pointing the client at a wrong port hosting a non-crush HTTP service that returns 200 with HTML.

Understand the failure class

Related errors


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