JuliusBrussee/caveman · warning

prefix replacement put: %w

Error message

prefix replacement put: %w

What it means

Returned by Store.RememberReplacement when the SQLite INSERT/UPSERT into prefix_replacements fails. The argument validation has already passed, so this is a database-level failure: constraint violation other than the handled conflict, disk I/O error, database locked, or schema drift.

Source

Thrown at proxy/internal/store/prefix_cache.go:55

// RememberReplacement durably records original→replacement and returns the
// authoritative bytes for that original. Storage is first-write-wins: if another
// in-flight request already stored a replacement for the same block, that one is
// returned and the caller forwards it, so two requests can never put two different
// prefixes on the wire for one logical message.
func (s *Store) RememberReplacement(scope string, original, replacement []byte, handle string) ([]byte, error) {
	if scope == "" || len(original) == 0 || len(replacement) == 0 || handle == "" {
		return nil, errors.New("prefix replacement: incomplete entry")
	}
	key := prefixCacheKey(scope, original)
	now := prefixCacheNow()
	if _, err := s.db.Exec(
		`INSERT INTO prefix_replacements (original_sha256, handle, replacement, created_at, last_used_at)
		 VALUES (?,?,?,?,?)
		 ON CONFLICT(original_sha256) DO UPDATE SET last_used_at=excluded.last_used_at`,
		key, handle, replacement, now, now,
	); err != nil {
		return nil, fmt.Errorf("prefix replacement put: %w", err)
	}
	stored, _, ok := s.readReplacement(key)
	if !ok {
		return nil, errors.New("prefix replacement: entry unreadable after write")
	}
	s.evictPrefixReplacements()
	return stored, nil
}

func (s *Store) readReplacement(key string) ([]byte, string, bool) {
	var handle string
	var replacement []byte
	row := s.db.QueryRow(`SELECT handle, replacement FROM prefix_replacements WHERE original_sha256 = ?`, key)
	switch err := row.Scan(&handle, &replacement); {
	case errors.Is(err, sql.ErrNoRows):
		return nil, "", false
	case err != nil:
		// NOT a miss: the entry may well exist. The caller still has to fail safe and

View on GitHub (pinned to 27d5a3981a)

Solutions

  1. Confirm the store was opened with store.Open (which applies schema + WAL + busy_timeout) and not a bare sql.Open elsewhere
  2. Raise busy_timeout in the DSN pragmas if concurrent writers cause SQLITE_BUSY (it must stay > 0 alongside journal_mode(WAL))
  3. Check that ~/.caveman/caveman.db and its directory are writable and the disk is not full; delete/rotate the DB if corrupted (sqlite3 .recover)
  4. At the call site, honor the fail-open contract: log the error and forward original bytes rather than failing the request

Example fix

// before: caller propagates and breaks traffic on a cache-write failure
repl, err := st.RememberReplacement(scope, orig, newBytes, handle)
if err != nil { return err }

// after: fail-open per the byte-safe contract
repl, err := st.RememberReplacement(scope, orig, newBytes, handle)
if err != nil {
    slog.Warn("prefix replacement write failed; forwarding original bytes", "error", err)
    return orig, nil
}
Defensive patterns

Strategy: fallback

Validate before calling

// Confirm the store is usable before the request path depends on it
if err := st.Ping(); err != nil { // or a trivial query
    log.Warn("prefix cache degraded; transforms will forward original bytes", "error", err)
}

Try / catch

repl, err := st.RememberReplacement(scope, orig, newBytes, handle)
if err != nil {
    // fail-open per byte-safe contract: forward original bytes, never fail the request
    slog.Warn("prefix replacement put failed; forwarding original", "error", err)
    return orig, nil
}
return repl, nil

Prevention

When it happens

Trigger: Calling RememberReplacement while another connection holds the write lock past busy_timeout (SQLITE_BUSY), after the store was Close()d (sql: database is closed), when the DB file's directory becomes read-only, or when the prefix_replacements table is missing/corrupt (e.g. an old DB without migrations, or a schema created by a different version).

Common situations: High-concurrency request paths racing on writes with a too-low busy_timeout; the DB on a sync service (Dropbox) that briefly locks the file; a caveman.db from an older binary missing the table; disk full. Note the design intent: callers should treat a failure here as fail-open and forward the original bytes.

Related errors


AI-assisted analysis of JuliusBrussee/caveman@27d5a3981a (2026-08-15). Data as JSON: /api/errors/41b077cdb45c71ae. Report an issue: GitHub.