fix(whatsapp-call): normalize request shape for Go media-server

CreateSessionRequest rejected every Rails call with HTTP 400 — the Go
struct expects ice_servers[].urls as []string and account_id as string,
but Rails was sending urls as a single string and account_id as an
integer.

- MediaServerClient#create_session now wraps scalar `urls` in an array
  and coerces account_id to a string before posting. Defensive
  normalization lives in the client so existing Call rows (whose meta
  was persisted in the old shape) keep working without a backfill.
- Default ICE server literals in IncomingCallService#default_ice_servers
  and WhatsappCallsController#create_outbound_call_via_media_server
  updated to the correct `urls: [..]` shape going forward.

Verified end-to-end via rails runner against a locally running
media-server: create_session → generate_agent_offer → terminate all
return 2xx for both incoming (with Meta SDP offer) and outgoing
(server generates the offer for Meta) directions.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Tanmay Deep Sharma
2026-04-21 15:59:12 +07:00
co-authored by Claude Opus 4.7
parent b7d8fcc156
commit a31c91d371
3 changed files with 17 additions and 3 deletions
@@ -201,7 +201,7 @@ class Api::V1::Accounts::WhatsappCallsController < Api::V1::Accounts::BaseContro
call_id: "pending_#{SecureRandom.hex(8)}",
direction: 'outgoing',
sdp_offer: nil,
ice_servers: [{ urls: 'stun:stun.l.google.com:19302' }],
ice_servers: [{ urls: ['stun:stun.l.google.com:19302'] }],
account_id: current_account.id
)
@@ -238,7 +238,7 @@ class Whatsapp::IncomingCallService
end
def default_ice_servers
[{ urls: 'stun:stun.l.google.com:19302' }]
[{ urls: ['stun:stun.l.google.com:19302'] }]
end
def fix_sdp_setup(sdp)
@@ -5,7 +5,8 @@ class Whatsapp::MediaServerClient
TIMEOUT = 10
def create_session(call_id:, direction:, sdp_offer:, ice_servers:, account_id: nil)
body = { call_id: call_id, direction: direction, meta_sdp_offer: sdp_offer, ice_servers: ice_servers, account_id: account_id }.compact
body = { call_id: call_id, direction: direction, meta_sdp_offer: sdp_offer,
ice_servers: normalize_ice_servers(ice_servers), account_id: account_id&.to_s }.compact
post('/sessions', body)
end
@@ -110,4 +111,17 @@ class Whatsapp::MediaServerClient
'Authorization' => "Bearer #{auth_token}"
}
end
# Go media server expects `urls` to always be an array of strings.
# Accept legacy data that may have `urls` as a single string.
def normalize_ice_servers(servers)
return [] if servers.blank?
Array(servers).map do |srv|
s = srv.respond_to?(:to_h) ? srv.to_h.transform_keys(&:to_s) : srv.stringify_keys
urls = s['urls']
s['urls'] = urls.is_a?(Array) ? urls : Array(urls).compact
s
end
end
end