-
Notifications
You must be signed in to change notification settings - Fork 17
Description
SignalWire SDK Bug Report: Race Condition in handleCallDialEvents
Summary
Race condition in @signalwire/realtime-api SDK causes TypeError: Cannot read properties of undefined (reading 'setPayload') when calling.call.dial (answered) event arrives before calling.call.state event during voiceClient.dialSip() or voiceClient.dialPhone().
Affected Versions
@signalwire/[email protected]@signalwire/[email protected](verified same code)
Bug Location
File: dist/index.node.mjs (and dist/index.node.js)
Function: handleCallDialEvents (~line 3055)
Root Cause
The voiceCallDialWorker forks two watchers in parallel:
yield sagaEffects11.fork(callDialWatcher); // Handles calling.call.dial
yield sagaEffects11.fork(callStateWatcher); // Handles calling.call.statehandleCallStateEvents creates the Call instance if it doesn't exist (defensive):
let callInstance = get(payload.call_id);
if (!callInstance) {
callInstance = new Call({ voice, payload, listeners }); // Creates it
}
set(payload.call_id, callInstance);handleCallDialEvents does NOT create the Call instance (not defensive):
case "answered": {
const callInstance = get(payload.call.call_id); // Returns undefined if state event hasn't arrived
callInstance.setPayload(payload.call); // CRASH
voice.emit("dial.answered", callInstance);
return true;
}When SignalWire's backend sends calling.call.dial with dial_state: "answered" before calling.call.state, the instanceMap doesn't have the Call yet, causing the crash.
Error Output
TypeError: Cannot read properties of undefined (reading 'setPayload')
at handleCallDialEvents (file:///app/node_modules/@signalwire/realtime-api/dist/index.node.mjs:3065:20)
at callDialWatcher (file:///app/node_modules/@signalwire/realtime-api/dist/index.node.mjs:3495:26)
at callDialWatcher.next (<anonymous>)
at next (/app/node_modules/@redux-saga/core/dist/redux-saga-core.prod.cjs.js:1101:27)
...
The above error occurred in task callDialWatcher
created by voiceCallDialWorker
Tasks cancelled due to error:
callStateWatcher
Impact
- Promise hangs forever - The
dialSip()/dialPhone()promise never resolves or rejects - No cleanup - Error occurs inside saga, not propagated to user's catch block
- State corruption - If same customer calls again, old saga may resume with new call's events, causing bizarre behavior (duplicate handlers, wrong conference routing)
Reproduction
The bug is intermittent and timing-dependent. More likely to occur under:
- High server load
- Cloud environments (Cloud Run, AWS Lambda) with variable latency
- Rapid successive calls
Suggested Fix
Apply the same defensive pattern used in handleCallStateEvents:
// src/voice/workers/handlers/callDialEventsHandler.ts
function handleCallDialEvents(options) {
const { payload, instanceMap, voice } = options;
const { get, set } = instanceMap; // Add 'set'
switch (payload.dial_state) {
case "failed": {
voice.emit("dial.failed", payload);
return true;
}
case "answered": {
let callInstance = get(payload.call.call_id);
if (!callInstance) {
// Defensive: create Call instance if state event hasn't arrived yet
callInstance = new Call({ voice, payload: payload.call });
set(payload.call.call_id, callInstance);
}
callInstance.setPayload(payload.call);
voice.emit("dial.answered", callInstance);
return true;
}
default:
return false;
}
}Workaround
We are using patch-package to apply the fix locally:
patches/@signalwire+realtime-api+4.1.2.patch
Environment
- Node.js 20
- Google Cloud Run
- SignalWire Relay v4 with SIP and PSTN dialing
Note: This bug affects production call handling applications. A saga crash during call setup can leave calls in inconsistent states with no way to recover gracefully.