OpenNHP/opennhp · error
keystore: insert otp
Error message
keystore: insert otp: %w
What it means
GenerateOTP inserts a row into otp_records; an Exec failure is wrapped as 'keystore: insert otp'. Typical causes are SQLITE_BUSY under write contention, a UNIQUE/constraint violation, or database I/O errors.
Solutions
- Read the wrapped cause: SQLITE_BUSY means contention — serialize writes or move to a client-server DB at scale.
- Never place the SQLite file on NFS/network storage; SQLite locking is local-disk only.
- Check disk quota/space in the container or volume.
- Ensure only one process writes; SetMaxOpenConns(1) already serializes within the process.
Defensive patterns
Strategy: retry
Validate before calling
// keep the database on local writable disk
if fi, err := os.Stat(dbPath); err != nil || !fi.Mode().Perm().IsRegular() { /* abort */ } Try / catch
if err != nil && strings.Contains(err.Error(), "insert otp") {
if strings.Contains(err.Error(), "SQLITE_BUSY") {
time.Sleep(100 * time.Millisecond) // bounded retry
}
} Prevention
- Do not place SQLite files on NFS
- Rate-limit OTP generation endpoints
- Alert on disk-quota exhaustion
- Keep one writer process per database
When it happens
Trigger: High OTP issuance rate exceeding the single-writer connection's throughput (busy_timeout 5000ms exhausted), disk full, or a corrupted/truncated otp_records table.
Common situations: Load tests hammering OTP generation against one SQLite file, deployments with the keystore on a network filesystem that breaks SQLite locking, container disk quota exhausted.
Related errors
- keystore: query otp
- keystore: query pending otp
- keystore: sweep otp
- keystore: open database
- keystore: migrate
AI-assisted analysis of OpenNHP/opennhp@6e04ca5ff0 (2026-09-07).
Data as JSON: /api/errors/2e0683c6993b3e23.
Report an issue: GitHub.
Appendix: source
Thrown at endpoints/server/keystore.go:243
if err != nil {
return "", fmt.Errorf("keystore: generate otp: %w", err)
}
now := time.Now().Unix()
expires := time.Now().Add(p.TTL).Unix()
// Invalidate previous unused OTPs for this user+device.
_, _ = s.db.Exec(
`UPDATE otp_records SET used = 1 WHERE usr_id = ? AND dev_id = ? AND used = 0`,
p.UserId, p.DeviceId,
)
_, err = s.db.Exec(
`INSERT INTO otp_records (usr_id, dev_id, otp_code, pub_key, created_at, expires_at) VALUES (?, ?, ?, ?, ?, ?)`,
p.UserId, p.DeviceId, code, p.PublicKey, now, expires,
)
if err != nil {
return "", fmt.Errorf("keystore: insert otp: %w", err)
}
log.Info("keystore: otp generated for user=%s device=%s", p.UserId, p.DeviceId)
return code, nil
}
// MaxOTPAttempts is the number of consecutive incorrect OTP guesses allowed
// before the OTP is invalidated.
const MaxOTPAttempts = 5
// ValidateOTP checks the OTP for the given user+device. Returns nil on
// success, or a specific error:
//
// ErrOTPInvalid — no matching OTP found (or wrong code)
// ErrOTPExpired — OTP has expired
// ErrOTPAlreadyUsed — OTP was already used
// ErrOTPRateLimited — too many failed attempts; OTP has been invalidated
// ErrOTPPublicKeyMismatch — OTP was issued for a different public keyView on GitHub (pinned to 6e04ca5ff0)