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
- Check the status code: 401/403 means fix or renew the API key (settings.apiKey).
- If 429, respect rate limits — increase refresh interval and cache responses.
- If 404, verify the league/competition path built by the caller.
- 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
- Renew free-tier football-data.org keys before expiry and store them in config, not code.
- Respect the 10 req/min free-tier limit: cache responses and lengthen refresh intervals.
- Validate competition IDs against football-data.org's documented codes.
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.