Summary
The undici/fetch instrumentation in ProviderService.instrumentFetch() manages the lifecycle of HTTP subsegments by juggling the X-Ray SDK's active-segment slot: onRequestStart calls getSegment() -> addNewSubsegment() -> setSegment(subsegment), and onResponse/onError call getSegment() again - expecting to find that same subsegment - then close() and setSegment(subsegment.parent) to restore.
This only works if all three diagnostics-channel callbacks resolve the same context slot. That holds today because the SDK's Lambda mode has a single process-wide CLS context, but it silently breaks as soon as invocations get their own contexts: undici's request:headers/request:error events can fire on an async chain rooted in the connection (created by a previous invocation, carrying its context snapshot) rather than the request. During the investigation of #5434 we observed exactly this on a deployed Node.js 22 function using per-invocation contexts (captureAsyncFunc semantics) with keep-alive connections: onResponse resolved a different context, isHttpSubsegment() failed, the fetch subsegment was never closed nor its parent restored - the handler subsegment's open-children counter never reached zero, so the entire invocation's trace data was silently dropped (details).
Why is this needed?
The decorator path (captureAsyncFunc) already gives each invocation a nested CLS context today, so the decorator + fetch + connection-reuse combination is potentially exposed to the same failure under Lambda Managed Instances concurrency. It also becomes a hard blocker for adopting per-invocation context isolation when upstream lands (aws/aws-xray-sdk-node#760, middyjs/middy#1661): with the current slot-juggling, the fix for #5434 would trade wrong-attribution for dropped traces.
Fixing it is also independent of upstream: correlating each subsegment to its undici request object removes the dependency on context-slot stability entirely.
Which area does this relate to?
Tracer
Solution
In onRequestStart, associate the created subsegment with the undici request object (e.g. WeakMap<request, subsegment>) instead of parking it in the active-segment slot. onResponse/onError then retrieve it from the map - both callbacks already receive the request in the channel message - enrich, close, and delete the entry. The setSegment(subsegment)/setSegment(subsegment.parent) slot dance can likely be dropped entirely; if the active-subsegment behavior during the request is deemed part of the contract (e.g. for nested manual segments), it needs a separate look, but the close/enrich path must not depend on it.
Acknowledgment
Future readers
Please react with 👍 and your use case to help us understand customer demand.
Summary
The undici/fetch instrumentation in
ProviderService.instrumentFetch()manages the lifecycle of HTTP subsegments by juggling the X-Ray SDK's active-segment slot:onRequestStartcallsgetSegment()->addNewSubsegment()->setSegment(subsegment), andonResponse/onErrorcallgetSegment()again - expecting to find that same subsegment - thenclose()andsetSegment(subsegment.parent)to restore.This only works if all three diagnostics-channel callbacks resolve the same context slot. That holds today because the SDK's Lambda mode has a single process-wide CLS context, but it silently breaks as soon as invocations get their own contexts: undici's
request:headers/request:errorevents can fire on an async chain rooted in the connection (created by a previous invocation, carrying its context snapshot) rather than the request. During the investigation of #5434 we observed exactly this on a deployed Node.js 22 function using per-invocation contexts (captureAsyncFuncsemantics) with keep-alive connections:onResponseresolved a different context,isHttpSubsegment()failed, the fetch subsegment was never closed nor its parent restored - the handler subsegment's open-children counter never reached zero, so the entire invocation's trace data was silently dropped (details).Why is this needed?
The decorator path (
captureAsyncFunc) already gives each invocation a nested CLS context today, so the decorator +fetch+ connection-reuse combination is potentially exposed to the same failure under Lambda Managed Instances concurrency. It also becomes a hard blocker for adopting per-invocation context isolation when upstream lands (aws/aws-xray-sdk-node#760, middyjs/middy#1661): with the current slot-juggling, the fix for #5434 would trade wrong-attribution for dropped traces.Fixing it is also independent of upstream: correlating each subsegment to its undici
requestobject removes the dependency on context-slot stability entirely.Which area does this relate to?
Tracer
Solution
In
onRequestStart, associate the created subsegment with the undicirequestobject (e.g.WeakMap<request, subsegment>) instead of parking it in the active-segment slot.onResponse/onErrorthen retrieve it from the map - both callbacks already receive therequestin the channel message - enrich, close, and delete the entry. ThesetSegment(subsegment)/setSegment(subsegment.parent)slot dance can likely be dropped entirely; if the active-subsegment behavior during the request is deemed part of the contract (e.g. for nested manual segments), it needs a separate look, but the close/enrich path must not depend on it.Acknowledgment
Future readers
Please react with 👍 and your use case to help us understand customer demand.