juanfont/headscale · error

invalid config: %w

Error message

invalid config: %w

What it means

ToURL() calls Validate() first and wraps any validation failure with the 'invalid config: ' prefix before building the modernc.org/sqlite connection string. This error is a wrapper: the real cause is one of the sentinel validation errors (empty path, negative busy_timeout, invalid journal_mode/auto_vacuum/wal_autocheckpoint/synchronous/txlock).

Source

Thrown at hscontrol/db/sqliteconfig/config.go:349

	}

	if c.Synchronous != "" && !c.Synchronous.IsValid() {
		return fmt.Errorf("%w: %s", ErrInvalidSynchronous, c.Synchronous)
	}

	if c.TxLock != "" && !c.TxLock.IsValid() {
		return fmt.Errorf("%w: %s", ErrInvalidTxLock, c.TxLock)
	}

	return nil
}

// ToURL builds a properly encoded SQLite connection string using _pragma parameters
// compatible with modernc.org/sqlite driver.
func (c *Config) ToURL() (string, error) {
	err := c.Validate()
	if err != nil {
		return "", fmt.Errorf("invalid config: %w", err)
	}

	// Handle different database types
	var baseURL string
	if c.Path == ":memory:" {
		baseURL = ":memory:"
	} else {
		baseURL = "file:" + c.Path
	}

	// Build query parameters
	var queryParts []string

	// Add _txlock first (it's a connection parameter, not a pragma)
	if c.TxLock != "" {
		queryParts = append(queryParts, "_txlock="+string(c.TxLock))
	}

View on GitHub (pinned to 565fd254d0)

Solutions

  1. Unwrap the error chain (errors.Is against the sqliteconfig.Err* sentinels) to find the exact field at fault.
  2. Fix the offending field per its sentinel's contract (non-empty path, valid pragma strings, proper ranges).
  3. Call cfg.Validate() early at config-load time to get a precise error before ToURL is reached.

Example fix

// before
url, err := cfg.ToURL()
if err != nil { log.Fatal(err) } // 'invalid config: invalid journal_mode: wal'

// after
if err := cfg.Validate(); err != nil { // precise, actionable
    log.Fatal().Err(err).Msg("sqlite config")
}
url, err := cfg.ToURL()
Defensive patterns

Strategy: try-catch

Validate before calling

// Fail fast with a precise error before building the DSN:
if err := cfg.Validate(); err != nil {
    return fmt.Errorf("validating sqlite config: %w", err)
}

Try / catch

url, err := cfg.ToURL()
if err != nil {
    switch {
    case errors.Is(err, sqliteconfig.ErrPathEmpty),
        errors.Is(err, sqliteconfig.ErrBusyTimeoutNegative),
        errors.Is(err, sqliteconfig.ErrInvalidJournalMode),
        errors.Is(err, sqliteconfig.ErrInvalidAutoVacuum),
        errors.Is(err, sqliteconfig.ErrWALAutocheckpoint),
        errors.Is(err, sqliteconfig.ErrInvalidSynchronous),
        errors.Is(err, sqliteconfig.ErrInvalidTxLock):
        // config bug: report field-level error, do not retry
    }
    return err
}

Prevention

When it happens

Trigger: Calling sqliteconfig.Config.ToURL() on any config that fails Validate(): empty Path, negative BusyTimeout, or any invalid enum pragma value. ToURL is the last step before opening the database, so this surfaces at headscale startup.

Common situations: Malformed database section in headscale's config file (typo'd pragma names, negative numbers), or programmatic configs built with unset/zero fields (empty Path triggers ErrPathEmpty).

Related errors


AI-assisted analysis of juanfont/headscale@565fd254d0 (2026-08-15). Data as JSON: /api/errors/4c944a5f1e0db8c9. Report an issue: GitHub.