multica-ai/multica · error

vcs: token unauthorized

Error message

vcs: token unauthorized

What it means

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.

Source

Thrown at server/internal/integrations/vcs/vcs.go:42

const (
	KindForgejo Kind = "forgejo"
	KindGitea   Kind = "gitea"
	KindGitLab  Kind = "gitlab"
)

// Valid reports whether k is a known provider kind.
func (k Kind) Valid() bool {
	switch k {
	case KindForgejo, KindGitea, KindGitLab:
		return true
	}
	return false
}

// ErrUnauthorized is returned by ValidateToken when the instance rejects the
// token (HTTP 401/403). Callers surface it as a connect-time validation
// failure distinct from transport/instance errors.
var ErrUnauthorized = errors.New("vcs: token unauthorized")

// EventKind is the normalized webhook event category. Anything a provider does
// not model maps to EventOther and is acknowledged but ignored.
type EventKind int

const (
	EventOther EventKind = iota
	EventPullRequest
	EventCIStatus
)

// PullRequestEvent is the provider-agnostic shape of a pull/merge request
// webhook. State is already normalized to one of open/closed/merged/draft, so
// the handler never re-derives it. GitLab "merge requests" map onto the same
// struct.
type PullRequestEvent struct {
	// Action is the raw provider action (e.g. "opened", "closed", "merge").
	// The handler only needs to know whether it is terminal; see Terminal.

View on GitHub (pinned to 2c0912b6ec)

Solutions

  1. Generate a fresh PAT in the provider UI with the required scopes and re-enter it.
  2. 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.
  3. Check token expiration policy (GitLab instance-level expiry limits) and IP allowlists on the instance.

Example fix

// before
if err := vcs.ValidateToken(ctx, kind, baseURL, token); err != nil {
	log.Printf("vcs connect failed: %v", err) // conflates auth with network
}

// after
err := vcs.ValidateToken(ctx, kind, baseURL, token)
switch {
case errors.Is(err, vcs.ErrUnauthorized):
	respond(w, 400, "token rejected by the instance — check value, scopes, and expiry")
case err != nil:
	respond(w, 502, "could not reach the instance to validate the token")
}
Defensive patterns

Strategy: try-catch

Validate before calling

// cheap pre-flight: does the token fetch the authenticated user?
req, _ := http.NewRequest("GET", instanceURL+"/api/v1/user", nil)
req.Header.Set("Authorization", "Bearer "+token)
resp, err := http.DefaultClient.Do(req)
if err == nil && (resp.StatusCode == 401 || resp.StatusCode == 403) {
	return errors.New("token rejected — fix before connecting")
}

Try / catch

err := vcs.ValidateToken(ctx, kind, baseURL, token)
if err != nil {
	if errors.Is(err, vcs.ErrUnauthorized) {
		// credential problem: ask for a new token, do not retry
		return respondTokenRejected(w)
	}
	// everything else is transport/instance — safe to retry with backoff
	return retryableError(err)
}

Prevention

When it happens

Trigger: 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.

Common situations: 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.

Understand the failure class

Related errors


AI-assisted analysis of multica-ai/multica@2c0912b6ec (2026-08-15). Data as JSON: /api/errors/a4fe254f2327dde6. Report an issue: GitHub.