From 463bb2f91c2ff6b54f551b54524f9044286e798c Mon Sep 17 00:00:00 2001 From: Adam Romanek Date: Fri, 23 Jan 2026 17:06:28 +0100 Subject: [PATCH] fix(webhook): detach context from request lifecycle to prevent cancellation in async mode --- pkg/modules/api/context_test.go | 4 ++-- pkg/modules/webhook/middleware.go | 23 +++++++++++++++++++++++ 2 files changed, 25 insertions(+), 2 deletions(-) diff --git a/pkg/modules/api/context_test.go b/pkg/modules/api/context_test.go index 1209f31c..c0d33049 100644 --- a/pkg/modules/api/context_test.go +++ b/pkg/modules/api/context_test.go @@ -27,7 +27,7 @@ func TestNewContext_Cancellation(t *testing.T) { 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()) req := httptest.NewRequest(http.MethodPost, "/", body).WithContext(reqCtx) req.Header.Set("Content-Type", writer.FormDataContentType()) @@ -48,7 +48,7 @@ func TestNewContext_Cancellation(t *testing.T) { } defer cancel() - // Verify initial state: context SHOULD NOT be done yet. + // Verify initial state: context SHOULD NOT be done yet. select { case <-ctx.Done(): t.Fatal("context should not be done immediately") diff --git a/pkg/modules/webhook/middleware.go b/pkg/modules/webhook/middleware.go index f5d870d3..8771f042 100644 --- a/pkg/modules/webhook/middleware.go +++ b/pkg/modules/webhook/middleware.go @@ -272,6 +272,29 @@ func webhookMiddleware(w *Webhook) api.Middleware { }) 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 // goroutine and return immediately. w.asyncCount.Add(1)