juanfont/headscale · error

deleting oauth access tokens: %w

Error message

deleting oauth access tokens: %w

What it means

RevokeOAuthClient first deletes all OAuthAccessTokens rows for the client, then the client itself, in one write transaction. This error is the token DELETE failing at the DB level; note it fires before the RowsAffected==0 check, so it is never the 'unknown client' signal — that is ErrOAuthClientNotFound returned later.

Source

Thrown at hscontrol/db/oauth.go:257

	err := hsdb.DB.Find(&clients).Error
	if err != nil {
		return nil, err
	}

	return clients, nil
}

// RevokeOAuthClient deletes a client and all access tokens it issued. An unknown
// client id returns [ErrOAuthClientNotFound], so a repeated DELETE is a clean
// 404. Unlike pre-auth keys (which soft-revoke for node-registration history), an
// OAuth client has no such history and is removed outright, matching Tailscale.
func (hsdb *HSDatabase) RevokeOAuthClient(clientID string) error {
	return hsdb.Write(func(tx *gorm.DB) error {
		err := tx.Where("client_id = ?", clientID).
			Delete(&types.OAuthAccessToken{}).Error
		if err != nil {
			return fmt.Errorf("deleting oauth access tokens: %w", err)
		}

		res := tx.Where("client_id = ?", clientID).Delete(&types.OAuthClient{})
		if res.Error != nil {
			return res.Error
		}

		if res.RowsAffected == 0 {
			return ErrOAuthClientNotFound
		}

		return nil
	})
}

// MintAccessToken stores a new [types.OAuthAccessToken] for clientID with the
// given (already narrowed) scopes/tags and expiration, returning the plaintext
// token (shown ONCE).

View on GitHub (pinned to 565fd254d0)

Solutions

  1. Unwrap the driver error to identify the failing constraint or lock
  2. Resolve DB contention (single writer, busy_timeout) and retry the revoke
  3. Re-run the DELETE — revocation is idempotent and a repeat returns clean 404 per the doc comment
Defensive patterns

Strategy: retry

Try / catch

if err := hsdb.RevokeOAuthClient(id); err != nil {
	if errors.Is(err, db.ErrOAuthClientNotFound) {
		return nil // already revoked; idempotent
	}
	if isTransientDBError(err) {
		return retry(3, func() error { return hsdb.RevokeOAuthClient(id) })
	}
	return err
}

Prevention

When it happens

Trigger: DB lock/timeout during revocation; FK constraint from a referencing table not modeled in the delete; connection loss mid-transaction.

Common situations: Revoking while many requests authenticate concurrently on SQLite; schema drift after skipped migrations.

Related errors


AI-assisted analysis of juanfont/headscale@565fd254d0 (2026-08-15). Data as JSON: /api/errors/7121107ee6166b6c. Report an issue: GitHub.