Skip to content

Maintenance: track fetch subsegments per request instead of juggling the active-segment slot in instrumentFetch #5445

Description

@dreamorosi

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    internalPRs that introduce changes in governance, tech debt and chores (linting setup, baseline, etc.)

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions