{"record":{"id":"76cf9f89a03f2673","repo":"tailscale/tailscale","slug":"wanted-http-status-code-d-but-got-d","errorCode":null,"errorMessage":"wanted HTTP status code %d but got %d","messagePattern":"wanted HTTP status code (.+?) but got (.+?)","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"cmd/gitops-pusher/gitops-pusher.go","lineNumber":372,"sourceCode":"\tif err != nil {\n\t\treturn err\n\t}\n\tdefer resp.Body.Close()\n\n\tvar ate ACLGitopsTestError\n\terr = json.NewDecoder(resp.Body).Decode(&ate)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tif len(ate.Message) != 0 || len(ate.Data) != 0 {\n\t\treturn ate\n\t}\n\n\tgot := resp.StatusCode\n\twant := http.StatusOK\n\tif got != want {\n\t\treturn fmt.Errorf(\"wanted HTTP status code %d but got %d\", want, got)\n\t}\n\n\treturn nil\n}\n\nvar lineColMessageSplit = regexp.MustCompile(`line ([0-9]+), column ([0-9]+): (.*)$`)\n\n// ACLGitopsTestError is redefined here so we can add a custom .Error() response\ntype ACLGitopsTestError struct {\n\ttsclient.ACLTestError\n}\n\nfunc (ate ACLGitopsTestError) Error() string {\n\tvar sb strings.Builder\n\n\tif *githubSyntax && lineColMessageSplit.MatchString(ate.Message) {\n\t\tsp := lineColMessageSplit.FindStringSubmatch(ate.Message)\n","sourceCodeStart":354,"sourceCodeEnd":390,"githubUrl":"https://github.com/tailscale/tailscale/blob/cfe32b8be6a33f8e24fbc369cbfbf7c729d9e042/cmd/gitops-pusher/gitops-pusher.go#L354-L390","documentation":"In testNewACLs, gitops-pusher POSTs the policy to https://<api-server>/api/v2/tailnet/<tailnet>/acl/validate. If the response body carries no ACLTestError message/data but the HTTP status is not 200, this generic mismatch error is returned (want is hardcoded to http.StatusOK). Typical statuses: 401/403 from a bad API key, 5xx during control outages, or non-200 responses whose bodies decode to an empty error object.","triggerScenarios":"Calling the test/apply flow with an invalid or expired API key, wrong --api-server, a tailnet name that does not exist, or hitting a control-plane 5xx whose body decodes to an empty JSON object (no 'message'/'data' fields), falling through to the status-code check.","commonSituations":"Expired ts-api-key in CI secrets; typo in --org/--tailnet; api.tailscale.com incident; proxy returning an HTML error page that fails JSON decode earlier (that surfaces as the json error instead).","solutions":["Print/inspect resp.StatusCode before the check (or capture the body) to identify 401/403/404/500.","Verify the API key is valid and has ACL permissions for the tailnet.","Check the tailnet name and --api-server spelling.","Retry transient 5xx; check status.tailscale.com for control incidents."],"exampleFix":"// before: opaque mismatch error\nif got != want {\n    return fmt.Errorf(\"wanted HTTP status code %d but got %d\", want, got)\n}\n\n// after: include the status text and a body snippet for diagnosis\nbody, _ := io.ReadAll(io.LimitReader(resp.Body, 512))\nif got != want {\n    return fmt.Errorf(\"wanted HTTP status code %d but got %d (%s): %s\", want, got, http.StatusText(got), body)\n}","handlingStrategy":"try-catch","validationCode":"// Pre-validate credentials before pushing policy.\nfunc apiKeyWorks(server, key string) bool {\n    req, _ := http.NewRequest(\"GET\", \"https://\"+server+\"/api/v2/tailnet/-/acl\", nil)\n    req.SetBasicAuth(key, \"\")\n    resp, err := http.DefaultClient.Do(req)\n    if err != nil { return false }\n    resp.Body.Close()\n    return resp.StatusCode != 401 && resp.StatusCode != 403\n}","typeGuard":"func isStatusMismatch(err error) bool {\n    return err != nil && strings.Contains(err.Error(), \"wanted HTTP status code\")\n}","tryCatchPattern":"err := testNewACLs(ctx, tailnet, *policyFname)\nif isStatusMismatch(err) {\n    // 401/403 => credentials; 5xx => retry with backoff; re-run after fixing keys\n}","preventionTips":["Rotate API keys before expiry; store them in CI secret managers, not the repo.","Fail fast on 401/403 (never retry auth errors).","Retry only 5xx with exponential backoff and jitter.","Log resp.StatusCode plus body snippet when wrapping this call."],"tags":["go","api","http","acl","tailscale","ci"],"backgroundTag":null,"analyzedSha":"cfe32b8be6a33f8e24fbc369cbfbf7c729d9e042","analyzedAt":"2026-08-15T19:58:31.583Z","schemaVersion":2},"datasetVersion":"2026-08-16T03:17:38.424Z"}