Skip to content

fix(symfony): end sub-request spans in handle post-hook to prevent scope leak#526

Open
BTC-Tim wants to merge 1 commit intoopen-telemetry:mainfrom
BTC-Tim:fix/symfony-subrequest-scope-leak
Open

fix(symfony): end sub-request spans in handle post-hook to prevent scope leak#526
BTC-Tim wants to merge 1 commit intoopen-telemetry:mainfrom
BTC-Tim:fix/symfony-subrequest-scope-leak

Conversation

@BTC-Tim
Copy link
Copy Markdown

@BTC-Tim BTC-Tim commented Mar 19, 2026

Problem

When Symfony renders an error response, HttpKernel::handle() is called twice: once for the MAIN_REQUEST and once internally as a SUB_REQUEST to render the error page via ExceptionController. The handle post-hook returns early for both calls because $exception is null for a successfully-rendered error page, leaving two scopes on the OTEL context stack:

[top]    scope 1 → sub-request span  (KIND_INTERNAL, child of scope 2)
[bottom] scope 2 → main request span (KIND_SERVER, root, no parent)

When terminate() fires it pops only the topmost scope (scope 1, the sub-request span) and ends it. Scope 2 — the root KIND_SERVER span — is never popped, never ended, and never exported.

In practice: child spans (e.g. Doctrine queries) appear in the trace backend with rootServiceName: <root span not yet received> because the parent never arrives.

This is distinct from the exception case addressed in #317. The bug occurs on normal request handling whenever Symfony produces a non-2xx response (404, 403, 500, etc.).

Fixes open-telemetry/opentelemetry-php#1905

Solution

Detect SUB_REQUEST in the handle post-hook and end those spans immediately, since sub-requests never receive a terminate() call:

$type = $params[1] ?? HttpKernelInterface::MAIN_REQUEST;

// Main request with no exception: terminate() hook handles ending the span
if ($type === HttpKernelInterface::MAIN_REQUEST && null === $exception) {
    return;
}

$span = Span::fromContext($scope->context());
$scope->detach();

if (null !== $exception) { ... }

// Sub-requests don't get a terminate() call — end the span here
if ($type === HttpKernelInterface::SUB_REQUEST) {
    $span->end();
}

By the time terminate() fires, only scope 2 (the main request span) remains on the stack and is properly ended and exported.

Also wraps $prop->inject() in a try-catch in the terminate hook to ensure $span->end() is always reached if response propagation fails.

Tests

  • Updated test_http_kernel_handle_subrequest to remove the erroneous terminate() call — standalone sub-requests don't receive terminate() in real Symfony, and the span is now correctly exported from the handle post-hook directly.
  • Added test_main_request_span_is_exported_when_subrequest_occurs: verifies that after a MAIN_REQUEST + SUB_REQUEST sequence followed by terminate(), both spans are exported, the sub-request span is a child of the main span, and the main span is a root span.
  • Added test_subrequest_scope_does_not_leak_into_next_request: verifies that a subsequent request after a MAIN_REQUEST + SUB_REQUEST cycle starts with a clean context and produces an independent root span.

All 12 tests pass.

…ope leak

When Symfony renders an error response, HttpKernel::handle() is called twice:
once for the MAIN_REQUEST and once internally as a SUB_REQUEST (via
ExceptionController). The handle post-hook was returning early for both calls
because $exception is null for a successfully-rendered error page, leaving two
scopes on the OTEL context stack.

When terminate() fired it popped only the topmost scope (the sub-request span),
ending that one. The main request scope (the root KIND_SERVER span) was never
popped, never ended, and therefore never exported.

Fix: detect SUB_REQUEST in the handle post-hook and end the span immediately,
since sub-requests never receive a terminate() call. This ensures the context
stack contains only the main request scope when terminate() fires.

Also wrap $prop->inject() in a try-catch in the terminate hook to guarantee
$span->end() is always reached even if response propagation fails.

Fixes: open-telemetry/opentelemetry-php#1905
@BTC-Tim BTC-Tim requested a review from a team as a code owner March 19, 2026 10:11
@welcome
Copy link
Copy Markdown

welcome bot commented Mar 19, 2026

Thanks for opening your first pull request! If you haven't yet signed our Contributor License Agreement (CLA), then please do so that we can accept your contribution. A link should appear shortly in this PR if you have not already signed one.

@linux-foundation-easycla
Copy link
Copy Markdown

CLA Not Signed

@BTC-Tim
Copy link
Copy Markdown
Author

BTC-Tim commented Mar 19, 2026

Sorry, this CLA is new for me / our organization. Review of it is in progress, but might take a while. Feel free to close or otherwise change this PR.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[contrib-auto-symfony] SUB_REQUEST scope not cleaned up in handle post-hook, causing root HTTP spans to never be exported

1 participant