Skip to content

Commit 8b0423f

Browse files
TEL-1026: Utility for extracting transfer error details from error- #999 (#808)
1 parent f0fd85a commit 8b0423f

3 files changed

Lines changed: 76 additions & 16 deletions

File tree

‎livekit-api/livekit/api/sip_service.py‎

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -49,9 +49,10 @@
4949

5050

5151
def _as_sip_error(err: ServerError) -> ServerError:
52-
"""Surface a SIP dialing failure as a SipCallError so callers can branch on
53-
the SIP status; other failures (auth, validation) are returned unchanged."""
54-
if "sip_status_code" in err.metadata:
52+
"""Surface a SIP dialing or transfer failure as a SipCallError so callers can
53+
branch on the SIP status or the transfer reason; other failures (auth,
54+
validation) are returned unchanged."""
55+
if "sip_status_code" in err.metadata or "sip_transfer_reason" in err.metadata:
5556
return SipCallError.from_server_error(err)
5657
return err
5758

‎livekit-api/livekit/api/twirp_client.py‎

Lines changed: 33 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -87,10 +87,19 @@ def __str__(self) -> str:
8787
return result
8888

8989

90+
_SIP_META_KEYS = (
91+
"sip_status_code",
92+
"sip_status",
93+
"sip_transfer_reason",
94+
"error_details",
95+
)
96+
97+
9098
class SipCallError(ServerError):
9199
"""A :class:`ServerError` from a SIP dialing call (``create_sip_participant`` /
92-
``transfer_sip_participant``) that failed with a SIP response status. The SIP
93-
code and reason are exposed as properties; any other error metadata remains
100+
``transfer_sip_participant``) that failed with a SIP response status, or a
101+
transfer that failed for any other reason. The SIP code and reason, and the
102+
transfer reason, are exposed as properties; any other error metadata remains
94103
available via :attr:`metadata`."""
95104

96105
@property
@@ -109,25 +118,36 @@ def sip_status(self) -> Optional[str]:
109118
"""The SIP reason phrase of the failed call, e.g. "Busy Here"."""
110119
return self.metadata.get("sip_status")
111120

121+
@property
122+
def sip_transfer_reason(self) -> Optional[str]:
123+
"""Why a transfer failed, e.g. "STR_RINGING_TIMEOUT". Only set for
124+
``transfer_sip_participant``."""
125+
return self.metadata.get("sip_transfer_reason")
126+
127+
@property
128+
def sip_transfer_id(self) -> Optional[str]:
129+
"""The id of the failed transfer, for matching against SIP transfer logs."""
130+
return self.metadata.get("sip_transfer_id")
131+
112132
@classmethod
113133
def from_server_error(cls, err: ServerError) -> "SipCallError":
114134
return cls(err.code, err.message, status=err.status, metadata=err.metadata)
115135

116136
def __str__(self) -> str:
117137
code = self.metadata.get("sip_status_code")
118-
if code is None:
138+
transfer_reason = self.metadata.get("sip_transfer_reason")
139+
if code is None and transfer_reason is None:
119140
return super().__str__()
120141
# A clear, SIP-specific representation, including any extra metadata.
121-
reason = self.metadata.get("sip_status")
122-
result = f"SIP call failed: {code}"
123-
if reason:
124-
result += f" {reason}"
125-
result += f" ({self.code})"
126-
extra = {
127-
k: v
128-
for k, v in self.metadata.items()
129-
if k not in ("sip_status_code", "sip_status", "error_details")
130-
}
142+
parts = []
143+
if transfer_reason is not None:
144+
parts.append(transfer_reason)
145+
if code is not None:
146+
reason = self.metadata.get("sip_status")
147+
parts.append(f"{code} {reason}" if reason else str(code))
148+
what = "SIP transfer failed" if transfer_reason is not None else "SIP call failed"
149+
result = f"{what}: {', '.join(parts)} ({self.code})"
150+
extra = {k: v for k, v in self.metadata.items() if k not in _SIP_META_KEYS}
131151
if extra:
132152
result += " [" + ", ".join(f"{k}={v}" for k, v in extra.items()) + "]"
133153
return result

‎tests/api/test_livekitapi.py‎

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@
3838

3939
import livekit.api as api
4040
from livekit.api import SipCallError, ServerError
41+
from livekit.api.sip_service import _as_sip_error
4142
from livekit.protocol.rtc import SessionDescription
4243

4344
BASE = os.getenv("LK_TEST_SERVER_URL", "http://127.0.0.1:9999")
@@ -564,6 +565,44 @@ def test_sip_no_answer():
564565
assert err.sip_status_code == 408
565566

566567

568+
# -- transfer failures surface the reason -------------------------------------
569+
570+
571+
def _transfer_error(**metadata: str) -> ServerError:
572+
return _as_sip_error(
573+
ServerError("deadline_exceeded", "call transfer failed", status=408, metadata=metadata)
574+
)
575+
576+
577+
def test_sip_transfer_reason():
578+
err = _transfer_error(sip_transfer_reason="STR_RINGING_TIMEOUT", sip_transfer_id="STR_abc")
579+
assert isinstance(err, SipCallError)
580+
assert err.sip_transfer_reason == "STR_RINGING_TIMEOUT"
581+
assert err.sip_transfer_id == "STR_abc"
582+
# No SIP response was involved in this failure.
583+
assert err.sip_status_code is None
584+
assert "STR_RINGING_TIMEOUT" in str(err)
585+
assert "STR_abc" in str(err)
586+
587+
588+
def test_sip_transfer_rejected_reports_reason_and_status():
589+
err = _transfer_error(
590+
sip_transfer_reason="STR_REJECTED",
591+
sip_status_code="486",
592+
sip_status="Busy Here",
593+
)
594+
assert err.sip_transfer_reason == "STR_REJECTED"
595+
assert err.sip_status_code == 486
596+
assert err.sip_status == "Busy Here"
597+
assert "STR_REJECTED" in str(err)
598+
assert "486" in str(err) and "Busy Here" in str(err)
599+
600+
601+
def test_non_sip_error_is_unchanged():
602+
err = ServerError("unauthenticated", "bad token", status=401)
603+
assert _as_sip_error(err) is err
604+
605+
567606
# -- cross-cutting: client-side dial timeout ----------------------------------
568607

569608

0 commit comments

Comments
 (0)