{"record":{"id":"9068c0614261cc5e","repo":"gastownhall/beads","slug":"failed-to-fetch-work-items-w","errorCode":null,"errorMessage":"failed to fetch work items: %w","messagePattern":"failed to fetch work items: %w","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"internal/ado/client.go","lineNumber":353,"sourceCode":"\t}\n\n\tvar all []WorkItem\n\tfor start := 0; start < len(ids); start += MaxBatchSize {\n\t\tend := start + MaxBatchSize\n\t\tif end > len(ids) {\n\t\t\tend = len(ids)\n\t\t}\n\t\tchunk := ids[start:end]\n\n\t\tparts := make([]string, len(chunk))\n\t\tfor i, id := range chunk {\n\t\t\tparts[i] = strconv.Itoa(id)\n\t\t}\n\n\t\turlStr := addAPIVersion(c.apiBase() + \"/wit/workitems?ids=\" + strings.Join(parts, \",\") + \"&$expand=All\")\n\t\trespBody, err := c.doRequest(ctx, http.MethodGet, urlStr, \"\", nil)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to fetch work items: %w\", err)\n\t\t}\n\n\t\tvar envelope listResponse\n\t\tif err := json.Unmarshal(respBody, &envelope); err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to parse work items response: %w\", err)\n\t\t}\n\n\t\tvar items []WorkItem\n\t\tif err := json.Unmarshal(envelope.Value, &items); err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to parse work items value: %w\", err)\n\t\t}\n\t\tall = append(all, items...)\n\t}\n\n\treturn all, nil\n}\n\n// buildPullWIQL constructs a safe WIQL query from validated filter fields.","sourceCodeStart":335,"sourceCodeEnd":371,"githubUrl":"https://github.com/gastownhall/beads/blob/71377f276968b452ee607177637970a4ff888584/internal/ado/client.go#L335-L371","documentation":"FetchWorkItems wraps any error from the batched GET /wit/workitems?ids=...&$expand=All call (which itself goes through doRequest) with \"failed to fetch work items\". It is a transport/execution wrapper: the cause is either a doRequest failure (rate limits, retries exhausted, auth) or an HTTP error returned by the ADO work-items endpoint. It does NOT cover JSON parsing — that produces \"failed to parse work items response\" instead.","triggerScenarios":"Calling FetchWorkItems (including via fetchWorkItemsByWIQL, which resolves WIQL results into IDs then calls FetchWorkItems) when the batched ids GET fails: retries exhausted on 429/5xx, invalid/nonexistent work-item IDs, insufficient PAT scope, revoked PAT, or network failure.","commonSituations":"WIQL query returns IDs the caller's PAT cannot read (area-path permissions); deleted/recycled work-item IDs in the id list; huge id batches exceeding URL limits; org URL misconfigured in the client base URL.","solutions":["Inspect the wrapped cause (%v of err) for the underlying status/body — APIError carries StatusCode and Body.","Verify the PAT has 'Work Items (Read)' scope and access to the project/area paths of the returned IDs.","Split large ID lists into smaller batches (URL length / API limits) and retry.","Remove or re-resolve stale IDs (deleted work items) before calling; re-run the WIQL query.","Confirm the client's organization/project URL is correct (wrong org yields 404s from the workitems endpoint)."],"exampleFix":"// before\nrespBody, err := c.doRequest(ctx, http.MethodGet, urlStr, \"\", nil)\nif err != nil { return nil, fmt.Errorf(\"failed to fetch work items: %w\", err) }\n// after\ncaller side:\nitems, err := c.FetchWorkItems(ctx, ids)\nif err != nil {\n    var apiErr *ado.APIError\n    if errors.As(err, &apiErr) { log.Printf(\"ADO %d: %s\", apiErr.StatusCode, apiErr.Body) }\n    return nil, err\n}","handlingStrategy":"try-catch","validationCode":"// validate IDs exist / are readable before batch fetch\nfor _, id := range ids {\n    if id <= 0 { return fmt.Errorf(\"invalid work item id %d\", id) }\n}\nif len(ids) > 200 { ids = ids[:200] } // keep URL within limits","typeGuard":null,"tryCatchPattern":"items, err := client.FetchWorkItems(ctx, ids)\nif err != nil {\n    var apiErr *ado.APIError\n    if errors.As(err, &apiErr) {\n        switch apiErr.StatusCode {\n        case 401, 403: // fix PAT / permissions\n        case 400:    // trim id list, re-run WIQL\n        default:     // transient: retry\n        }\n    }\n    return err\n}","preventionTips":["Check PAT scope ('Work Items (Read)') and area-path permissions before batch reads.","Chunk ID lists to a few hundred per request.","Re-run WIQL queries to prune deleted work-item IDs.","Use errors.As with *ado.APIError to branch on status code.","Validate the configured organization/project URL at client startup."],"tags":["azure-devops","http","work-items","error-wrapping"],"backgroundTag":"api-request-failed","analyzedSha":"71377f276968b452ee607177637970a4ff888584","analyzedAt":"2026-08-30T18:55:39.744Z","schemaVersion":2},"datasetVersion":"2026-08-30T23:17:21.991Z"}