{"record":{"id":"a2b9a7dd3ef7f039","repo":"gastownhall/beads","slug":"failed-to-parse-issue-response-w","errorCode":null,"errorMessage":"failed to parse issue response: %w","messagePattern":"failed to parse issue response: %w","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"internal/gitlab/client.go","lineNumber":406,"sourceCode":"\tvar links []IssueLink\n\tif err := json.Unmarshal(respBody, &links); err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to parse issue links response: %w\", err)\n\t}\n\n\treturn links, nil\n}\n\n// FetchIssueByIID retrieves a single issue by its project-scoped IID.\nfunc (c *Client) FetchIssueByIID(ctx context.Context, iid int) (*Issue, error) {\n\turlStr := c.buildURL(\"/projects/\"+c.projectPath()+\"/issues/\"+strconv.Itoa(iid), nil)\n\trespBody, _, err := c.doRequest(ctx, http.MethodGet, urlStr, nil)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to fetch issue %d: %w\", iid, err)\n\t}\n\n\tvar issue Issue\n\tif err := json.Unmarshal(respBody, &issue); err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to parse issue response: %w\", err)\n\t}\n\n\treturn &issue, nil\n}\n\n// FetchMilestones retrieves milestones from the project with optional state filter.\n// state can be: \"active\", \"closed\", or \"\" (all).\nfunc (c *Client) FetchMilestones(ctx context.Context, state string) ([]Milestone, error) {\n\tparams := map[string]string{\n\t\t\"per_page\": strconv.Itoa(MaxPageSize),\n\t}\n\tif state != \"\" {\n\t\tparams[\"state\"] = state\n\t}\n\n\turlStr := c.buildURL(\"/projects/\"+c.projectPath()+\"/milestones\", params)\n\trespBody, _, err := c.doRequest(ctx, http.MethodGet, urlStr, nil)\n\tif err != nil {","sourceCodeStart":388,"sourceCodeEnd":424,"githubUrl":"https://github.com/gastownhall/beads/blob/71377f276968b452ee607177637970a4ff888584/internal/gitlab/client.go#L388-L424","documentation":"After FetchIssueByIID gets a successful HTTP response from /projects/:id/issues/:iid, the body is unmarshaled into the Issue struct. This error wraps the json.Unmarshal failure when the response body is not valid JSON or does not match the Issue struct shape. The HTTP call succeeded, so the problem is the payload content, not connectivity.","triggerScenarios":"Calling Client.FetchIssueByIID when a proxy/SSO layer returns HTML instead of JSON, the body is empty or truncated, or a GitLab API change introduces fields whose types conflict with the Issue struct (e.g. string vs number).","commonSituations":"Corporate proxies injecting HTML error/login pages; self-hosted GitLab versions with divergent issue payloads; misconfigured base URL hitting a non-API route; gzip/encoding issues stripping the body.","solutions":["Capture and log the raw response body plus the wrapped *json.UnmarshalTypeError/json.SyntaxError to identify the mismatch","Confirm the client base URL points at the GitLab API host (not a web UI or SSO redirect)","Compare the payload against the Issue struct fields; update struct types/json tags to match your GitLab version","Retry if the body could have been truncated by a transient gateway error"],"exampleFix":"// before\nissue, err := client.FetchIssueByIID(ctx, iid)\n// after\nissue, err := client.FetchIssueByIID(ctx, iid)\nif err != nil {\n    var syn *json.SyntaxError\n    if errors.As(err, &syn) {\n        log.Printf(\"invalid JSON at offset %d — body likely HTML/empty\", syn.Offset)\n    }\n    return err\n}","handlingStrategy":"type-guard","validationCode":"// Confirm base URL is the API host before calling\nif !strings.HasSuffix(strings.TrimSuffix(gitlabHost, \"/\"), \"/api\") && !strings.Contains(gitlabHost, \"/api/\") {\n    // client should build /api/v4 paths itself; a web-UI URL will yield HTML\n    log.Printf(\"warning: %s may not be an API host\", gitlabHost)\n}","typeGuard":"func isJSONParseError(err error) bool {\n    var se *json.SyntaxError\n    var te *json.UnmarshalTypeError\n    return errors.As(err, &se) || errors.As(err, &te)\n}","tryCatchPattern":"issue, err := client.FetchIssueByIID(ctx, iid)\nif err != nil {\n    if isJSONParseError(err) {\n        // body was HTML/empty — inspect proxy config, not the issue ID\n        return fmt.Errorf(\"non-JSON issue payload: %w\", err)\n    }\n    return err\n}\nif issue == nil || issue.IID == 0 {\n    return errors.New(\"empty issue payload\")\n}","preventionTips":["Never front the API host with an SSO redirect for non-interactive clients","Log raw bodies when unmarshal errors occur to detect HTML error pages","Keep the Issue struct's json tags in sync with your GitLab release","Watch for GitLab upgrades that change field types (string vs number)"],"tags":["gitlab","json","api-response"],"backgroundTag":"malformed-json-response","analyzedSha":"71377f276968b452ee607177637970a4ff888584","analyzedAt":"2026-08-30T18:55:39.744Z","schemaVersion":2},"datasetVersion":"2026-08-30T23:17:21.991Z"}