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
- Unwrap the driver error to identify the failing constraint or lock
- Resolve DB contention (single writer, busy_timeout) and retry the revoke
- 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
- Remember revocation is idempotent — repeated 404 is success
- Run revoke outside peak write load on SQLite
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
- saving oauth client: %w
- saving node(%d) after adding IPs: %w
- checking name uniqueness: %w
- renaming node in database: %w
- registering existing node in database: %w
AI-assisted analysis of juanfont/headscale@565fd254d0 (2026-08-15).
Data as JSON: /api/errors/7121107ee6166b6c.
Report an issue: GitHub.