@@ -21,10 +21,7 @@ import (
2121// inviteFailure is the verdict for a failed outbound INVITE: how to record
2222// the call, how to bucket the SLI, and which error to surface back.
2323type inviteFailure struct {
24- status CallStatus
25- term stats.Termination
26- reason livekit.DisconnectReason
27- reportErr error // nil skips writing SIPCallInfo.Error
24+ EndCall
2825 returnErr error
2926}
3027
@@ -54,18 +51,20 @@ func (e SDPError) GRPCStatus() *status.Status {
5451
5552func (e SDPError ) ClassifyInvite () inviteFailure {
5653 res := inviteFailure {
57- status : callRejected ,
58- reason : livekit .DisconnectReason_MEDIA_FAILURE ,
59- reportErr : e .Err ,
54+ EndCall : EndCall {
55+ Status : callRejected ,
56+ Reason : livekit .DisconnectReason_MEDIA_FAILURE ,
57+ Report : e .Err ,
58+ },
6059 returnErr : e ,
6160 }
6261 switch {
6362 case errors .Is (e .Err , sdp .ErrNoCommonMedia ):
64- res .term = stats .ClientError ("no-common-codec" )
63+ res .Term = stats .ClientError ("no-common-codec" )
6564 case errors .Is (e .Err , sdp .ErrNoCommonCrypto ):
66- res .term = stats .ClientError ("encryption-required" )
65+ res .Term = stats .ClientError ("encryption-required" )
6766 default :
68- res .term = stats .ClientError ("sdp-error" )
67+ res .Term = stats .ClientError ("sdp-error" )
6968 }
7069 return res
7170}
@@ -89,10 +88,12 @@ func (e transactionTimeoutError) ClassifyInvite() inviteFailure {
8988 reason = "no-final-response"
9089 }
9190 return inviteFailure {
92- status : callUnavailable ,
93- term : stats .ClientError (reason ),
94- reason : livekit .DisconnectReason_SIP_TRUNK_FAILURE ,
95- reportErr : e , // keep so the customer sees their destination didn't complete
91+ EndCall : EndCall {
92+ Status : callUnavailable ,
93+ Term : stats .ClientError (reason ),
94+ Reason : livekit .DisconnectReason_SIP_TRUNK_FAILURE ,
95+ Report : e , // keep so the customer sees their destination didn't complete
96+ },
9697 returnErr : psrpc .NewError (psrpc .Canceled , e ),
9798 }
9899}
@@ -107,106 +108,108 @@ func classifyInviteError(err error) inviteFailure {
107108 }
108109
109110 res := inviteFailure {
110- status : callDropped ,
111- term : stats .ServerError ("invite-failed" ),
112- reason : livekit .DisconnectReason_UNKNOWN_REASON ,
113- reportErr : err ,
111+ EndCall : EndCall {
112+ Status : callDropped ,
113+ Term : stats .ServerError ("invite-failed" ),
114+ Reason : livekit .DisconnectReason_UNKNOWN_REASON ,
115+ Report : err ,
116+ },
114117 returnErr : err ,
115118 }
116119
117120 if sipStatus , ok := errors.AsType [* livekit.SIPStatus ](err ); ok {
118121 code := int (sipStatus .Code )
119122 switch code {
120123 case int (sip .StatusUnauthorized ), int (sip .StatusProxyAuthRequired ):
121- res .status , res .term , res .reason = callRejected , stats .ClientError ("auth-required" ), livekit .DisconnectReason_USER_REJECTED
122- res .reportErr = nil
124+ res .Status , res .Term , res .Reason = callRejected , stats .ClientError ("auth-required" ), livekit .DisconnectReason_USER_REJECTED
125+ res .Report = nil
123126 case int (sip .StatusForbidden ):
124- res .status , res .term , res .reason = callRejected , stats .ClientError ("forbidden" ), livekit .DisconnectReason_USER_REJECTED
125- res .reportErr = nil
127+ res .Status , res .Term , res .Reason = callRejected , stats .ClientError ("forbidden" ), livekit .DisconnectReason_USER_REJECTED
128+ res .Report = nil
126129 case int (sip .StatusNotFound ):
127- res .status , res .term , res .reason = callUnavailable , stats .ClientError ("not-found" ), livekit .DisconnectReason_USER_UNAVAILABLE
128- res .reportErr = nil
130+ res .Status , res .Term , res .Reason = callUnavailable , stats .ClientError ("not-found" ), livekit .DisconnectReason_USER_UNAVAILABLE
131+ res .Report = nil
129132 case int (sip .StatusRequestTimeout ):
130- res .status , res .term , res .reason = callUnavailable , stats .ClientError ("request-timeout" ), livekit .DisconnectReason_USER_UNAVAILABLE
131- res .reportErr = nil
133+ res .Status , res .Term , res .Reason = callUnavailable , stats .ClientError ("request-timeout" ), livekit .DisconnectReason_USER_UNAVAILABLE
134+ res .Report = nil
132135 case int (sip .StatusTemporarilyUnavailable ):
133- res .status , res .term , res .reason = callUnavailable , stats .ClientError ("unavailable" ), livekit .DisconnectReason_USER_UNAVAILABLE
134- res .reportErr = nil
136+ res .Status , res .Term , res .Reason = callUnavailable , stats .ClientError ("unavailable" ), livekit .DisconnectReason_USER_UNAVAILABLE
137+ res .Report = nil
135138 case int (sip .StatusBusyHere ):
136- res .status , res .term , res .reason = callRejected , stats .ClientError ("busy" ), livekit .DisconnectReason_USER_REJECTED
137- res .reportErr = nil
139+ res .Status , res .Term , res .Reason = callRejected , stats .ClientError ("busy" ), livekit .DisconnectReason_USER_REJECTED
140+ res .Report = nil
138141 case int (sip .StatusNotAcceptableHere ):
139- res .status , res .term , res .reason = callRejected , stats .ClientError ("not-acceptable" ), livekit .DisconnectReason_USER_REJECTED
140- res .reportErr = nil
142+ res .Status , res .Term , res .Reason = callRejected , stats .ClientError ("not-acceptable" ), livekit .DisconnectReason_USER_REJECTED
143+ res .Report = nil
141144 default :
142145 switch {
143146 case code >= 400 && code < 500 :
144- res .status , res .term , res .reason = callRejected , stats .ClientError (fmt .Sprintf ("client-error-%d" , code )), livekit .DisconnectReason_USER_UNAVAILABLE
145- res .reportErr = nil
147+ res .Status , res .Term , res .Reason = callRejected , stats .ClientError (fmt .Sprintf ("client-error-%d" , code )), livekit .DisconnectReason_USER_UNAVAILABLE
148+ res .Report = nil
146149 case code >= 500 && code < 600 :
147150 // Some upstreams (notably Twilio) return a 5xx when the customer's own trunk exceeds its configured CPS or
148151 // concurrent-call cap. That's a customer-side rate limit, not upstream infrastructure breakage, so it must not count
149152 // against the server-error SLI. Match on the response body — brittle, so kept narrow to the known phrases.
150153 body := strings .ToLower (sipStatus .GetStatus ())
151154 switch {
152155 case strings .Contains (body , "cps limit exceeded" ):
153- res .status , res .term , res .reason = callRejected , stats .ClientError ("cps-limit-exceeded" ), livekit .DisconnectReason_SIP_TRUNK_FAILURE
156+ res .Status , res .Term , res .Reason = callRejected , stats .ClientError ("cps-limit-exceeded" ), livekit .DisconnectReason_SIP_TRUNK_FAILURE
154157 // keep reportErr so the customer can see they hit their cap
155158 case strings .Contains (body , "concurrent call limit exceeded" ):
156- res .status , res .term , res .reason = callRejected , stats .ClientError ("concurrent-limit-exceeded" ), livekit .DisconnectReason_SIP_TRUNK_FAILURE
159+ res .Status , res .Term , res .Reason = callRejected , stats .ClientError ("concurrent-limit-exceeded" ), livekit .DisconnectReason_SIP_TRUNK_FAILURE
157160 // keep reportErr so the customer can see they hit their cap
158161 default :
159162 // Carrier-side 5xx; keep reportErr for the detail.
160- res .status , res .term , res .reason = callDropped , stats .UpstreamError (fmt .Sprintf ("upstream-server-error-%d" , code )), livekit .DisconnectReason_SIP_TRUNK_FAILURE
163+ res .Status , res .Term , res .Reason = callDropped , stats .UpstreamError (fmt .Sprintf ("upstream-server-error-%d" , code )), livekit .DisconnectReason_SIP_TRUNK_FAILURE
161164 }
162165 case code >= 600 && code < 700 :
163- res .status , res .term , res .reason = callRejected , stats .ClientError (fmt .Sprintf ("global-decline-%d" , code )), livekit .DisconnectReason_USER_REJECTED
164- res .reportErr = nil
166+ res .Status , res .Term , res .Reason = callRejected , stats .ClientError (fmt .Sprintf ("global-decline-%d" , code )), livekit .DisconnectReason_USER_REJECTED
167+ res .Report = nil
165168 }
166169 }
167170 return res
168171 }
169172
170173 if errors .Is (err , ErrSIPRequestTimeout ) {
171- res .status , res .term , res .reason = callUnavailable , stats .ClientError ("no-answer" ), livekit .DisconnectReason_USER_UNAVAILABLE
172- res .reportErr = nil
174+ res .Status , res .Term , res .Reason = callUnavailable , stats .ClientError ("no-answer" ), livekit .DisconnectReason_USER_UNAVAILABLE
175+ res .Report = nil
173176 return res
174177 }
175178
176179 // Context cancellation / deadline. Check before *net.OpError because an
177180 // op error can wrap a context error, and the context cause is more
178181 // informative.
179182 if errors .Is (err , context .DeadlineExceeded ) {
180- res .status , res .term , res .reason = callDropped , stats .ServerError ("deadline-exceeded" ), livekit .DisconnectReason_UNKNOWN_REASON
183+ res .Status , res .Term , res .Reason = callDropped , stats .ServerError ("deadline-exceeded" ), livekit .DisconnectReason_UNKNOWN_REASON
181184 res .returnErr = psrpc .NewError (psrpc .DeadlineExceeded , err )
182185 return res
183186 }
184187 if errors .Is (err , context .Canceled ) {
185- res .status , res .term , res .reason = callRejected , stats .ClientError ("canceled" ), livekit .DisconnectReason_USER_UNAVAILABLE
186- res .reportErr = nil
188+ res .Status , res .Term , res .Reason = callRejected , stats .ClientError ("canceled" ), livekit .DisconnectReason_USER_UNAVAILABLE
189+ res .Report = nil
187190 res .returnErr = psrpc .NewError (psrpc .Canceled , err )
188191 return res
189192 }
190193
191194 // Specific net error types before *net.OpError (which wraps them).
192195 if _ , ok := errors.AsType [* net.AddrError ](err ); ok {
193- res .status , res .term , res .reason = callDropped , stats .ClientError ("address-error" ), livekit .DisconnectReason_SIP_TRUNK_FAILURE
196+ res .Status , res .Term , res .Reason = callDropped , stats .ClientError ("address-error" ), livekit .DisconnectReason_SIP_TRUNK_FAILURE
194197 res .returnErr = psrpc .NewError (psrpc .InvalidArgument , err )
195198 return res
196199 }
197200 if _ , ok := errors.AsType [* net.DNSError ](err ); ok {
198- res .status , res .term , res .reason = callDropped , stats .ClientError ("dns-resolution" ), livekit .DisconnectReason_SIP_TRUNK_FAILURE
201+ res .Status , res .Term , res .Reason = callDropped , stats .ClientError ("dns-resolution" ), livekit .DisconnectReason_SIP_TRUNK_FAILURE
199202 res .returnErr = psrpc .NewError (psrpc .InvalidArgument , err )
200203 return res
201204 }
202205 if _ , ok := errors.AsType [* net.OpError ](err ); ok {
203- res .status , res .term , res .reason = callDropped , stats .ServerError ("network-error" ), livekit .DisconnectReason_SIP_TRUNK_FAILURE
206+ res .Status , res .Term , res .Reason = callDropped , stats .ServerError ("network-error" ), livekit .DisconnectReason_SIP_TRUNK_FAILURE
204207 res .returnErr = psrpc .NewError (psrpc .Unavailable , err )
205208 return res
206209 }
207210
208211 if errors .Is (err , ErrAuthMaxRetry ) || errors .Is (err , ErrAuthMissingCreds ) || errors .Is (err , ErrAuthNoHeader ) {
209- res .status , res .term , res .reason = callRejected , stats .ClientError ("auth-failed" ), livekit .DisconnectReason_USER_REJECTED
212+ res .Status , res .Term , res .Reason = callRejected , stats .ClientError ("auth-failed" ), livekit .DisconnectReason_USER_REJECTED
210213 // keep reportErr so the auth detail is recorded
211214 return res
212215 }
0 commit comments