diff --git a/pkg/modules/webhook/async_context.go b/pkg/modules/webhook/async_context.go index 97b9811..0dc355d 100644 --- a/pkg/modules/webhook/async_context.go +++ b/pkg/modules/webhook/async_context.go @@ -11,22 +11,30 @@ import ( // conversion deadline. // // Echo cancels the request context as soon as the synchronous handler returns -// [api.ErrAsyncProcess], which would abort the asynchronous work. Replacing the -// embedded context severs that cancellation. The returned cancel function -// cleans up both the detached context and the original working directory. +// [api.ErrAsyncProcess], which would abort the asynchronous work. Detaching via +// [context.WithoutCancel] severs that cancellation while keeping the context +// values, most importantly the active trace span, so downstream conversion and +// webhook spans stay in the caller's trace instead of starting a new one. +// [context.WithoutCancel] also drops the deadline, so it is re-layered. The +// returned cancel function cleans up both the detached context and the original +// working directory. func detachAsyncContext(ctx *api.Context, cancel context.CancelFunc) context.CancelFunc { - if deadline, ok := ctx.Deadline(); ok { - detachedCtx, detachedCancel := context.WithDeadline(context.Background(), deadline) - ctx.Context = detachedCtx + deadline, hasDeadline := ctx.Deadline() + base := context.WithoutCancel(ctx.Context) - originalCancel := cancel - return func() { - detachedCancel() - originalCancel() - } + var detachedCtx context.Context + var detachedCancel context.CancelFunc + if hasDeadline { + detachedCtx, detachedCancel = context.WithDeadline(base, deadline) + } else { + // Fallback if no deadline was set (rare, as newContext enforces it). + detachedCtx, detachedCancel = context.WithCancel(base) } + ctx.Context = detachedCtx - // Fallback if no deadline was set (rare, as newContext enforces it). - ctx.Context = context.Background() - return cancel + originalCancel := cancel + return func() { + detachedCancel() + originalCancel() + } } diff --git a/pkg/modules/webhook/async_context_test.go b/pkg/modules/webhook/async_context_test.go index 9c900c2..402e7a4 100644 --- a/pkg/modules/webhook/async_context_test.go +++ b/pkg/modules/webhook/async_context_test.go @@ -5,9 +5,37 @@ import ( "testing" "time" + "go.opentelemetry.io/otel/trace" + "github.com/gotenberg/gotenberg/v8/pkg/modules/api" ) +func TestDetachAsyncContext_PreservesTraceContext(t *testing.T) { + traceID, _ := trace.TraceIDFromHex("0123456789abcdef0123456789abcdef") + spanID, _ := trace.SpanIDFromHex("0123456789abcdef") + sc := trace.NewSpanContext(trace.SpanContextConfig{ + TraceID: traceID, + SpanID: spanID, + TraceFlags: trace.FlagsSampled, + Remote: true, + }) + + reqCtx, reqCancel := context.WithDeadline( + trace.ContextWithSpanContext(context.Background(), sc), + time.Now().Add(2*time.Hour), + ) + defer reqCancel() + + ctx := &api.Context{Context: reqCtx} + cancel := detachAsyncContext(ctx, func() {}) + defer cancel() + + got := trace.SpanContextFromContext(ctx.Context) + if got.TraceID() != sc.TraceID() { + t.Errorf("expected the detached context to keep trace id %s, got %s", sc.TraceID(), got.TraceID()) + } +} + func TestDetachAsyncContext_PreservesDeadline(t *testing.T) { deadline := time.Now().Add(2 * time.Hour) reqCtx, reqCancel := context.WithDeadline(context.Background(), deadline)