OpenNHP/opennhp · error
keystore: query otp
Error message
keystore: query otp: %w
What it means
ValidateOTP runs the lookup for a matching unused, unexpired OTP row; if QueryRow/Scan fails with anything other than sql.ErrNoRows it is wrapped as 'keystore: query otp'. This is an infrastructure error, distinct from invalid/expired codes which return sentinel errors.
Solutions
- Log the full wrapped error chain to identify the SQLite error code.
- Run PRAGMA integrity_check via sqlite3 CLI if corruption is suspected.
- Restore schema consistency: do not alter otp_records outside migrate().
- Verify the db file and its directory are writable (WAL requires write access even for reads).
Defensive patterns
Strategy: try-catch
Try / catch
if err := store.ValidateOTP(u, d, code); err != nil {
switch {
case errors.Is(err, common.ErrOTPInvalid), errors.Is(err, common.ErrOTPExpired), errors.Is(err, common.ErrOTPCooldown):
// user-facing sentinels — safe to expose
default:
log.Error("otp db failure: %v", err) // 'query otp' wrapper: infrastructure
return http.StatusServiceUnavailable
}
} Prevention
- Distinguish sentinel errors (errors.Is) from DB failures
- Never hand-edit otp_records schema
- Run integrity checks after host crashes
When it happens
Trigger: SQLite error during the SELECT (corrupt db, I/O error, BUSY beyond timeout) or a Scan type mismatch between the queried columns and the destination variables.
Common situations: Corrupted database after a host crash, the otp_records table altered manually/externally so Scan destinations no longer match, read-only mount on the db file.
Understand the failure class
Background: Database query failed: Internal Server Error 500s wrapping SQL, Prisma, and connection failures — what to check first — this error's family across 16 libraries.
Related errors
- keystore: query pending otp
- keystore: insert otp
- keystore: query pubkey conflict
- keystore: sweep otp
- keystore: open database
AI-assisted analysis of OpenNHP/opennhp@6e04ca5ff0 (2026-09-07).
Data as JSON: /api/errors/85094bc5274cf0e2.
Report an issue: GitHub.
Appendix: source
Thrown at endpoints/server/keystore.go:305
}
if time.Now().Unix() > expiresAt {
return common.ErrOTPExpired
}
// Verify the registering public key matches the one bound at OTP issuance.
if storedPubKey != "" && pubKey != storedPubKey {
return common.ErrOTPPublicKeyMismatch
}
// Mark as used — reset attempts to 0 on success.
_, err = s.db.Exec(`UPDATE otp_records SET used = 1, attempts = 0 WHERE id = ?`, id)
if err != nil {
log.Error("keystore: mark otp used: %v", err)
}
log.Info("keystore: otp validated for user=%s device=%s", userId, deviceId)
return nil
}
if err != sql.ErrNoRows {
return fmt.Errorf("keystore: query otp: %w", err)
}
// Code did not match — track the failed attempt on the most recent
// pending (unused, unexpired) OTP for this user+device.
err = s.db.QueryRow(
`SELECT id, expires_at, used, attempts FROM otp_records
WHERE usr_id = ? AND dev_id = ? AND used = 0
ORDER BY created_at DESC LIMIT 1`,
userId, deviceId,
).Scan(&id, &expiresAt, &used, &attempts)
if err == sql.ErrNoRows {
return common.ErrOTPInvalid
}
if err != nil {
return fmt.Errorf("keystore: query pending otp: %w", err)
}
if time.Now().Unix() > expiresAt {View on GitHub (pinned to 6e04ca5ff0)