From 51c1b6159af78b407cc087a97d1a508e48b10b8b Mon Sep 17 00:00:00 2001 From: AgentSmithClaw <13392688435a@gmail.com> Date: Sun, 19 Jul 2026 12:29:32 +0800 Subject: [PATCH] fix(api): make HTTP response cache writes atomic Write cache entries to a temp file in the same directory and rename into place so concurrent gh processes and mid-write crashes cannot leave a partial or interleaved cache file. Fixes #252 --- pkg/api/cache.go | 26 +++++++++++++++++++++----- 1 file changed, 21 insertions(+), 5 deletions(-) diff --git a/pkg/api/cache.go b/pkg/api/cache.go index 7085d052..18ab3966 100644 --- a/pkg/api/cache.go +++ b/pkg/api/cache.go @@ -176,18 +176,21 @@ func (fs *fileStorage) store(key string, res *http.Response) (storeErr error) { fs.mu.Lock() defer fs.mu.Unlock() - if storeErr = os.MkdirAll(filepath.Dir(cacheFile), 0755); storeErr != nil { + dir := filepath.Dir(cacheFile) + if storeErr = os.MkdirAll(dir, 0755); storeErr != nil { return } + // Write to a temporary file in the same directory, then rename into place + // so concurrent processes and partial writes cannot corrupt the cache entry. var f *os.File - if f, storeErr = os.OpenFile(cacheFile, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600); storeErr != nil { + if f, storeErr = os.CreateTemp(dir, ".gh-cache-*"); storeErr != nil { return } - + tmpName := f.Name() defer func() { - if err := f.Close(); storeErr == nil && err != nil { - storeErr = err + if storeErr != nil { + _ = os.Remove(tmpName) } }() @@ -202,6 +205,19 @@ func (fs *fileStorage) store(key string, res *http.Response) (storeErr error) { res.Body = origBody } + if cerr := f.Close(); storeErr == nil && cerr != nil { + storeErr = cerr + } + if storeErr != nil { + return + } + + if err := os.Chmod(tmpName, 0600); err != nil { + storeErr = err + return + } + + storeErr = os.Rename(tmpName, cacheFile) return }