gastownhall/beads · error

failed to fetch milestone by IID

Error message

failed to fetch milestone by IID %d: %w

What it means

FetchMilestoneByIID queries /projects/:id/milestones with an iids[] filter via doRequest, which handles auth, retries, and HTTP errors. This error wraps any doRequest failure — network errors, timeouts, context cancellation, or HTTP error statuses (401/403/500) returned by GitLab. It is raised before JSON parsing, so the request never completed successfully.

Solutions

  1. Unwrap the error to read the HTTP status or transport cause (url.Error, context.DeadlineExceeded)
  2. Check token validity and read_api scope; re-authenticate if 401/403
  3. Verify the client's configured project path matches the milestone's project
  4. For timeouts, increase the client timeout or retry; confirm network egress to the GitLab host

Example fix

// before
m, err := client.FetchMilestoneByIID(ctx, iid)
if err != nil { return err }
// after
m, err := client.FetchMilestoneByIID(ctx, iid)
if err != nil {
    if errors.Is(err, context.DeadlineExceeded) {
        return retryWithBackoff(...)
    }
    return fmt.Errorf("milestone %d lookup: %w", iid, err)
}
Defensive patterns

Strategy: retry

Validate before calling

if iid <= 0 {
    return fmt.Errorf("invalid milestone IID %d", iid)
}
if err := ctx.Err(); err != nil {
    return err
}

Type guard

func isAuthError(err error) bool {
    return strings.Contains(err.Error(), "401") || strings.Contains(err.Error(), "403")
}

Try / catch

m, err := client.FetchMilestoneByIID(ctx, iid)
if err != nil {
    if isAuthError(err) {
        return fmt.Errorf("check GITLAB token/scopes: %w", err) // don't retry auth
    }
    if isTransient(err) {
        return fetchMilestoneWithRetry(ctx, iid)
    }
    return err
}

Prevention

When it happens

Trigger: Calling Client.FetchMilestoneByIID with expired/invalid credentials, a misconfigured project path, network outage, or a cancelled context while the filtered milestones query runs.

Common situations: PAT expired or lacking read_api scope; project namespace changed/renamed so the URL 404s; VPN or proxy blocking the GitLab host in CI; context deadline exceeded on slow GitLab instances.

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/96236dcaab5b1ed8. Report an issue: GitHub.

Appendix: source

Thrown at internal/gitlab/client.go:446

	var milestones []Milestone
	if err := json.Unmarshal(respBody, &milestones); err != nil {
		return nil, fmt.Errorf("failed to parse milestones response: %w", err)
	}

	return milestones, nil
}

// FetchMilestoneByIID retrieves a single milestone by its project-scoped IID.
// Returns nil if no milestone matches the given IID.
func (c *Client) FetchMilestoneByIID(ctx context.Context, iid int) (*Milestone, error) {
	params := map[string]string{
		"iids[]": strconv.Itoa(iid),
	}

	urlStr := c.buildURL("/projects/"+c.projectPath()+"/milestones", params)
	respBody, _, err := c.doRequest(ctx, http.MethodGet, urlStr, nil)
	if err != nil {
		return nil, fmt.Errorf("failed to fetch milestone by IID %d: %w", iid, err)
	}

	var milestones []Milestone
	if err := json.Unmarshal(respBody, &milestones); err != nil {
		return nil, fmt.Errorf("failed to parse milestone response: %w", err)
	}

	if len(milestones) == 0 {
		return nil, nil
	}

	return &milestones[0], nil
}

// CreateMilestone creates a new milestone in GitLab.
func (c *Client) CreateMilestone(ctx context.Context, title, description string) (*Milestone, error) {
	body := map[string]interface{}{
		"title":       title,

View on GitHub (pinned to 71377f2769)