fix(webhook): detach async goroutine from pooled echo.Context

This commit is contained in:
Julien Neuhart
2026-04-21 20:16:04 +02:00
parent c204cadfc5
commit 4b192b1498
5 changed files with 195 additions and 3 deletions

View File

@@ -334,13 +334,38 @@ func webhookMiddleware(w *Webhook) api.Middleware {
// As a webhook URL has been given, we handle the request in a
// goroutine and return immediately.
//
// Echo returns the echo.Context back to its sync.Pool as
// soon as this synchronous handler returns ErrAsyncProcess.
// A concurrent request can then claim the recycled context
// and c.Reset() wipes the shared store, which would cause
// any c.Get("...").(T) assertion downstream of the webhook
// goroutine to panic on a nil value and crash the process.
// Snapshot the keys downstream reads onto a detached
// wrapper before spawning the goroutine so pool reuse
// cannot reach into our async work.
detached := newPoolSafeContext(c, "logger", "context", "correlationId", "correlationIdHeader", "startTime")
w.asyncCount.Add(1)
go func() {
defer cancel()
defer w.asyncCount.Add(-1)
// Defense in depth: any panic that escapes the
// downstream chain (including future regressions of
// the pool-reuse bug) routes through handleError and
// leaves the process running.
defer func() {
r := recover()
if r == nil {
return
}
ctx.Log().Error(fmt.Sprintf("webhook goroutine panic: %v", r))
handleError(fmt.Errorf("internal error: %v", r))
}()
// Call the next middleware in the chain.
err := next(c)
err := next(detached)
if err != nil {
if errors.Is(err, api.ErrNoOutputFile) {
errNoOutputFile := fmt.Errorf("%w - the webhook middleware cannot handle the result of this route", err)