{"record":{"id":"a4fe254f2327dde6","repo":"multica-ai/multica","slug":"vcs-token-unauthorized","errorCode":null,"errorMessage":"vcs: token unauthorized","messagePattern":"vcs: token unauthorized","errorType":"exception","errorClass":null,"httpStatus":400,"severity":"error","filePath":"server/internal/integrations/vcs/vcs.go","lineNumber":42,"sourceCode":"const (\n\tKindForgejo Kind = \"forgejo\"\n\tKindGitea   Kind = \"gitea\"\n\tKindGitLab  Kind = \"gitlab\"\n)\n\n// Valid reports whether k is a known provider kind.\nfunc (k Kind) Valid() bool {\n\tswitch k {\n\tcase KindForgejo, KindGitea, KindGitLab:\n\t\treturn true\n\t}\n\treturn false\n}\n\n// ErrUnauthorized is returned by ValidateToken when the instance rejects the\n// token (HTTP 401/403). Callers surface it as a connect-time validation\n// failure distinct from transport/instance errors.\nvar ErrUnauthorized = errors.New(\"vcs: token unauthorized\")\n\n// EventKind is the normalized webhook event category. Anything a provider does\n// not model maps to EventOther and is acknowledged but ignored.\ntype EventKind int\n\nconst (\n\tEventOther EventKind = iota\n\tEventPullRequest\n\tEventCIStatus\n)\n\n// PullRequestEvent is the provider-agnostic shape of a pull/merge request\n// webhook. State is already normalized to one of open/closed/merged/draft, so\n// the handler never re-derives it. GitLab \"merge requests\" map onto the same\n// struct.\ntype PullRequestEvent struct {\n\t// Action is the raw provider action (e.g. \"opened\", \"closed\", \"merge\").\n\t// The handler only needs to know whether it is terminal; see Terminal.","sourceCodeStart":24,"sourceCodeEnd":60,"githubUrl":"https://github.com/multica-ai/multica/blob/2c0912b6ec764b373d44eeea1e80f0d9f11ab417/server/internal/integrations/vcs/vcs.go#L24-L60","documentation":"VCS integration sentinel error: ValidateToken asked the Forgejo/Gitea/GitLab instance about a token and the instance answered HTTP 401/403 — the token is not accepted. Callers surface it as a connect-time validation failure, distinct from transport/instance errors.","triggerScenarios":"Calling vcs.ValidateToken with a revoked, expired, mistyped, or insufficient-scope PAT against a Forgejo, Gitea, or GitLab instance. Only 401/403 responses map to this error; network failures and 5xx do not.","commonSituations":"Personal access token rotated or expired since it was saved; token pasted with whitespace or truncated; token lacks the scopes needed by the endpoint being probed (read:user, api, etc.); SSO/session enforcement on GitLab rejecting plain PATs.","solutions":["Generate a fresh PAT in the provider UI with the required scopes and re-enter it.","Verify the token manually: curl -H \"Authorization: Bearer <token>\" https://instance/api/v1/user (or /api/v4/user for GitLab) — a 401/403 confirms rejection.","Check token expiration policy (GitLab instance-level expiry limits) and IP allowlists on the instance."],"exampleFix":"// before\nif err := vcs.ValidateToken(ctx, kind, baseURL, token); err != nil {\n\tlog.Printf(\"vcs connect failed: %v\", err) // conflates auth with network\n}\n\n// after\nerr := vcs.ValidateToken(ctx, kind, baseURL, token)\nswitch {\ncase errors.Is(err, vcs.ErrUnauthorized):\n\trespond(w, 400, \"token rejected by the instance — check value, scopes, and expiry\")\ncase err != nil:\n\trespond(w, 502, \"could not reach the instance to validate the token\")\n}","handlingStrategy":"try-catch","validationCode":"// cheap pre-flight: does the token fetch the authenticated user?\nreq, _ := http.NewRequest(\"GET\", instanceURL+\"/api/v1/user\", nil)\nreq.Header.Set(\"Authorization\", \"Bearer \"+token)\nresp, err := http.DefaultClient.Do(req)\nif err == nil && (resp.StatusCode == 401 || resp.StatusCode == 403) {\n\treturn errors.New(\"token rejected — fix before connecting\")\n}","typeGuard":null,"tryCatchPattern":"err := vcs.ValidateToken(ctx, kind, baseURL, token)\nif err != nil {\n\tif errors.Is(err, vcs.ErrUnauthorized) {\n\t\t// credential problem: ask for a new token, do not retry\n\t\treturn respondTokenRejected(w)\n\t}\n\t// everything else is transport/instance — safe to retry with backoff\n\treturn retryableError(err)\n}","preventionTips":["Store token creation/expiry metadata and warn before provider-side expiry.","Distinguish 401/403 (this error) from network errors in dashboards — only the latter merits retries.","Scope PATs minimally and re-validate at connect time rather than trusting stored values."],"tags":["vcs","authentication","token","gitlab","forgejo","gitea","go"],"backgroundTag":null,"analyzedSha":"2c0912b6ec764b373d44eeea1e80f0d9f11ab417","analyzedAt":"2026-08-15T13:25:18.241Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}