juanfont/headscale · error
saving oauth client: %w
Error message
saving oauth client: %w
What it means
CreateOAuthClient persists the new client row (with its Argon2id secret hash) inside a write transaction; this error is that tx.Save failing. The plaintext secret is returned only on success, so failure means no usable credential was created. Most common cause is a unique-constraint hit on client_id or a database availability problem.
Source
Thrown at hscontrol/db/oauth.go:175
return "", nil, err
}
now := time.Now().UTC()
client := types.OAuthClient{
ClientID: clientID,
SecretHash: hash,
Scopes: scopes,
Tags: tags,
Description: description,
UserID: creatorUserID,
CreatedAt: &now,
}
err = hsdb.Write(func(tx *gorm.DB) error {
return tx.Save(&client).Error
})
if err != nil {
return "", nil, fmt.Errorf("saving oauth client: %w", err)
}
return secretStr, &client, nil
}
// AuthenticateOAuthClient validates a presented client secret and returns the
// matching, unrevoked [types.OAuthClient]. The client id is derived from the
// secret (its middle segment), so any separately-supplied client_id is
// redundant, matching Tailscale, where get-authkey passes a dummy id and the
// server derives the real one from the secret.
func (hsdb *HSDatabase) AuthenticateOAuthClient(secretStr string) (*types.OAuthClient, error) {
if secretStr == "" {
return nil, ErrOAuthClientFailedToParse
}
// Tailscale allows the secret to carry optional ?key=value attributes when
// used directly as an auth key; strip them before parsing.
secretStr, _, _ = strings.Cut(secretStr, "?")View on GitHub (pinned to 565fd254d0)
Solutions
- Unwrap the driver error to identify constraint vs availability
- Verify the creator user exists before creating the client
- Retry creation after resolving DB health — the id is regenerated each attempt
- Treat as a 5xx in API surfaces; the secret was not issued
Defensive patterns
Strategy: try-catch
Validate before calling
// Verify the creator user exists before creating the client
if _, err := hsdb.GetUserByID(creatorID); err != nil {
return fmt.Errorf("refusing to create client for missing user: %w", err)
} Try / catch
if _, _, err := hsdb.CreateOAuthClient(...); err != nil {
if isUniqueViolation(err) || isTransientDBError(err) {
// safe to retry; a new client id is generated each attempt
}
return err
} Prevention
- Treat creation failure as 'no credential issued' — never reuse a secret from a failed call
- Keep DB health monitored during admin operations
- Return 5xx, not the plaintext secret, on failure
When it happens
Trigger: astronomically rare client_id collision; DB lock/timeout; NOT NULL or FK violation (creator user id missing); connection loss mid-transaction.
Common situations: Creating clients while the DB is under heavy write load on SQLite; referencing a user id from a partially deleted user.
Related errors
- saving node to database: %w
- deleting oauth access tokens: %w
- saving node(%d) after adding IPs: %w
- checking name uniqueness: %w
- renaming node in database: %w
AI-assisted analysis of juanfont/headscale@565fd254d0 (2026-08-15).
Data as JSON: /api/errors/fcb47539255394d0.
Report an issue: GitHub.