From c6c3ba054e828c786dd6526fba06a1754c4412c8 Mon Sep 17 00:00:00 2001 From: Tanmay Deep Sharma Date: Tue, 21 Apr 2026 18:31:20 +0700 Subject: [PATCH] fix(whatsapp-call): stop browser from bypassing media server on inbound MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Inbound calls were reaching the media server, DTLS was completing after the DTLSRoleServer fix, and yet no media flowed — Meta still timed out at 20s. Tracing the session 182412 showed why: the browser uploaded a local .webm recording at t+22s. That only happens on the legacy browser-direct-to-Meta path, which means the browser negotiated WebRTC straight with Meta instead of waiting for the media server's agent offer. Root cause: GET /whatsapp_calls/:id returns Meta's sdp_offer whenever the call is ringing, regardless of media-server mode. The FE calls that endpoint from acceptWhatsappCallById when the call isn't in the incoming-calls store yet (page reload, bubble click). It then sees a truthy sdpOffer, isServerRelayCall() returns false, and the browser dials Meta itself. Meta's DTLS was with *our* media server, so the browser's peer connection never got media and Meta saw silence. Fix: - show omits sdp_offer / ice_servers when media_server_enabled and returns media_server_enabled as an explicit flag so the FE picks the right path. - broadcast_incoming_call carries the same flag through ActionCable. - FE isServerRelayCall prefers the explicit flag; only falls back to sdpOffer presence when the flag isn't provided. - acceptWhatsappCallById and the ActionCable handler propagate the flag into the call object. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../dashboard/composables/useWhatsappCallSession.js | 11 +++++++++-- app/javascript/dashboard/helper/actionCable.js | 1 + .../api/v1/accounts/whatsapp_calls_controller.rb | 10 ++++++++-- .../app/services/whatsapp/incoming_call_service.rb | 1 + 4 files changed, 19 insertions(+), 4 deletions(-) diff --git a/app/javascript/dashboard/composables/useWhatsappCallSession.js b/app/javascript/dashboard/composables/useWhatsappCallSession.js index 5ab0f60de..26b4c7144 100644 --- a/app/javascript/dashboard/composables/useWhatsappCallSession.js +++ b/app/javascript/dashboard/composables/useWhatsappCallSession.js @@ -143,9 +143,15 @@ function waitForIceGatheringComplete(pc) { }); } -// ── Server-relay mode: detect by absence of sdpOffer in incoming call data ── +// ── Server-relay mode detection ── +// Prefer the explicit flag set by Rails whenever it's available (call object +// from GET /whatsapp_calls/:id, inbox serializer, or the incoming ActionCable +// broadcast). Fall back to the presence of sdpOffer so legacy deployments keep +// working when the field isn't set. function isServerRelayCall(call) { - return !call.sdpOffer; + if (call?.mediaServerEnabled === true) return true; + if (call?.mediaServerEnabled === false) return false; + return !call?.sdpOffer; } /** @@ -290,6 +296,7 @@ export async function acceptWhatsappCallById(callId) { conversationId: data.conversation_id, sdpOffer: data.sdp_offer, iceServers: data.ice_servers, + mediaServerEnabled: data.media_server_enabled, caller: data.caller, }; callsStore.addIncomingCall(call); diff --git a/app/javascript/dashboard/helper/actionCable.js b/app/javascript/dashboard/helper/actionCable.js index b34c1cc5f..0393f6603 100644 --- a/app/javascript/dashboard/helper/actionCable.js +++ b/app/javascript/dashboard/helper/actionCable.js @@ -226,6 +226,7 @@ class ActionCableConnector extends BaseActionCableConnector { caller: data.caller, sdpOffer: data.sdp_offer || null, iceServers: data.ice_servers || null, + mediaServerEnabled: data.media_server_enabled, }); }; diff --git a/enterprise/app/controllers/api/v1/accounts/whatsapp_calls_controller.rb b/enterprise/app/controllers/api/v1/accounts/whatsapp_calls_controller.rb index a779cda97..dad947e88 100644 --- a/enterprise/app/controllers/api/v1/accounts/whatsapp_calls_controller.rb +++ b/enterprise/app/controllers/api/v1/accounts/whatsapp_calls_controller.rb @@ -14,8 +14,14 @@ class Api::V1::Accounts::WhatsappCallsController < Api::V1::Accounts::BaseContro conversation_id: @call.conversation_id, inbox_id: @call.inbox_id, message_id: @call.message_id, - sdp_offer: @call.ringing? ? @call.sdp_offer : nil, - ice_servers: @call.ice_servers, + # In server-relay mode the browser must not talk WebRTC to Meta directly + # — the media server owns that peer connection. Omitting sdp_offer forces + # the FE's isServerRelayCall() check to return true so the accept flow + # waits for the whatsapp_call.agent_offer broadcast from Rails instead of + # negotiating straight with Meta. + sdp_offer: @call.ringing? && !Call.media_server_enabled? ? @call.sdp_offer : nil, + ice_servers: Call.media_server_enabled? ? [] : @call.ice_servers, + media_server_enabled: Call.media_server_enabled?, caller: caller_info } end diff --git a/enterprise/app/services/whatsapp/incoming_call_service.rb b/enterprise/app/services/whatsapp/incoming_call_service.rb index ee498725c..12ef23fcd 100644 --- a/enterprise/app/services/whatsapp/incoming_call_service.rb +++ b/enterprise/app/services/whatsapp/incoming_call_service.rb @@ -157,6 +157,7 @@ class Whatsapp::IncomingCallService direction: call.direction_label, inbox_id: call.inbox_id, conversation_id: call.conversation_id, + media_server_enabled: Call.media_server_enabled?, caller: { name: contact.name, phone: contact.phone_number,