fix(proxy): prevent use-after-recycle in background cache refresh

Copy the request before spawning the background goroutine. The
fasthttp.RequestCtx is recycled after the handler returns, so passing
it to a goroutine causes data corruption under high concurrency.

The caller now AcquireRequest+CopyTo before go(), and the goroutine
releases it. backgroundRefresh no longer accepts ctx directly.
This commit is contained in:
xfy 2026-06-03 01:08:50 +08:00
parent c2dd4fa9a3
commit 2c3cc1ba38
2 changed files with 13 additions and 16 deletions

View File

@ -55,20 +55,14 @@ func (p *Proxy) writeCachedResponse(ctx *fasthttp.RequestCtx, entry *cache.Proxy
// 该方法在独立 goroutine 中运行,不阻塞主请求流程。
//
// 参数:
// - ctx: 原始 FastHTTP 请求上下文(仅用于复制请求信息
// - req: 预复制的请求副本(调用方负责 Acquire/Release
// - target: 要刷新的后端目标
// - hashKey: 缓存哈希键
// - origKey: 缓存原始键
func (p *Proxy) backgroundRefresh(ctx *fasthttp.RequestCtx, target *loadbalance.Target, hashKey uint64, origKey string) {
// 创建新的请求上下文副本
req := fasthttp.AcquireRequest()
func (p *Proxy) backgroundRefresh(req *fasthttp.Request, target *loadbalance.Target, hashKey uint64, origKey string) {
resp := fasthttp.AcquireResponse()
defer fasthttp.ReleaseRequest(req)
defer fasthttp.ReleaseResponse(resp)
// 复制原始请求
ctx.Request.CopyTo(req)
// 如果启用 Revalidate添加条件请求头
if p.config.Cache.Revalidate {
if entry, ok, _ := p.cache.Get(hashKey, origKey); ok {

View File

@ -651,14 +651,17 @@ func (p *Proxy) ServeHTTP(ctx *fasthttp.RequestCtx) {
}
return
}
// 过期缓存,尝试后台刷新,同时返回旧数据
if !p.config.Cache.BackgroundUpdateDisable {
entry.Updating.Store(true)
go func() {
defer entry.Updating.Store(false)
p.backgroundRefresh(ctx, target, hashKey, origKey)
}()
}
// 过期缓存,尝试后台刷新,同时返回旧数据
if !p.config.Cache.BackgroundUpdateDisable {
entry.Updating.Store(true)
reqCopy := fasthttp.AcquireRequest()
ctx.Request.CopyTo(reqCopy)
go func() {
defer entry.Updating.Store(false)
defer fasthttp.ReleaseRequest(reqCopy)
p.backgroundRefresh(reqCopy, target, hashKey, origKey)
}()
}
upstreamAddr = upstreamCache
upstreamStatus = entry.Status