OpenNHP/opennhp · error

keystore: open database

Error message

keystore: open database %s: %w

What it means

NewAgentKeyStore wraps any error from sql.Open (driver registration / DSN parse) with this message. sql.Open for the 'sqlite' driver rarely touches disk — it fails mainly when the driver is not registered (blank import missing) or the DSN is malformed. The wrapped cause identifies which.

Solutions

  1. Verify the sqlite driver is imported for its side effect: _ "modernc.org/sqlite" (or the mattn variant).
  2. Check the DSN query params match the driver: _journal_mode=WAL&_busy_timeout=5000 works for modernc/mattn; other drivers differ.
  3. Confirm go.mod contains the sqlite driver module and the binary was rebuilt.
  4. Log the wrapped %w cause — it names the driver/DSN problem exactly.

Example fix

// before
import (
    "database/sql"
)
// after
import (
    "database/sql"
    _ "modernc.org/sqlite"
)
Defensive patterns

Strategy: validation

Validate before calling

// driver must be registered before opening
import _ "modernc.org/sqlite"
if _, err := sql.Open("sqlite", ":memory:"); err != nil {
    return fmt.Errorf("sqlite driver unavailable: %w", err)
}

Prevention

When it happens

Trigger: Calling NewAgentKeyStore with a driver name mismatch (e.g. modernc.org/sqlite vs mattn/go-sqlite3 DSN style), or when the sqlite driver was never imported so sql.Open returns 'sql: unknown driver'.

Common situations: Adding a dependency swap without updating the DSN query parameters (e.g. _journal_mode=WAL unsupported by the chosen driver), or building with a driver package accidentally dropped from go.mod.

Understand the failure class

Background: "open() failed", "failed to open file", "cannot create file" — what a file open error means and how to fix it — this error's family across 42 libraries.

Related errors


AI-assisted analysis of OpenNHP/opennhp@6e04ca5ff0 (2026-09-07). Data as JSON: /api/errors/91a6412f87348d9e. Report an issue: GitHub.

Appendix: source

Thrown at endpoints/server/keystore.go:42

// public key when the operator has not configured agentKeyTTLSeconds.
// 24 hours. Mirrors how OTPTTLSeconds is defaulted at the helper layer.
const DefaultAgentKeyTTLSeconds int64 = 86400

// NewAgentKeyStore opens (or creates) the SQLite database at dbPath.
// The directory is created if it does not exist.
func NewAgentKeyStore(dbPath string) (*AgentKeyStore, error) {
	if dbPath == "" {
		dbPath = filepath.Join("data", "nhp_server.db")
	}

	dir := filepath.Dir(dbPath)
	if err := os.MkdirAll(dir, 0700); err != nil {
		return nil, fmt.Errorf("keystore: create directory %s: %w", dir, err)
	}

	db, err := sql.Open("sqlite", dbPath+"?_journal_mode=WAL&_busy_timeout=5000")
	if err != nil {
		return nil, fmt.Errorf("keystore: open database %s: %w", dbPath, err)
	}

	// Connection pool tuning — SQLite is single-writer; one open conn is
	// usually correct. Keep a small idle pool for concurrent read queries.
	db.SetMaxOpenConns(1)
	db.SetMaxIdleConns(1)
	db.SetConnMaxLifetime(0)

	store := &AgentKeyStore{db: db}
	if err := store.migrate(); err != nil {
		db.Close()
		return nil, fmt.Errorf("keystore: migrate: %w", err)
	}

	log.Info("keystore: database opened at %s", dbPath)
	return store, nil
}

View on GitHub (pinned to 6e04ca5ff0)