{"record":{"id":"cc025193d7304d7b","repo":"juanfont/headscale","slug":"api-error-d-s","errorCode":null,"errorMessage":"api error (%d): %s","messagePattern":"api error \\((.+?)\\): (.+?)","errorType":"http","errorClass":null,"httpStatus":null,"severity":"error","filePath":"cmd/headscale/cli/oauth_client.go","lineNumber":250,"sourceCode":"\n\treturn ctx, client, cancel, nil\n}\n\n// v2Error turns a non-2xx v2 response into an error. The v2 API emits the\n// Tailscale error body ({\"message\":...}) rather than RFC 7807, so it reads the\n// \"message\" field instead of the generated problem+json types.\nfunc v2Error(status int, body []byte) error {\n\tif status >= http.StatusOK && status < http.StatusMultipleChoices {\n\t\treturn nil\n\t}\n\n\tvar e struct {\n\t\tMessage string `json:\"message\"`\n\t}\n\n\tif json.Unmarshal(body, &e) == nil && e.Message != \"\" {\n\t\t//nolint:err113 // surfacing the server's message\n\t\treturn fmt.Errorf(\"api error (%d): %s\", status, e.Message)\n\t}\n\n\t//nolint:err113 // surfacing the server's body\n\treturn fmt.Errorf(\"api error (%d): %s\", status, strings.TrimSpace(string(body)))\n}\n\nfunc ptrStr(s *string) string {\n\tif s == nil {\n\t\treturn \"\"\n\t}\n\n\treturn *s\n}\n\nfunc ptrStrs(s *[]string) []string {\n\tif s == nil {\n\t\treturn nil\n\t}","sourceCodeStart":232,"sourceCodeEnd":268,"githubUrl":"https://github.com/juanfont/headscale/blob/565fd254d06c4c7f9a8cad1714a43445c79ba420/cmd/headscale/cli/oauth_client.go#L232-L268","documentation":"Returned by the headscale CLI's v2 API error decoder when the OAuth client endpoint answers with a non-2xx status whose body is JSON containing a \"message\" field (the Tailscale-style error body, not RFC 7807 problem+json). The CLI formats the HTTP status code and the server's message into one error string. It is a plain fmt.Errorf, so the status code is only available by parsing the text.","triggerScenarios":"Any call through the v2 client (OAuth 2.0 client management, e.g. creating a client with a redirect URI the server rejects) that returns 4xx/5xx with a body like {\"message\":\"invalid redirect_uri\"}. Typical statuses: 400 validation failure, 401 bad/expired API key, 404 unknown id, 500 server-side failure.","commonSituations":"API key created for a different server or revoked; clock skew breaking token validity; sending a field combination the v2 schema rejects; server version older/newer than the CLI's generated client, so the endpoint validates differently.","solutions":["Read the status number in parentheses: 401/403 means fix the API key (headscale apikeys create), 400 means fix the request payload, 404 means the referenced id does not exist.","Reproduce with curl against the same v2 endpoint with the same bearer token to see the raw body.","If 401 persists, verify the server address and key in the CLI configuration (headscale config view / HEAADSCALE_ env vars).","For 5xx codes, check the headscale server logs for the corresponding request failure."],"exampleFix":"// before: creating an OAuth client with an empty redirect URI\nclient.CreateOAuthClientWithResponse(ctx, body)\n\n// after: validate before sending\nif len(redirectURIs) == 0 {\n    return fmt.Errorf(\"at least one redirect URI is required\")\n}","handlingStrategy":"try-catch","validationCode":null,"typeGuard":"// matches \"api error (401): ...\" from the v2 client\nfunc isV2APIError(err error) bool {\n\treturn err != nil && strings.HasPrefix(err.Error(), \"api error (\")\n}\n\nfunc v2Status(err error) int {\n\tvar n int\n\tif _, e := fmt.Sscanf(err.Error(), \"api error (%d)\", &n); e == nil {\n\t\treturn n\n\t}\n\treturn 0\n}","tryCatchPattern":"err := createOAuthClient(...)\nif err != nil {\n    if isV2APIError(err) {\n        switch v2Status(err) {\n        case 401, 403:\n            // refresh/recreate the API key, then retry once\n        case 400:\n            // fix request payload; do not retry\n        default:\n            return err\n        }\n    }\n    return err\n}","preventionTips":["Rotate API keys on a schedule and store them in a secret manager, not in scripts.","Validate request fields (redirect URIs, names) client-side before calling the v2 API.","Keep the CLI version aligned with the server version so generated schemas match."],"tags":["api","http","cli","oauth"],"backgroundTag":null,"analyzedSha":"565fd254d06c4c7f9a8cad1714a43445c79ba420","analyzedAt":"2026-08-15T13:12:30.133Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}