{"record":{"id":"41b077cdb45c71ae","repo":"JuliusBrussee/caveman","slug":"prefix-replacement-put-w","errorCode":null,"errorMessage":"prefix replacement put: %w","messagePattern":"prefix replacement put: %w","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"warning","filePath":"proxy/internal/store/prefix_cache.go","lineNumber":55,"sourceCode":"\n// RememberReplacement durably records original→replacement and returns the\n// authoritative bytes for that original. Storage is first-write-wins: if another\n// in-flight request already stored a replacement for the same block, that one is\n// returned and the caller forwards it, so two requests can never put two different\n// prefixes on the wire for one logical message.\nfunc (s *Store) RememberReplacement(scope string, original, replacement []byte, handle string) ([]byte, error) {\n\tif scope == \"\" || len(original) == 0 || len(replacement) == 0 || handle == \"\" {\n\t\treturn nil, errors.New(\"prefix replacement: incomplete entry\")\n\t}\n\tkey := prefixCacheKey(scope, original)\n\tnow := prefixCacheNow()\n\tif _, err := s.db.Exec(\n\t\t`INSERT INTO prefix_replacements (original_sha256, handle, replacement, created_at, last_used_at)\n\t\t VALUES (?,?,?,?,?)\n\t\t ON CONFLICT(original_sha256) DO UPDATE SET last_used_at=excluded.last_used_at`,\n\t\tkey, handle, replacement, now, now,\n\t); err != nil {\n\t\treturn nil, fmt.Errorf(\"prefix replacement put: %w\", err)\n\t}\n\tstored, _, ok := s.readReplacement(key)\n\tif !ok {\n\t\treturn nil, errors.New(\"prefix replacement: entry unreadable after write\")\n\t}\n\ts.evictPrefixReplacements()\n\treturn stored, nil\n}\n\nfunc (s *Store) readReplacement(key string) ([]byte, string, bool) {\n\tvar handle string\n\tvar replacement []byte\n\trow := s.db.QueryRow(`SELECT handle, replacement FROM prefix_replacements WHERE original_sha256 = ?`, key)\n\tswitch err := row.Scan(&handle, &replacement); {\n\tcase errors.Is(err, sql.ErrNoRows):\n\t\treturn nil, \"\", false\n\tcase err != nil:\n\t\t// NOT a miss: the entry may well exist. The caller still has to fail safe and","sourceCodeStart":37,"sourceCodeEnd":73,"githubUrl":"https://github.com/JuliusBrussee/caveman/blob/27d5a3981a347890211bb1bf2439e5c821a63bc9/proxy/internal/store/prefix_cache.go#L37-L73","documentation":"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.","triggerScenarios":"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).","commonSituations":"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.","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"],"exampleFix":"// before: caller propagates and breaks traffic on a cache-write failure\nrepl, err := st.RememberReplacement(scope, orig, newBytes, handle)\nif err != nil { return err }\n\n// after: fail-open per the byte-safe contract\nrepl, err := st.RememberReplacement(scope, orig, newBytes, handle)\nif err != nil {\n    slog.Warn(\"prefix replacement write failed; forwarding original bytes\", \"error\", err)\n    return orig, nil\n}","handlingStrategy":"fallback","validationCode":"// Confirm the store is usable before the request path depends on it\nif err := st.Ping(); err != nil { // or a trivial query\n    log.Warn(\"prefix cache degraded; transforms will forward original bytes\", \"error\", err)\n}","typeGuard":null,"tryCatchPattern":"repl, err := st.RememberReplacement(scope, orig, newBytes, handle)\nif err != nil {\n    // fail-open per byte-safe contract: forward original bytes, never fail the request\n    slog.Warn(\"prefix replacement put failed; forwarding original\", \"error\", err)\n    return orig, nil\n}\nreturn repl, nil","preventionTips":["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"],"tags":["sqlite","persistence","concurrency","go"],"backgroundTag":null,"analyzedSha":"27d5a3981a347890211bb1bf2439e5c821a63bc9","analyzedAt":"2026-08-15T09:26:11.751Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}