{"record":{"id":"142ad0721196fa0f","repo":"wtfutil/wtf","slug":"unexpected-status-code-d-from-football-api","errorCode":null,"errorMessage":"unexpected status code %d from football API","messagePattern":"unexpected status code (.+?) from football API","errorType":"http","errorClass":null,"httpStatus":null,"severity":"error","filePath":"modules/football/client.go","lineNumber":47,"sourceCode":"func (client *Client) footballRequest(path string, id int) (*http.Response, error) {\n\n\turl := fmt.Sprintf(\"%s/competitions/%d/%s\", footballAPIUrl, id, path)\n\treq, err := http.NewRequest(\"GET\", url, http.NoBody)\n\treq.Header.Add(\"Accept\", \"application/json\")\n\treq.Header.Add(\"Content-Type\", \"application/json\")\n\treq.Header.Add(\"X-Auth-Token\", client.apiKey)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\thttpClient := &http.Client{}\n\tresp, err := httpClient.Do(req)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tif resp.StatusCode < 200 || resp.StatusCode >= 300 {\n\t\tdefer func() { _ = resp.Body.Close() }()\n\t\treturn nil, fmt.Errorf(\"unexpected status code %d from football API\", resp.StatusCode)\n\t}\n\n\treturn resp, nil\n}\n","sourceCodeStart":29,"sourceCodeEnd":52,"githubUrl":"https://github.com/wtfutil/wtf/blob/bb838c1ccb0f0f3223690df44afdec663d622881/modules/football/client.go#L29-L52","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":"// before\nreturn nil, fmt.Errorf(\"unexpected status code %d from football API\", resp.StatusCode)\n// after\nbody, _ := io.ReadAll(io.LimitReader(resp.Body, 512))\nreturn nil, fmt.Errorf(\"unexpected status code %d from football API: %s\", resp.StatusCode, body)","handlingStrategy":"retry","validationCode":"if settings.apiKey == \"\" {\n\treturn fmt.Errorf(\"football API key missing — set apiKey in widget settings\")\n}","typeGuard":"func isRetryableStatus(code int) bool {\n\treturn code == http.StatusTooManyRequests || code >= 500\n}","tryCatchPattern":"resp, err := footballRequest(req)\nif err != nil {\n\tif isRetryableStatus(statusFromErr(err)) {\n\t\ttime.Sleep(30 * time.Second)\n\t\tresp, err = footballRequest(req) // bounded retry for 429/5xx\n\t}\n\tif err != nil {\n\t\tlog.Printf(\"football api: %v\", err)\n\t\treturn\n\t}\n}","preventionTips":["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."],"tags":["http","api","api-key","go"],"backgroundTag":"http-non-200-response","analyzedSha":"bb838c1ccb0f0f3223690df44afdec663d622881","analyzedAt":"2026-09-03T17:02:45.030Z","contentChangedAt":"2026-09-03T17:02:45.030Z","schemaVersion":2},"datasetVersion":"2026-09-11T00:17:11.886Z"}