From 312f2efc81c843ebf1ba048594196241c8810988 Mon Sep 17 00:00:00 2001 From: DohanKim Date: Wed, 10 Jun 2026 17:31:40 +0900 Subject: [PATCH] fix: tolerate transport send errors when relaying TURN client messages handle_ex_turn_msg matches the transport send result with a bare ':ok =' in both the gathering-transaction and relay-candidate branches. When the underlying socket has already been closed (e.g. a late ex_turn timer/refresh message arriving for a relay candidate whose socket went away), the send returns {:error, :closed} and the whole ICEAgent crashes with a MatchError, taking the PeerConnection down with it: ** (MatchError) no match of right hand side value: {:error, :closed} lib/ex_ice/priv/ice_agent.ex:950: ExICE.Priv.ICEAgent.handle_ex_turn_msg/3 Observed in production (ex_ice 0.13.0 relaying via Cloudflare TURN, in an ex_webrtc/Membrane SFU where the agent crash cascaded into the media pipeline). Handle the error the way nearby send paths already do: log, then drop the gathering transaction / close the relay candidate gracefully. --- lib/ex_ice/priv/ice_agent.ex | 31 +++++++++++++++++++++++++++---- 1 file changed, 27 insertions(+), 4 deletions(-) diff --git a/lib/ex_ice/priv/ice_agent.ex b/lib/ex_ice/priv/ice_agent.ex index b4d0844..612e47e 100644 --- a/lib/ex_ice/priv/ice_agent.ex +++ b/lib/ex_ice/priv/ice_agent.ex @@ -954,8 +954,20 @@ defmodule ExICE.Priv.ICEAgent do {:send, dst, data, client} -> tr = %{tr | client: client} - :ok = ice_agent.transport_module.send(tr.socket, dst, data) - put_in(ice_agent.gathering_transactions[tr_id], tr) + + case ice_agent.transport_module.send(tr.socket, dst, data) do + :ok -> + put_in(ice_agent.gathering_transactions[tr_id], tr) + + {:error, reason} -> + Logger.debug(""" + Couldn't send TURN message for gathering transaction, reason: #{inspect(reason)}. \ + Dropping the transaction.\ + """) + + {_, ice_agent} = pop_in(ice_agent.gathering_transactions[tr_id]) + update_gathering_state(ice_agent) + end {:error, _reason, _client} -> {_, ice_agent} = pop_in(ice_agent.gathering_transactions[tr_id]) @@ -971,9 +983,20 @@ defmodule ExICE.Priv.ICEAgent do {:send, dst, data, client} -> cand = %{cand | client: client} ice_agent = put_in(ice_agent.local_cands[cand.base.id], cand) + # we can't use do_send here as it will try to create permission for the turn address - :ok = ice_agent.transport_module.send(cand.base.socket, dst, data) - ice_agent + case ice_agent.transport_module.send(cand.base.socket, dst, data) do + :ok -> + ice_agent + + {:error, reason} -> + Logger.debug(""" + Couldn't send TURN message on candidate: #{inspect(cand)}, reason: #{inspect(reason)}. \ + Closing candidate.\ + """) + + close_candidate(ice_agent, cand) + end {:permission_expired, _ip, client} -> cand = %{cand | client: client}