wtfutil/wtf · error

unexpected status code %d from football API

Error message

unexpected status code %d from football API

What it means

footballRequest treats any status outside 200-299 from the football API as fatal and returns "unexpected status code %d from football API", closing the body first. The wrapper deliberately omits the body, so the status code (401 invalid API key, 403, 429, 5xx) is the only clue. All higher-level calls (GetStandings, GetMatches) surface this error.

Source

Thrown at modules/football/client.go:47

func (client *Client) footballRequest(path string, id int) (*http.Response, error) {

	url := fmt.Sprintf("%s/competitions/%d/%s", footballAPIUrl, id, path)
	req, err := http.NewRequest("GET", url, http.NoBody)
	req.Header.Add("Accept", "application/json")
	req.Header.Add("Content-Type", "application/json")
	req.Header.Add("X-Auth-Token", client.apiKey)
	if err != nil {
		return nil, err
	}
	httpClient := &http.Client{}
	resp, err := httpClient.Do(req)
	if err != nil {
		return nil, err
	}

	if resp.StatusCode < 200 || resp.StatusCode >= 300 {
		defer func() { _ = resp.Body.Close() }()
		return nil, fmt.Errorf("unexpected status code %d from football API", resp.StatusCode)
	}

	return resp, nil
}

View on GitHub (pinned to bb838c1ccb)

Solutions

  1. Check the status code: 401/403 means fix or renew the API key (settings.apiKey).
  2. If 429, respect rate limits — increase refresh interval and cache responses.
  3. If 404, verify the league/competition path built by the caller.
  4. Retry on 5xx; the request itself is likely correct.

Example fix

// before
return nil, fmt.Errorf("unexpected status code %d from football API", resp.StatusCode)
// after
body, _ := io.ReadAll(io.LimitReader(resp.Body, 512))
return nil, fmt.Errorf("unexpected status code %d from football API: %s", resp.StatusCode, body)
Defensive patterns

Strategy: retry

Validate before calling

if settings.apiKey == "" {
	return fmt.Errorf("football API key missing — set apiKey in widget settings")
}

Type guard

func isRetryableStatus(code int) bool {
	return code == http.StatusTooManyRequests || code >= 500
}

Try / catch

resp, err := footballRequest(req)
if err != nil {
	if isRetryableStatus(statusFromErr(err)) {
		time.Sleep(30 * time.Second)
		resp, err = footballRequest(req) // bounded retry for 429/5xx
	}
	if err != nil {
		log.Printf("football api: %v", err)
		return
	}
}

Prevention

When it happens

Trigger: Any non-2xx from api.football-data.org (or configured base URL): missing/expired X-Auth-Token (400/403), exceeded free-tier rate limit (429), unknown resource path, or upstream 5xx.

Common situations: Free football-data.org key expired or rate-limited (10 calls/minute), wrong competition id in config producing 404s, key not yet activated, or network middleware returning errors.

Related errors


AI-assisted analysis of wtfutil/wtf@bb838c1ccb (2026-09-03). Data as JSON: /api/errors/142ad0721196fa0f. Report an issue: GitHub.