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 andView on GitHub (pinned to 27d5a3981a)
Solutions
- Confirm the store was opened with store.Open (which applies schema + WAL + busy_timeout) and not a bare sql.Open elsewhere
- Raise busy_timeout in the DSN pragmas if concurrent writers cause SQLITE_BUSY (it must stay > 0 alongside journal_mode(WAL))
- Check that ~/.caveman/caveman.db and its directory are writable and the disk is not full; delete/rotate the DB if corrupted (sqlite3 .recover)
- 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
- Keep WAL + busy_timeout pragmas on the DSN (required by the cache-safety contract)
- Never call RememberReplacement after Store.Close
- Treat this store as advisory: any error path must forward original bytes
- Watch for SQLITE_BUSY under concurrency and raise busy_timeout before shipping
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
- memory %s changed during supersede
- ccr: typed object id collision
- native runtime: socket already active
- open sqlite %q: %w
- migrate sqlite %q: %w
AI-assisted analysis of JuliusBrussee/caveman@27d5a3981a (2026-08-15).
Data as JSON: /api/errors/41b077cdb45c71ae.
Report an issue: GitHub.