fix(webhook): detach context from request lifecycle to prevent cancellation in async mode

This commit is contained in:
Adam Romanek
2026-01-23 17:06:28 +01:00
committed by Julien Neuhart
parent e14cab0c8b
commit 463bb2f91c
2 changed files with 25 additions and 2 deletions

View File

@@ -27,7 +27,7 @@ func TestNewContext_Cancellation(t *testing.T) {
t.Fatalf("failed to close multipart writer: %v", err) t.Fatalf("failed to close multipart writer: %v", err)
} }
// Create a request with a cancellable context. // Create a request with a cancellable context.
reqCtx, cancelReq := context.WithCancel(context.Background()) reqCtx, cancelReq := context.WithCancel(context.Background())
req := httptest.NewRequest(http.MethodPost, "/", body).WithContext(reqCtx) req := httptest.NewRequest(http.MethodPost, "/", body).WithContext(reqCtx)
req.Header.Set("Content-Type", writer.FormDataContentType()) req.Header.Set("Content-Type", writer.FormDataContentType())
@@ -48,7 +48,7 @@ func TestNewContext_Cancellation(t *testing.T) {
} }
defer cancel() defer cancel()
// Verify initial state: context SHOULD NOT be done yet. // Verify initial state: context SHOULD NOT be done yet.
select { select {
case <-ctx.Done(): case <-ctx.Done():
t.Fatal("context should not be done immediately") t.Fatal("context should not be done immediately")

View File

@@ -272,6 +272,29 @@ func webhookMiddleware(w *Webhook) api.Middleware {
}) })
return c.NoContent(http.StatusNoContent) return c.NoContent(http.StatusNoContent)
} }
if deadline, ok := ctx.Deadline(); ok {
// Create a new context derived from Background (detached from Request)
// but with the same deadline as the original context.
detachedCtx, detachedCancel := context.WithDeadline(context.Background(), deadline)
// Replace the embedded context in the api.Context struct.
// The modules downstream will now use this detached context.
ctx.Context = detachedCtx
// We must wrap the cancel function.
// 1. detachedCancel() cleans up our new detached context.
// 2. originalCancel() (captured from c.Get("cancel")) cleans up the working directory.
originalCancel := cancel
cancel = func() {
detachedCancel()
originalCancel()
}
} else {
// Fallback if no deadline was set (rare, as newContext enforces it).
ctx.Context = context.Background()
}
// As a webhook URL has been given, we handle the request in a // As a webhook URL has been given, we handle the request in a
// goroutine and return immediately. // goroutine and return immediately.
w.asyncCount.Add(1) w.asyncCount.Add(1)