CallKit requestTransaction error code 2

Hello,

In production, a large number of users experience outgoing call reporting fails with the following error:

com.apple.CallKit.error.requesttransaction Code=2

The iOS version doesn't matter, errors are present in v15-26

Details

  • My CXProvider held as a global singleton, so it’s unlikely to be deinited.
  • There is no explicit call to CXProvider.invalidate() in the app.

If I manually invalidate the CXProvider, I observe the expected failure when trying to create an outgoing call (com.apple.CallKit.error.requesttransaction error 2).

However, If I recreate the CXProvider after the error, outgoing calls are reported correctly.

Many users trigger the providerDidReset delegate method (CXProviderDelegate) before this error.

According to the documentation, providerDidReset can be called by the system, and we are supposed to end all active calls, but the documentation doesn't suggest recreating the CXProvider.

Question

Should I recreate CXProvider after providerDidReset and forget about that, or could this error be caused by something else?

In production, a large number of users experience outgoing call reporting fails with the following error:

How many is a "large number"? The problem here is that what trigger this:

Many users trigger the providerDidReset delegate method (CXProviderDelegate) before this error.

...is an XPC connection failure with callservicesd, typically caused by callservicesd crashing. That's not common, so if your app is causing "frequent" resets, then that's an issue that might be worth looking into.

Moving to here:

com.apple.CallKit.error.requesttransaction Code=2

Error 2 is "CXErrorCodeRequestTransactionErrorUnknownCallProvider", which basically means callservicesd doesn't recognize your provider as "valid". Again, that's not a common failure, but it could happen if your existing provider didn't register properly if/when it reconnected.

That leads to here:

Should I recreate CXProvider after providerDidReset and forget about that, or could this error be caused by something else?

Yes, that's what I would do.

__
Kevin Elliott
DTS Engineer, CoreOS/Hardware

Dear Apple with regards to above - can you pls shed a light on connection between:

  • CXProvider delegate method providerDidReset
  • invalidate() method CXProvider

Questions (assuming provider and delegate are still alive and connected):

  1. if to call invalidate() then delegate gonna receive providerDidReset. Is it 100% always true?

  2. My current Xcode has line like: The provider must be invalidated before it is deallocated. So if system calls providerDidReset there is no need to call invalidate() on existing provider. Just deallocate existing and create a new one?

Dear Apple, with regards to the above - can you please shed light on the connection between:

CXProvider delegate method providerDidReset

This is called when the XPC connection between CXProvider and callservicesd is interrupted.

invalidate() method in CXProvider

Among other things, this breaks the XPC connection between CXProvider and callservicesd.

  1. If you call invalidate(), then the delegate will receive providerDidReset. Is it 100% always true?

When it comes to software engineering, I am violently allergic to the word "always". The system is big, complicated, and constantly evolving, which means there's a pretty big difference between:

  • "This is what the system normally does"

VS

  • "The system behaves this way under all circumstances today, in previous versions, and in all future versions"

Yes, I suspect providerDidReset generally does get called when you invalidate, both now and in the past. That's the strongest promise I'll make.

  1. My current Xcode has a line like: The provider must be invalidated before it is deallocated.

Yes, that's what the comment in the CXProvider.h says.

So if the system calls providerDidReset, there is no need to call invalidate() on the existing provider. Just deallocate the existing one and create a new one?

No. The header says "The provider must be invalidated before it is deallocated". That means you need to call "invalidate" before you drop your last reference to a CXProvider.

Note that the issue here isn't actually about what invalidate() does today or even whether that call is necessary. The issue here is about complying with the API contract. We told you that you MUST call invalidate before CXProvider will be deallocated. That means we're allowed to leak that CXProvider object if you fail to call invalidate on it. The fact that we may not happen to leak today doesn't mean we didn't leak in the past or that we won't leak in the future.

That leads to my question- what problem are you actually having? Why is it an issue to call invalidate?

__
Kevin Elliott
DTS Engineer, CoreOS/Hardware

It is rather rare to reproduce and explain bugs

Some users have remote logs for calls. Log for iOS 26.2.1 & identifier iPhone18,2

To understand:

  • there is no inital providerDidBegin for first CXProvider as logger misses early logs
  • if our logic triggers CXProvider invalidation so Provider to invalidate nil
  • if iOS triggers CXProvider invalidation so Provider to invalidate Optional(<CXXPCProvider: ...>)
  • func invalidateProvider(..) invalidates provider when either nil or equal to current provider
  • second call providerDidReset happens after invalidateProvider as delagate is not set to nil

Pls keep an eye on CXProvider 0x10e0463c0. A very first one

12:45:33_473-730:[CallsProviderDelegate.swift:47 -- providerDidReset(:)] providerDidReset <CXXPCProvider: 0x10e0463c0> 12:45:33_478-738:[CallsController.swift:307 -- invalidateProvider(:)] Provider to invalidate Optional(<CXXPCProvider: 0x10e0463c0>) 12:45:33_478-739:[CallsController.swift:308 -- invalidateProvider(:)] Current provider <CXXPCProvider: 0x10e0463c0> 12:45:33_488-742:[CallsProviderDelegate.swift:47 -- providerDidReset(:)] providerDidReset <CXXPCProvider: 0x10e0463c0> 12:45:33_495-754:[CallsProviderDelegate.swift:61 -- providerDidBegin(:)] DidBegin: <CXXPCProvider: 0x1261f3680> 12:45:33_497-755:[CallsProviderDelegate.swift:61 -- providerDidBegin(:)] DidBegin: <CXXPCProvider: 0x10e0463c0>

providerDidBegin is fired for for 0x10e0463c0

Here happens strange thing*

14:25:41_743-3257:[CallsController.swift:307 -- invalidateProvider(:)] Provider to invalidate nil 14:25:41_743-3258:[CallsController.swift:308 -- invalidateProvider(:)] Current provider <CXXPCProvider: 0x1261f3680> 14:25:41_744-3261:[CallsProviderDelegate.swift:47 -- providerDidReset(:)] providerDidReset <CXXPCProvider: 0x1261f3680> 14:25:41_749-3286:[CallsProviderDelegate.swift:61 -- providerDidBegin(:)] DidBegin: <CXXPCProvider: 0x1259d5fc0>

14:25:49_363-3362:[CallsController.swift:307 -- invalidateProvider(:)] Provider to invalidate nil 14:25:49_363-3363:[CallsController.swift:308 -- invalidateProvider(:)] Current provider <CXXPCProvider: 0x1259d5fc0> 14:25:49_365-3367:[CallsProviderDelegate.swift:47 -- providerDidReset(:)] providerDidReset <CXXPCProvider: 0x1259d5fc0> 14:25:49_369-3389:[CallsController.swift:307 -- invalidateProvider(:)] Provider to invalidate Optional(<CXXPCProvider: 0x1259d5fc0>) 14:25:49_369-3390:[CallsController.swift:308 -- invalidateProvider(:)] Current provider <CXXPCProvider: 0x1259d6480> 14:25:49_372-3393:[CallsProviderDelegate.swift:61 -- providerDidBegin(:)] DidBegin: <CXXPCProvider: 0x1259d6480>

14:26:33_094-3504:[CallsController.swift:307 -- invalidateProvider(:)] Provider to invalidate nil 14:26:33_094-3505:[CallsController.swift:308 -- invalidateProvider(:)] Current provider <CXXPCProvider: 0x1259d6480> 14:26:33_097-3511:[CallsProviderDelegate.swift:47 -- providerDidReset(:)] providerDidReset <CXXPCProvider: 0x1259d6480> 14:26:33_104-3535:[CallsProviderDelegate.swift:61 -- providerDidBegin(:)] DidBegin: <CXXPCProvider: 0x1259d5800>

17:09:47_668-4911:[CallsProviderDelegate.swift:47 -- providerDidReset(:)] providerDidReset <CXXPCProvider: 0x1259d5800> 17:09:47_676-4920:[CallsController.swift:307 -- invalidateProvider(:)] Provider to invalidate Optional(<CXXPCProvider: 0x1259d5800>) 17:09:47_676-4921:[CallsController.swift:308 -- invalidateProvider(:)] Current provider <CXXPCProvider: 0x1259d5800> 17:09:47_691-4925:[CallsProviderDelegate.swift:47 -- providerDidReset(:)] providerDidReset <CXXPCProvider: 0x10e0463c0> 17:09:47_691-4933:[CallsController.swift:307 -- invalidateProvider(:)] Provider to invalidate Optional(<CXXPCProvider: 0x10e0463c0>) 17:09:47_691-4934:[CallsController.swift:308 -- invalidateProvider(:)] Current provider <CXXPCProvider: 0x1258eab00> 17:09:47_698-4937:[CallsProviderDelegate.swift:47 -- providerDidReset(:)] providerDidReset <CXXPCProvider: 0x1259d5800> 17:09:47_699-4945:[CallsController.swift:307 -- invalidateProvider(:)] Provider to invalidate Optional(<CXXPCProvider: 0x1259d5800>) 17:09:47_699-4946:[CallsController.swift:308 -- invalidateProvider(:)] Current provider <CXXPCProvider: 0x1258eab00> 17:09:47_701-4949:[CallsProviderDelegate.swift:61 -- providerDidBegin(:)] DidBegin: <CXXPCProvider: 0x1258eab00>

0x10e0463c0 is still alive

19:44:21_051-5641:[CallsProviderDelegate.swift:47 -- providerDidReset(:)] providerDidReset <CXXPCProvider: 0x1258eab00> 19:44:21_059-5650:[CallsController.swift:307 -- invalidateProvider(:)] Provider to invalidate Optional(<CXXPCProvider: 0x1258eab00>) 19:44:21_059-5651:[CallsController.swift:308 -- invalidateProvider(:)] Current provider <CXXPCProvider: 0x1258eab00> 19:44:21_068-5655:[CallsProviderDelegate.swift:47 -- providerDidReset(:)] providerDidReset <CXXPCProvider: 0x10e0463c0> 19:44:21_069-5663:[CallsController.swift:307 -- invalidateProvider(:)] Provider to invalidate Optional(<CXXPCProvider: 0x10e0463c0>) 19:44:21_069-5664:[CallsController.swift:308 -- invalidateProvider(:)] Current provider <CXXPCProvider: 0x124e34240> 19:44:21_085-5667:[CallsProviderDelegate.swift:47 -- providerDidReset(:)] providerDidReset <CXXPCProvider: 0x1258eab00> 19:44:21_091-5675:[CallsController.swift:307 -- invalidateProvider(:)] Provider to invalidate Optional(<CXXPCProvider: 0x1258eab00>) 19:44:21_091-5676:[CallsController.swift:308 -- invalidateProvider(:)] Current provider <CXXPCProvider: 0x124e34240> 19:44:21_104-5679:[CallsProviderDelegate.swift:61 -- providerDidBegin(:)] DidBegin: <CXXPCProvider: 0x124e34240>

0x10e0463c0 is still alive

A very strange place* has log:

14:13:19_554-3215:[CallsController.swift:599 -- set(call:ended:)] Report call ended, reason: CXCallEndedReason(rawValue: 2)

Code

Logs show that CallKit didn't end it. Code fails to register new calls. Tries to reset CXProvider. While logs have line:

14:25:41_744-3264:[CallsProviderDelegate.swift:50 -- providerDidReset(_:)] ☎️ providerDidReset calls count 1

Count is

For all above:

  1. Such hard destiny for 0x10e0463c0 is observed first time. May be it is iOS bug. You can refer to retain cycles on our side but other providers seem to be retained/released properly. But may be any other explanations?

  2. Logic to end call in CallKit is based on reason. If reason then use reportCall(with, endedAt, reason:) otherwise CXEndCallAction is used. Strange pattern is observed not first time. iOS calls providerDidReset for a first CXProvider. And after a few calls func reportCall(with UUID: UUID, endedAt dateEnded: Date?, reason endedReason: CXCallEndedReason) does NOT end call.call.UUID is proper. Would be nice to read any explanation.

So, let me start by going back to what I said here:

...is an XPC connection failure with callservicesd, typically caused by callservicesd crashing. That's not common, so if your app is causing "frequent" resets, then that's an issue that might be worth looking into.

By definition, that's not a "normal" failure. Your app is basically being notified of a failure because it's not clear what else we can/should do. That also means I don't necessarily have a great "solution", certainly not one I can guarantee to be reliable.

That leads to here:

there is no initial providerDidBegin for first CXProvider as logger misses early logs

Do you have any sense about how "long" your app is typically able to stay active, both in terms of being awake/active and in absolute terms (wake or suspended)?

This primarily comes up with long-running kiosks apps, but one of the issues that can happen to long-running apps is that as they get better and better at working for very long periods of time (in kiosk apps, this is typically days/weeks of foreground time), the more likely they are to run into strange bugs/problems that would otherwise never occur.

Eventually, this VERY much reaches a point of diminishing returns, where pushing your app lifetime further "out" in time just means whatever bug you find will be that much harder to reproduce.

CallKit apps are generally short-lived enough that this isn't really an issue, but if your app IS ending up running for long periods of time, then you may want to consider putting code in place to periodically exit as an easy way to minimize these issues.

if our logic triggers CXProvider invalidation so Provider to invalidate nil

FYI, I would recommend destroying ALL of your call "infrastructure", not just your individual provider. Case in point, it's possible CXProvider can persist (see below) and you don't want to confuse whatever is acting as its delegate.

All of that leads to here:

12:45:33_473-730 ... 14:25:41_743-3257

That kind of time gap indicates that your app suspended, in which case I'd probably use the opportunity to simply exit() and reset the entire app. That is, at the point your app "realized" it was going to suspend post-providerDidReset, you can call "exit()" and terminate yourself. Your app will then be relaunched the next time your app receives a VoIP push.

This obviously isn't the most "elegant" solution, but the advantage is it basically removes the need to try and understand or adapt to whatever is happening to the larger system. Again, this is only happening because callservicesd has already crashed, something you really have no good way to predict or anticipate. If the user can't "see" your app, then you'll never know you exited and your future relaunch means you’re starting from a "clean" state at the point you actually have work to do.

The one thing I'd highlight here is that your primary goal here generally ISN'T "make things work". A provider failure has already destroyed your active call and it generally isn't possible to rebuild that call in a way that will feel/look right to the user. Your main goal here is actually making sure that your app doesn't STAY broken, which is why calling exit may be a better approach.

  1. Such hard destiny for 0x10e0463c0 is observed first time. Maybe it is iOS bug. You can refer to retain cycles on our side but other providers seem to be retained/released properly. But maybe any other explanations?

CXProvider is part of a cluster of classes which manage both its XPC connection to callservicesd and the local CXCall objects your app uses to manage its calls. Depending on the state of things when that failure occurs, it's possible that your local CXCalls may keep the provider active.

Notably, on this point:

  1. Logic to end call in CallKit is based on reason. If reason, then use reportCall(with: endedAt: reason:) otherwise CXEndCallAction is used. Strange pattern is observed not the first time. iOS calls providerDidReset for a first CXProvider. And after a few calls, func reportCall(with UUID: UUID, endedAt dateEnded: Date?, reason endedReason: CXCallEndedReason) does NOT end call.call.UUID is proper. Would be nice to read any explanation.

If callservicesd relaunched, then it doesn't know what calls you previously had active, which can basically leave your app tracking dead calls. You can try to end them yourself, but again, if you're in the background then I'm not sure calling exit() isn't the better option.

__
Kevin Elliott
DTS Engineer, CoreOS/Hardware

CallKit requestTransaction error code 2
 
 
Q