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

  1. Unwrap the driver error to identify constraint vs availability
  2. Verify the creator user exists before creating the client
  3. Retry creation after resolving DB health — the id is regenerated each attempt
  4. 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

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


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