gastownhall/beads · error
failed to remove work item link: %w
Error message
failed to remove work item link: %w
What it means
This error wraps any failure from the HTTP PATCH that deletes a relation (JSON-Patch 'remove /relations/{index}') from a work item in RemoveWorkItemLink. Like its sibling add-link error, it wraps the original doRequest error with %w so status codes and ADO messages remain inspectable. It marks the failure at the 'remove link' call boundary.
Source
Thrown at internal/ado/client.go:561
_, err := c.doRequest(ctx, http.MethodPatch, urlStr, "application/json-patch+json", ops)
if err != nil {
return fmt.Errorf("failed to add work item link: %w", err)
}
return nil
}
// RemoveWorkItemLink removes a relation link by index from the given work item.
func (c *Client) RemoveWorkItemLink(ctx context.Context, sourceID, relationIndex int) error {
ops := []PatchOperation{
{
Op: "remove",
Path: fmt.Sprintf("/relations/%d", relationIndex),
},
}
urlStr := addAPIVersion(fmt.Sprintf("%s/wit/workitems/%d", c.apiBase(), sourceID))
_, err := c.doRequest(ctx, http.MethodPatch, urlStr, "application/json-patch+json", ops)
if err != nil {
return fmt.Errorf("failed to remove work item link: %w", err)
}
return nil
}
// ListProjects returns all team projects in the organization.
// This is an org-level endpoint, not project-scoped.
func (c *Client) ListProjects(ctx context.Context) ([]Project, error) {
urlStr := addAPIVersion(c.orgBase() + "/projects")
respBody, err := c.doRequest(ctx, http.MethodGet, urlStr, "", nil)
if err != nil {
return nil, fmt.Errorf("failed to list projects: %w", err)
}
var envelope listResponse
if err := json.Unmarshal(respBody, &envelope); err != nil {
return nil, fmt.Errorf("failed to parse projects response: %w", err)
}
View on GitHub (pinned to 71377f2769)
Solutions
- Re-fetch the work item's current relations and recompute relationIndex before retrying (indices shift on concurrent changes)
- Check the wrapped cause (errors.As) for ADO's status code — 404 usually means stale index or wrong ID
- Verify the PAT has Work Items write scope and has not expired
- Confirm sourceID exists in the configured organization/project
- Retry transient network/5xx failures; ADO occasionally throttles writes (429)
Example fix
// before
item, _ := client.GetWorkItem(ctx, id)
idx := findRel(item, relURL)
client.RemoveWorkItemLink(ctx, id, idx) // idx may be stale later
// after
item, _ := client.GetWorkItem(ctx, id)
idx := findRel(item, relURL)
if idx < 0 {
return nil // nothing to remove
}
if err := client.RemoveWorkItemLink(ctx, id, idx); err != nil {
return fmt.Errorf("remove link from %d (idx %d): %w", id, idx, err)
} Defensive patterns
Strategy: validation
Validate before calling
// validate the index against freshly fetched relations immediately before removal
wi, err := client.GetWorkItem(ctx, sourceID)
if err != nil {
return err
}
if relationIndex < 0 || relationIndex >= len(wi.Relations) {
return fmt.Errorf("relation index %d out of range (have %d)", relationIndex, len(wi.Relations))
} Type guard
func validRelationIndex(n, total int) bool { return n >= 0 && n < total } Try / catch
if err := client.RemoveWorkItemLink(ctx, sourceID, idx); err != nil {
if strings.Contains(err.Error(), "404") {
// stale index: refetch relations and recompute
return retryWithFreshIndex(ctx, sourceID, relURL)
}
return err
} Prevention
- Re-fetch relations and compute the index immediately before removing
- Remove links one at a time rather than batching index-based removals (indices shift)
- Identify relations by URL/id, not by position, wherever possible
- Handle 404 as 'already removed' in idempotent workflows
When it happens
Trigger: Calling Client.RemoveWorkItemLink(ctx, sourceID, relationIndex) when doRequest fails: 404 because sourceID or the relation index no longer exists (relations shift when other links are removed), 401 invalid PAT, network error, or 400 for a negative/out-of-range index.
Common situations: Removing a link by a stale index captured before another process modified relations; concurrent edits to the work item causing index drift; expired PAT after long-running jobs; typo in the work item ID.
Understand the failure class
Background: 'Something went wrong' / 'Request failed (500)' / 'HTTP error! status: 404' — what failed HTTP requests actually mean and how to find the real cause — this error's family across 28 libraries.
Related errors
- failed to fetch work items: %w
- failed to add work item link: %w
- failed to list projects: %w
- transient error %d (attempt %d/%d)
- max retries (%d) exceeded: %w
AI-assisted analysis of gastownhall/beads@71377f2769 (2026-08-30).
Data as JSON: /api/errors/6d77b03b82957b6e.
Report an issue: GitHub.