{"record":{"id":"ea37c8b3fcbe0427","repo":"charmbracelet/crush","slug":"failed-to-decode-workspaces-w","errorCode":null,"errorMessage":"failed to decode workspaces: %w","messagePattern":"failed to decode workspaces: %w","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"internal/client/proto.go","lineNumber":35,"sourceCode":"\t\"github.com/charmbracelet/crush/internal/message\"\n\t\"github.com/charmbracelet/crush/internal/proto\"\n\t\"github.com/charmbracelet/crush/internal/pubsub\"\n\t\"github.com/charmbracelet/x/powernap/pkg/lsp/protocol\"\n)\n\n// ListWorkspaces retrieves all workspaces from the server.\nfunc (c *Client) ListWorkspaces(ctx context.Context) ([]proto.Workspace, error) {\n\trsp, err := c.get(ctx, \"/workspaces\", nil, nil)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to list workspaces: %w\", err)\n\t}\n\tdefer rsp.Body.Close()\n\tif rsp.StatusCode != http.StatusOK {\n\t\treturn nil, fmt.Errorf(\"failed to list workspaces: status code %d\", rsp.StatusCode)\n\t}\n\tvar workspaces []proto.Workspace\n\tif err := json.NewDecoder(rsp.Body).Decode(&workspaces); err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to decode workspaces: %w\", err)\n\t}\n\treturn workspaces, nil\n}\n\n// CreateWorkspace creates a new workspace on the server.\nfunc (c *Client) CreateWorkspace(ctx context.Context, ws proto.Workspace) (*proto.Workspace, error) {\n\tws.ClientID = c.clientID\n\trsp, err := c.post(ctx, \"/workspaces\", nil, jsonBody(ws), http.Header{\"Content-Type\": []string{\"application/json\"}})\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to create workspace: %w\", err)\n\t}\n\tdefer rsp.Body.Close()\n\tif err := checkStatus(rsp); err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to create workspace: %w\", err)\n\t}\n\tvar created proto.Workspace\n\tif err := json.NewDecoder(rsp.Body).Decode(&created); err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to decode workspace: %w\", err)","sourceCodeStart":17,"sourceCodeEnd":53,"githubUrl":"https://github.com/charmbracelet/crush/blob/7944b8e52225d8805e31eacbf7ef24856b0dfb7a/internal/client/proto.go#L17-L53","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","solutions":["Read the wrapped json error: *json.UnmarshalTypeError signals schema drift between client and server.","Align client and server versions so proto.Workspace matches.","Capture the raw response body to confirm the actual payload shape.","Check for intermediate proxies altering or truncating the response."],"exampleFix":"// before\nvar workspaces []proto.Workspace\nif err := json.NewDecoder(rsp.Body).Decode(&workspaces); err != nil {\n\treturn nil, fmt.Errorf(\"failed to decode workspaces: %w\", err)\n}\n// after: tolerate null/absent array\nbody, _ := io.ReadAll(rsp.Body)\nvar workspaces []proto.Workspace\nif len(bytes.TrimSpace(body)) > 0 {\n\tif err := json.Unmarshal(body, &workspaces); err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to decode workspaces: %w (body: %s)\", err, truncate(body, 256))\n\t}\n}","handlingStrategy":"validation","validationCode":"// Verify the endpoint returns a JSON array before relying on decode\nresp, err := http.Get(baseURL + \"/workspaces\")\nif err == nil {\n\tct := resp.Header.Get(\"Content-Type\")\n\tif !strings.Contains(ct, \"application/json\") {\n\t\treturn fmt.Errorf(\"unexpected content type %q from server\", ct)\n\t}\n\tresp.Body.Close()\n}","typeGuard":"func isWorkspaceSchemaMismatch(err error) bool {\n\tvar typeErr *json.UnmarshalTypeError\n\treturn errors.As(err, &typeErr)\n}","tryCatchPattern":"workspaces, err := client.ListWorkspaces(ctx)\nif err != nil {\n\tvar typeErr *json.UnmarshalTypeError\n\tif errors.As(err, &typeErr) {\n\t\t// Schema drift: refresh server or fall back to empty list\n\t\tlog.Printf(\"workspace schema mismatch at offset %d; restarting server\", typeErr.Offset)\n\t\treturn restartServerAndList(ctx)\n\t}\n\treturn err\n}","preventionTips":["Version-lock client and server so proto.Workspace stays in sync.","Add a startup smoke test that decodes a sample /workspaces response.","Treat a JSON null or empty body as an empty workspace list defensively.","Bypass or configure proxies so they do not alter response bodies."],"tags":["json","decoding","client","schema"],"backgroundTag":"json-decode-failed","analyzedSha":"7944b8e52225d8805e31eacbf7ef24856b0dfb7a","analyzedAt":"2026-08-29T12:48:59.079Z","schemaVersion":2},"datasetVersion":"2026-08-29T17:17:51.833Z"}