gastownhall/beads · error

oauth: token request failed (%s): %s

Error message

oauth: token request failed (%s): %s

What it means

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.

Source

Thrown at internal/linear/oauth.go:143

		return fmt.Errorf("oauth: failed to create token request: %w", err)
	}
	req.Header.Set("Content-Type", "application/x-www-form-urlencoded")

	resp, err := m.client.Do(req)
	if err != nil {
		return fmt.Errorf("oauth: token request failed: %w", err)
	}
	defer func() { _ = resp.Body.Close() }()

	body, err := io.ReadAll(io.LimitReader(resp.Body, 1<<20)) // 1MB limit
	if err != nil {
		return fmt.Errorf("oauth: failed to read token response: %w", err)
	}

	if resp.StatusCode != http.StatusOK {
		var errResp oauthErrorResponse
		if json.Unmarshal(body, &errResp) == nil && errResp.Error != "" {
			return fmt.Errorf("oauth: token request failed (%s): %s", errResp.Error, errResp.Description)
		}
		return fmt.Errorf("oauth: token request returned status %d: %s", resp.StatusCode, string(body))
	}

	var tokenResp oauthTokenResponse
	if err := json.Unmarshal(body, &tokenResp); err != nil {
		return fmt.Errorf("oauth: failed to parse token response: %w", err)
	}

	if tokenResp.AccessToken == "" {
		return fmt.Errorf("oauth: token response missing access_token")
	}

	m.token = tokenResp.AccessToken
	m.expiresAt = m.nowFunc().Add(time.Duration(tokenResp.ExpiresIn) * time.Second)

	debug.Logf("oauth: acquired token (expires in %ds)", tokenResp.ExpiresIn)
	return nil

View on GitHub (pinned to 71377f2769)

Solutions

  1. Read errResp.Error/Description in the wrapped message and fix the matching OAuth parameter.
  2. Re-verify client_id and client_secret against the provider (most common: invalid_client after rotation).
  3. Trim the requested scopes to those actually granted to the integration.
  4. If credentials live in env/secrets, confirm the deployment is using the refreshed values.

Example fix

// before
"client_secret": {m.config.ClientSecret} // stale after rotation
// after
"client_secret": {secretFromSecretManager}" // refreshed credential
Defensive patterns

Strategy: validation

Validate before calling

if cfg.ClientID == "" || cfg.ClientSecret == "" {
    return fmt.Errorf("oauth client credentials missing")
}

Try / catch

var oauthErr *OAuthError // parse errResp.Error/Description from message
if strings.Contains(err.Error(), "invalid_client") {
    // rotate credentials and reload config
}

Prevention

When it happens

Trigger: resp.StatusCode != 200 and json.Unmarshal into oauthErrorResponse succeeds with a non-empty Error field during acquireToken.

Common situations: Wrong client_id/client_secret after a credential rotation; invalid scopes requested; expired or revoked authorization; grant_type mismatch with what the server supports.

Related errors


AI-assisted analysis of gastownhall/beads@71377f2769 (2026-08-30). Data as JSON: /api/errors/3b513b24a5239e9e. Report an issue: GitHub.