AI & Agent Dev Bug Sandbox logo
AI & Agent Dev Bug Sandbox
Back to Radar

Telemetry _Controller Is Always Undefined Due To Promise Identity Loss In Record(), So FlushDetached'S Abort Is A Silent No-Op

In Telemetry.record(), the promise stored in this.queue is the result of chaining .then().catch().then() on the promise returned by submitRecord(). Because .then()/.catch() return new Promise objects and do not copy custom properties, the final promise never has the _controller property that submitRecord() attached to its returned promise. The line (prom as any)._controller = (prom as any)._controller is a no-op. Consequently, flushDetached() always sees item._controller as undefined, making the abort() call a silent no-op.

mediumConfidence 95%Next.jsAffected V16.3.0-Canary.97

Origin Analysis

In record(), the code assigns _controller to itself after the promise has been chained. The chained promise is a different object from the original promise returned by submitRecord(), which is the only object that actually has _controller set. Thus the controller reference is lost.
1. Run next dev with telemetry enabled. 2. Call telemetry.record(event) and inspect the returned promise's _controller property. 3. Observe that _controller is always undefined, because the returned promise is the chained promise, not the original submitRecord() promise.

Fixing Code Block

record = (_events, deferred) => { const sourcePromise = deferred ? Promise.resolve() : this.submitRecord(_events); const prom = sourcePromise .then((value) => ({ isFulfilled: true, isRejected: false, value })) .catch((reason) => ({ isFulfilled: false, isRejected: true, reason })) .then((res) => { if (!deferred) { this.queue.delete(prom); } return res; }); (prom as any)._events = Array.isArray(_events) ? _events : [_events]; (prom as any)._controller = (sourcePromise as any)._controller; this.queue.add(prom); return prom; };
Store the original promise returned by submitRecord() in a local variable sourcePromise before chaining. After the chained promise is created, copy the _controller property from sourcePromise to the final promise. This ensures that the promise stored in this.queue retains the controller reference, so flushDetached() can abort in-flight requests.

Edge Case Audit

This fix assumes that submitRecord() always attaches _controller to its returned promise for non-deferred records. In deferred mode, sourcePromise is Promise.resolve() and has no _controller, so the final promise will also have undefined _controller, which is safe but means no abort for deferred events. The change is minimal and synchronous; there is no concurrency risk. Rollback: revert to the original code if unexpected behavior occurs. Note that if submitRecord() is later refactored to not expose _controller directly, this copying will still yield undefined but will not break anything.

Ecosystem Topology