{"record":{"id":"7f9fbd768ebb84fd","repo":"chenhg5/cc-connect","slug":"webex-unauthorized-401-check-bot-token","errorCode":null,"errorMessage":"webex: unauthorized (401) — check bot token","messagePattern":"webex: unauthorized \\(401\\) — check bot token","errorType":"exception","errorClass":null,"httpStatus":401,"severity":"critical","filePath":"platform/webex/client.go","lineNumber":24,"sourceCode":"\t\"encoding/json\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"mime\"\n\t\"mime/multipart\"\n\t\"net/http\"\n\t\"strconv\"\n\t\"strings\"\n\t\"time\"\n)\n\nconst webexBaseURL = \"https://webexapis.com/v1\"\n\n// maxRetryAfter caps how long we honor a 429 Retry-After header before retrying.\nconst maxRetryAfter = 60 * time.Second\n\n// errUnauthorized signals a 401 from the Webex API so callers can stop retrying.\nvar errUnauthorized = errors.New(\"webex: unauthorized (401) — check bot token\")\n\n// webexClient abstracts the Webex REST API so tests can stub it.\ntype webexClient interface {\n\tGetMe(ctx context.Context) (*person, error)\n\tCreateDevice(ctx context.Context) (*device, error)\n\tDeleteDevice(ctx context.Context, deviceURL string) error\n\tGetMessage(ctx context.Context, id string) (*message, error)\n\tDownloadFile(ctx context.Context, url string) (*downloadedFile, error)\n\tPostMessage(ctx context.Context, roomID, parentID, markdown string) error\n\tPostFile(ctx context.Context, roomID string, f *downloadedFile) error\n}\n\n// httpClient is the real webexClient backed by net/http.\ntype httpClient struct {\n\ttoken   string\n\thc      *http.Client\n\tbaseURL string // Webex REST base; overridable in tests.\n}","sourceCodeStart":6,"sourceCodeEnd":42,"githubUrl":"https://github.com/chenhg5/cc-connect/blob/4000b2338aa6e850c99df54f8b0ed6ed7460b401/platform/webex/client.go#L6-L42","documentation":"errUnauthorized is a sentinel error declared in platform/webex/client.go. The Webex REST client wraps it with the failing operation name when webexapis.com returns HTTP 401, so callers can detect auth failure via errors.Is and stop retrying instead of hammering the API. It always indicates the bot token was rejected.","triggerScenarios":"Any API call routed through the client's do/request helpers (GetMe, CreateDevice, message fetches) that receives HTTP 401; also propagated by TestGetMessageUnauthorized and connectLoop when the initial connection or subsequent requests are rejected with 401.","commonSituations":"Expired or revoked Webex bot access token in config.toml; token copied with whitespace or from the wrong bot; environment where the token was rotated after startup; wrong integration using a user token instead of a bot token.","solutions":["Regenerate the Webex bot access token in the Webex Developer portal and update it in config.toml, then restart cc-connect.","Verify the token with curl: `curl -H 'Authorization: Bearer <token>' https://webexapis.com/v1/people/me` — expect 200.","Check for trailing whitespace/newlines or env-var interpolation mistakes in the configured token.","Ensure you are using a bot token (not an OAuth user token) with the required scopes."],"exampleFix":"// before (config.toml)\n[platforms.webex]\naccess_token = \"OLD_EXPIRED_TOKEN\"\n// after\n[platforms.webex]\naccess_token = \"Y2x...newly-generated-bot-token\"","handlingStrategy":"try-catch","validationCode":"// before starting cc-connect, verify the bot token:\ntok := cfg.WebexAccessToken\nreq, _ := http.NewRequest(\"GET\", \"https://webexapis.com/v1/people/me\", nil)\nreq.Header.Set(\"Authorization\", \"Bearer \"+tok)\nresp, err := http.DefaultClient.Do(req)\nif err != nil || resp.StatusCode != 200 {\n    return fmt.Errorf(\"webex bot token invalid (status %v)\", resp.StatusCode)\n}","typeGuard":null,"tryCatchPattern":"if errors.Is(err, webex.ErrUnauthorized) {\n    // non-retryable: alert and stop, don't loop\n    slog.Error(\"webex auth rejected; token must be refreshed\", \"err\", err)\n    return\n}","preventionTips":["Validate the token with GET /people/me at startup and on 401","Track token expiry; Webex bot tokens can be revoked/rotated — redeploy config after rotation","Trim whitespace/newlines when copying tokens into config","Use a bot token, not a user OAuth token, and store it via env/secret manager"],"tags":["webex","authentication","http-401","bot-token"],"backgroundTag":"authentication-required","analyzedSha":"4000b2338aa6e850c99df54f8b0ed6ed7460b401","analyzedAt":"2026-09-06T11:45:09.575Z","contentChangedAt":"2026-09-06T11:45:09.575Z","schemaVersion":2},"datasetVersion":"2026-09-14T05:17:10.506Z"}