{"record":{"id":"7764c01b139355d6","repo":"juanfont/headscale","slug":"saving-api-key-to-database-w","errorCode":null,"errorMessage":"saving API key to database: %w","messagePattern":"saving API key to database: %w","errorType":"http","errorClass":null,"httpStatus":500,"severity":"error","filePath":"hscontrol/db/api_key.go","lineNumber":57,"sourceCode":"\tsecret := rands.HexString(apiKeyHashLength)\n\n\t// Full key string (shown ONCE to user)\n\tkeyStr := apiKeyPrefix + prefix + \"-\" + secret\n\n\t// bcrypt hash of secret\n\thash, err := bcrypt.GenerateFromPassword([]byte(secret), bcrypt.DefaultCost)\n\tif err != nil {\n\t\treturn \"\", nil, err\n\t}\n\n\tkey := types.APIKey{\n\t\tPrefix:     prefix,\n\t\tHash:       hash,\n\t\tExpiration: expiration,\n\t}\n\n\tif err := hsdb.DB.Save(&key).Error; err != nil { //nolint:noinlineerr\n\t\treturn \"\", nil, fmt.Errorf(\"saving API key to database: %w\", err)\n\t}\n\n\treturn keyStr, &key, nil\n}\n\n// ListAPIKeys returns the list of [types.APIKey] values for a user.\nfunc (hsdb *HSDatabase) ListAPIKeys() ([]types.APIKey, error) {\n\tkeys := []types.APIKey{}\n\n\terr := hsdb.DB.Find(&keys).Error\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn keys, nil\n}\n\n// GetAPIKey returns a [types.APIKey] for a given key.","sourceCodeStart":39,"sourceCodeEnd":75,"githubUrl":"https://github.com/juanfont/headscale/blob/565fd254d06c4c7f9a8cad1714a43445c79ba420/hscontrol/db/api_key.go#L39-L75","documentation":"Returned by HSDatabase.CreateAPIKey when GORM's DB.Save(&key) fails while persisting a newly generated API key. The secret has already been bcrypt-hashed, so the error is purely a database write failure: connection loss, constraint violation, SQLite database lock, or insufficient disk/permissions. The generated key string is discarded (empty string returned) because the caller never receives it.","triggerScenarios":"Calling CreateAPIKey (e.g. via 'headscale apikeys create') while the database is locked by another writer (SQLite 'database is locked'), the DB connection has dropped, the api_keys table schema is out of sync with types.APIKey, or the unique index idx_api_keys_prefix collides with an existing prefix.","commonSituations":"SQLite deployments where the headscale CLI and server run concurrently and contend for the file; Postgres unreachable mid-operation; disk full on the host; a database restored from an older headscale version whose api_keys table lacks columns GORM tries to write.","solutions":["Check the wrapped error text: 'database is locked' means concurrent SQLite access - stop other writers or move to PostgreSQL.","Verify database connectivity and disk space on the database host.","Confirm the api_keys schema matches the binary version (run migrations / use the same headscale version for CLI and server).","Retry the API key creation; prefixes are random so a unique-index collision is effectively impossible on retry."],"exampleFix":"// before\nkeyStr, key, err := hsdb.CreateAPIKey(userID, expiration, nil)\nif err != nil {\n    log.Fatal().Err(err).Msg(\"failed\")\n}\n\n// after\nkeyStr, key, err := hsdb.CreateAPIKey(userID, expiration, nil)\nif err != nil {\n    if strings.Contains(err.Error(), \"database is locked\") {\n        // retry once after the SQLite writer finishes, or route writes through one process\n    }\n    return fmt.Errorf(\"creating API key: %w\", err)\n}","handlingStrategy":"retry","validationCode":"// Verify the database is writable before generating a key\nvar one int\nif err := hsdb.DB.Raw(\"SELECT 1\").Scan(&one).Error; err != nil {\n    return fmt.Errorf(\"database not ready: %w\", err)\n}","typeGuard":"func isTransientDBError(err error) bool {\n    if err == nil {\n        return false\n    }\n    msg := err.Error()\n    return strings.Contains(msg, \"database is locked\") ||\n        strings.Contains(msg, \"SQLSTATE 40001\") ||\n        strings.Contains(msg, \"connection refused\")\n}","tryCatchPattern":"keyStr, key, err := hsdb.CreateAPIKey(userID, exp, nil)\nif err != nil {\n    if isTransientDBError(err) {\n        // single retry; prefixes are random so no collision risk\n        keyStr, key, err = hsdb.CreateAPIKey(userID, exp, nil)\n    }\n    if err != nil {\n        return fmt.Errorf(\"creating API key: %w\", err)\n    }\n}","preventionTips":["Run only one headscale process (server or CLI) against a SQLite database at a time.","Prefer PostgreSQL for deployments where the CLI and server run concurrently.","Monitor disk space on the database host; bcrypt hashes plus rows are small but the failure mode is binary."],"tags":["database","gorm","api-key","sqlite","bcrypt"],"backgroundTag":null,"analyzedSha":"565fd254d06c4c7f9a8cad1714a43445c79ba420","analyzedAt":"2026-08-15T13:12:30.133Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}