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

  1. Log the full wrapped error chain to identify the SQLite error code.
  2. Run PRAGMA integrity_check via sqlite3 CLI if corruption is suspected.
  3. Restore schema consistency: do not alter otp_records outside migrate().
  4. 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

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


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)