{"record":{"id":"aab85364010bf5dc","repo":"charmbracelet/crush","slug":"failed-to-decode-skills-w","errorCode":null,"errorMessage":"failed to decode skills: %w","messagePattern":"failed to decode skills: %w","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"internal/client/config.go","lineNumber":231,"sourceCode":"\tif err := json.NewDecoder(rsp.Body).Decode(&result); err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to decode init prompt response: %w\", err)\n\t}\n\treturn result.Prompt, nil\n}\n\n// ListSkills retrieves the visible skills for a workspace.\nfunc (c *Client) ListSkills(ctx context.Context, id string) ([]proto.SkillInfo, error) {\n\trsp, err := c.get(ctx, fmt.Sprintf(\"/workspaces/%s/skills\", id), nil, nil)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to list skills: %w\", err)\n\t}\n\tdefer rsp.Body.Close()\n\tif rsp.StatusCode != http.StatusOK {\n\t\treturn nil, fmt.Errorf(\"failed to list skills: status code %d\", rsp.StatusCode)\n\t}\n\tvar skills []proto.SkillInfo\n\tif err := json.NewDecoder(rsp.Body).Decode(&skills); err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to decode skills: %w\", err)\n\t}\n\treturn skills, nil\n}\n\n// ReadSkill reads a skill's content by ID from the server.\nfunc (c *Client) ReadSkill(ctx context.Context, id, skillID string) (*proto.ReadSkillResponse, error) {\n\trsp, err := c.post(ctx, fmt.Sprintf(\"/workspaces/%s/skills/read\", id), nil, jsonBody(proto.ReadSkillRequest{\n\t\tSkillID: skillID,\n\t}), http.Header{\"Content-Type\": []string{\"application/json\"}})\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to read skill: %w\", err)\n\t}\n\tdefer rsp.Body.Close()\n\tif rsp.StatusCode != http.StatusOK {\n\t\treturn nil, fmt.Errorf(\"failed to read skill: status code %d\", rsp.StatusCode)\n\t}\n\tvar result proto.ReadSkillResponse\n\tif err := json.NewDecoder(rsp.Body).Decode(&result); err != nil {","sourceCodeStart":213,"sourceCodeEnd":249,"githubUrl":"https://github.com/charmbracelet/crush/blob/7944b8e52225d8805e31eacbf7ef24856b0dfb7a/internal/client/config.go#L213-L249","documentation":"Returned by Client.ListSkills when the server responded 200 OK but the response body could not be decoded into []proto.SkillInfo. The server sent malformed JSON, an unexpected shape (e.g. an object wrapping the array, or an error payload with a 200 status), or an empty/truncated body. This is a client/server contract mismatch.","triggerScenarios":"Server returns 200 with HTML (proxy error page), an empty body, a JSON object instead of an array, or field types that don't match proto.SkillInfo's JSON schema.","commonSituations":"Reverse proxy intercepting the request and returning a 200 HTML page; server-side API version change renaming or retyping SkillInfo fields; middleware writing extra content to the response body.","solutions":["Dump the raw response body (e.g. with a debug HTTP proxy or by checking server logs) to see what was actually returned.","Verify the server's skills endpoint returns a JSON array matching proto.SkillInfo's json tags.","Check for client/server version skew and upgrade the mismatched side.","Bypass any proxy/middleware to confirm it isn't rewriting the response.","Validate the payload with curl | jq to confirm it is a well-formed array."],"exampleFix":"// before\nskills, err := client.ListSkills(ctx, wsID) // fails: cannot unmarshal object into []SkillInfo\n// after\n// Confirm server shape first:\n//   curl -s $BASE/workspaces/$WS/skills | jq 'type'  -> must be \"array\"\nskills, err := client.ListSkills(ctx, wsID)\nif err != nil {\n    return fmt.Errorf(\"server returned unexpected skills payload; check client/server versions: %w\", err)\n}","handlingStrategy":"type-guard","validationCode":null,"typeGuard":"func isDecodeError(err error) bool {\n    return err != nil && strings.Contains(err.Error(), \"failed to decode skills\")\n}\n\nfunc isValidSkillList(skills []proto.SkillInfo) bool {\n    for _, s := range skills { if s.ID == \"\" { return false } }\n    return true\n}","tryCatchPattern":"skills, err := client.ListSkills(ctx, wsID)\nif err != nil {\n    if isDecodeError(err) {\n        // contract mismatch: log raw payload via server logs, degrade gracefully\n        return emptySkillsFallback()\n    }\n    return err\n}","preventionTips":["Keep client proto structs in sync with the server API.","Smoke-test endpoints with curl | jq after server upgrades.","Watch for proxies that replace JSON bodies with HTML pages.","Add contract/golden tests for the skills endpoint."],"tags":["http-client","json-decode","skills","contract-mismatch"],"backgroundTag":"json-unmarshal-failed","analyzedSha":"7944b8e52225d8805e31eacbf7ef24856b0dfb7a","analyzedAt":"2026-08-29T12:48:59.079Z","schemaVersion":2},"datasetVersion":"2026-08-29T17:17:51.833Z"}