{"record":{"id":"fcb47539255394d0","repo":"juanfont/headscale","slug":"saving-oauth-client-w","errorCode":null,"errorMessage":"saving oauth client: %w","messagePattern":"saving oauth client: %w","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"hscontrol/db/oauth.go","lineNumber":175,"sourceCode":"\t\treturn \"\", nil, err\n\t}\n\n\tnow := time.Now().UTC()\n\tclient := types.OAuthClient{\n\t\tClientID:    clientID,\n\t\tSecretHash:  hash,\n\t\tScopes:      scopes,\n\t\tTags:        tags,\n\t\tDescription: description,\n\t\tUserID:      creatorUserID,\n\t\tCreatedAt:   &now,\n\t}\n\n\terr = hsdb.Write(func(tx *gorm.DB) error {\n\t\treturn tx.Save(&client).Error\n\t})\n\tif err != nil {\n\t\treturn \"\", nil, fmt.Errorf(\"saving oauth client: %w\", err)\n\t}\n\n\treturn secretStr, &client, nil\n}\n\n// AuthenticateOAuthClient validates a presented client secret and returns the\n// matching, unrevoked [types.OAuthClient]. The client id is derived from the\n// secret (its middle segment), so any separately-supplied client_id is\n// redundant, matching Tailscale, where get-authkey passes a dummy id and the\n// server derives the real one from the secret.\nfunc (hsdb *HSDatabase) AuthenticateOAuthClient(secretStr string) (*types.OAuthClient, error) {\n\tif secretStr == \"\" {\n\t\treturn nil, ErrOAuthClientFailedToParse\n\t}\n\n\t// Tailscale allows the secret to carry optional ?key=value attributes when\n\t// used directly as an auth key; strip them before parsing.\n\tsecretStr, _, _ = strings.Cut(secretStr, \"?\")","sourceCodeStart":157,"sourceCodeEnd":193,"githubUrl":"https://github.com/juanfont/headscale/blob/565fd254d06c4c7f9a8cad1714a43445c79ba420/hscontrol/db/oauth.go#L157-L193","documentation":"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.","triggerScenarios":" astronomically rare client_id collision; DB lock/timeout; NOT NULL or FK violation (creator user id missing); connection loss mid-transaction.","commonSituations":"Creating clients while the DB is under heavy write load on SQLite; referencing a user id from a partially deleted user.","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"],"exampleFix":null,"handlingStrategy":"try-catch","validationCode":"// Verify the creator user exists before creating the client\nif _, err := hsdb.GetUserByID(creatorID); err != nil {\n\treturn fmt.Errorf(\"refusing to create client for missing user: %w\", err)\n}","typeGuard":null,"tryCatchPattern":"if _, _, err := hsdb.CreateOAuthClient(...); err != nil {\n\tif isUniqueViolation(err) || isTransientDBError(err) {\n\t\t// safe to retry; a new client id is generated each attempt\n\t}\n\treturn err\n}","preventionTips":["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"],"tags":["go","oauth","database","gorm","constraints"],"backgroundTag":null,"analyzedSha":"565fd254d06c4c7f9a8cad1714a43445c79ba420","analyzedAt":"2026-08-15T13:12:30.133Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}