gastownhall/beads · error

failed to list projects: %w

Error message

failed to list projects: %w

What it means

This error wraps an HTTP failure from the org-level GET {_org}/_apis/projects endpoint used by ListProjects. It fires before any JSON decoding, meaning the request itself failed (transport error or non-success status). The original error is preserved via %w.

Source

Thrown at internal/ado/client.go:572

			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)
	}

	var projects []Project
	if err := json.Unmarshal(envelope.Value, &projects); err != nil {
		return nil, fmt.Errorf("failed to parse projects value: %w", err)
	}
	return projects, nil
}

// GetWorkItemTypes returns the work item types available in the project.
func (c *Client) GetWorkItemTypes(ctx context.Context) ([]WorkItemType, error) {
	urlStr := addAPIVersion(c.apiBase() + "/wit/workitemtypes")
	respBody, err := c.doRequest(ctx, http.MethodGet, urlStr, "", nil)

View on GitHub (pinned to 71377f2769)

Solutions

  1. Check the wrapped error's status code; 401 means fix the PAT, 404 usually means wrong org URL
  2. Verify the client's organization base URL (orgBase) is correct and reachable
  3. Regenerate the PAT with appropriate scope (project/team read access)
  4. Test the endpoint manually: GET https://dev.azure.com/{org}/_apis/projects?api-version=... with the same PAT
  5. Rule out network/proxy issues by curling the org URL from the same host

Example fix

// before
cfg := ado.Config{OrgURL: "dev.azure.com/myorg"} // missing scheme
// after
cfg := ado.Config{OrgURL: "https://dev.azure.com/myorg"}
projects, err := client.ListProjects(ctx)
Defensive patterns

Strategy: try-catch

Validate before calling

// verify org URL and PAT before calling
if !strings.HasPrefix(orgURL, "https://dev.azure.com/") && !strings.Contains(orgURL, ".visualstudio.com") {
  return fmt.Errorf("suspicious org URL: %s", orgURL)
}
resp, err := http.Get(orgURL + "/_apis/projects?api-version=7.1") // smoke test w/ auth header

Try / catch

projects, err := client.ListProjects(ctx)
if err != nil {
  if strings.Contains(err.Error(), "401") || strings.Contains(err.Error(), "TF") {
    return nil, fmt.Errorf("auth failed listing projects — check PAT scope/expiry: %w", err)
  }
  return nil, err
}

Prevention

When it happens

Trigger: Calling Client.ListProjects(ctx) when doRequest fails: unauthenticated/invalid PAT (401), wrong organization URL in client config (404/DNS failure), network outage, or ADO returning 203/throttling responses treated as failures.

Common situations: Misconfigured organization URL (e.g. missing https:// or wrong org name); PAT without scope for the org; offline/VPN-restricted environments; org renamed or deleted.

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


AI-assisted analysis of gastownhall/beads@71377f2769 (2026-08-30). Data as JSON: /api/errors/c3413493e6252d52. Report an issue: GitHub.