gastownhall/beads · error
failed to parse milestone response
Error message
failed to parse milestone response: %w
What it means
After FetchMilestoneByIID gets a successful response from /projects/:id/milestones?iids[]=N, the body is unmarshaled into []Milestone. This error wraps the json.Unmarshal failure when the body is not a valid JSON array matching the Milestone struct. Note FetchMilestoneByIID itself returns (nil, nil) for an empty array, so this error is strictly about malformed payloads, not 'not found'.
Solutions
- Log the raw response body and the wrapped json error (SyntaxError vs UnmarshalTypeError) to identify the mismatch
- Verify the base URL targets the GitLab API host, not a web UI or SSO redirect
- Update the Milestone struct json tags/types to match your GitLab version's actual payload
- Retry if a transient gateway error could have truncated the body
Example fix
// before
m, err := client.FetchMilestoneByIID(ctx, iid)
// after
m, err := client.FetchMilestoneByIID(ctx, iid)
if err != nil {
var se *json.SyntaxError
if errors.As(err, &se) {
log.Printf("non-JSON milestone payload (offset %d): likely HTML from proxy", se.Offset)
}
return err
} Defensive patterns
Strategy: type-guard
Validate before calling
// Validate API host configuration to avoid proxy-injected HTML
if u, err := url.Parse(gitlabHost); err != nil || u.Scheme == "" || u.Host == "" {
return fmt.Errorf("misconfigured gitlab host: %q", gitlabHost)
} Type guard
func isPayloadError(err error) bool {
var se *json.SyntaxError
var te *json.UnmarshalTypeError
return errors.As(err, &se) || errors.As(err, &te)
}
// Also narrow the nil result:
func milestoneFound(m *Milestone) bool { return m != nil && m.ID != 0 } Try / catch
m, err := client.FetchMilestoneByIID(ctx, iid)
if err != nil {
if isPayloadError(err) {
return fmt.Errorf("malformed milestone payload: %w", err)
}
return err
}
if !milestoneFound(m) {
return nil // legitimately not found — function returns nil,nil
} Prevention
- Remember FetchMilestoneByIID returns (nil, nil) when not found — always nil-check the result
- Keep the Milestone struct in sync with your GitLab version
- Bypass proxies/SSO for API traffic to avoid HTML responses
- Log raw bodies on parse errors to diagnose gateway truncation
When it happens
Trigger: Calling Client.FetchMilestoneByIID when a proxy/SSO injects HTML into the response, the body is empty/truncated, or a GitLab version emits milestone field types that conflict with the Milestone struct (e.g. number-vs-string IDs).
Common situations: Corporate proxies or error pages returned with 200 responses; self-managed GitLab versions with divergent milestone schemas; wrong base URL hitting a non-API route; truncated responses from flaky gateways.
Understand the failure class
- Parsing and encoding errors: unexpected token, malformed input — why parsers reject input and how to find the real culprit.
Related errors
- failed to parse milestones response
- failed to parse issue links response
- failed to parse issue response
- failed to create milestone
- failed to fetch milestone by IID
AI-assisted analysis of gastownhall/beads@71377f2769 (2026-08-30).
Data as JSON: /api/errors/17de3e3727c681f7.
Report an issue: GitHub.
Appendix: source
Thrown at internal/gitlab/client.go:451
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,
"description": description,
}
urlStr := c.buildURL("/projects/"+c.projectPath()+"/milestones", nil)
respBody, _, err := c.doRequest(ctx, http.MethodPost, urlStr, body)View on GitHub (pinned to 71377f2769)