{"record":{"id":"3b513b24a5239e9e","repo":"gastownhall/beads","slug":"oauth-token-request-failed-s-s","errorCode":null,"errorMessage":"oauth: token request failed (%s): %s","messagePattern":"oauth: token request failed \\((.+?)\\): (.+?)","errorType":"http","errorClass":null,"httpStatus":null,"severity":"error","filePath":"internal/linear/oauth.go","lineNumber":143,"sourceCode":"\t\treturn fmt.Errorf(\"oauth: failed to create token request: %w\", err)\n\t}\n\treq.Header.Set(\"Content-Type\", \"application/x-www-form-urlencoded\")\n\n\tresp, err := m.client.Do(req)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"oauth: token request failed: %w\", err)\n\t}\n\tdefer func() { _ = resp.Body.Close() }()\n\n\tbody, err := io.ReadAll(io.LimitReader(resp.Body, 1<<20)) // 1MB limit\n\tif err != nil {\n\t\treturn fmt.Errorf(\"oauth: failed to read token response: %w\", err)\n\t}\n\n\tif resp.StatusCode != http.StatusOK {\n\t\tvar errResp oauthErrorResponse\n\t\tif json.Unmarshal(body, &errResp) == nil && errResp.Error != \"\" {\n\t\t\treturn fmt.Errorf(\"oauth: token request failed (%s): %s\", errResp.Error, errResp.Description)\n\t\t}\n\t\treturn fmt.Errorf(\"oauth: token request returned status %d: %s\", resp.StatusCode, string(body))\n\t}\n\n\tvar tokenResp oauthTokenResponse\n\tif err := json.Unmarshal(body, &tokenResp); err != nil {\n\t\treturn fmt.Errorf(\"oauth: failed to parse token response: %w\", err)\n\t}\n\n\tif tokenResp.AccessToken == \"\" {\n\t\treturn fmt.Errorf(\"oauth: token response missing access_token\")\n\t}\n\n\tm.token = tokenResp.AccessToken\n\tm.expiresAt = m.nowFunc().Add(time.Duration(tokenResp.ExpiresIn) * time.Second)\n\n\tdebug.Logf(\"oauth: acquired token (expires in %ds)\", tokenResp.ExpiresIn)\n\treturn nil","sourceCodeStart":125,"sourceCodeEnd":161,"githubUrl":"https://github.com/gastownhall/beads/blob/71377f276968b452ee607177637970a4ff888584/internal/linear/oauth.go#L125-L161","documentation":"The token endpoint returned a non-200 status and its JSON body contained an OAuth error field (RFC 6749 error responses such as invalid_client or invalid_grant). The server explicitly rejected the token request.","triggerScenarios":"resp.StatusCode != 200 and json.Unmarshal into oauthErrorResponse succeeds with a non-empty Error field during acquireToken.","commonSituations":"Wrong client_id/client_secret after a credential rotation; invalid scopes requested; expired or revoked authorization; grant_type mismatch with what the server supports.","solutions":["Read errResp.Error/Description in the wrapped message and fix the matching OAuth parameter.","Re-verify client_id and client_secret against the provider (most common: invalid_client after rotation).","Trim the requested scopes to those actually granted to the integration.","If credentials live in env/secrets, confirm the deployment is using the refreshed values."],"exampleFix":"// before\n\"client_secret\": {m.config.ClientSecret} // stale after rotation\n// after\n\"client_secret\": {secretFromSecretManager}\" // refreshed credential\n","handlingStrategy":"validation","validationCode":"if cfg.ClientID == \"\" || cfg.ClientSecret == \"\" {\n    return fmt.Errorf(\"oauth client credentials missing\")\n}","typeGuard":null,"tryCatchPattern":"var oauthErr *OAuthError // parse errResp.Error/Description from message\nif strings.Contains(err.Error(), \"invalid_client\") {\n    // rotate credentials and reload config\n}","preventionTips":["Rotate and verify client credentials before deployments.","Keep requested scopes minimal and matching the provider's granted scopes.","Alert on invalid_client/invalid_grant — they mean config drift, not transient faults."],"tags":["oauth","invalid-client","authentication","go"],"backgroundTag":"oauth-invalid-client","analyzedSha":"71377f276968b452ee607177637970a4ff888584","analyzedAt":"2026-08-30T18:55:39.744Z","schemaVersion":2},"datasetVersion":"2026-08-30T23:17:21.991Z"}