Skip to content

bug: iOS: ASWebAuthenticationSession is deallocated by ARC right after start(), causing a spurious cancel (error 1) on every call #694

Description

@kazis

Bug Report

Capacitor Version

💊   Capacitor Doctor  💊

Latest Dependencies:

  @capacitor/cli: 8.5.1
  @capacitor/core: 8.5.1
  @capacitor/android: 8.5.1
  @capacitor/ios: 8.5.1

Installed Dependencies:

  @capacitor/cli: 8.5.1
  @capacitor/core: 8.5.1
  @capacitor/android: 8.5.1
  @capacitor/ios: 8.5.1

[success] iOS looking great! 👌
[success] Android looking great! 👌

Plugin Version

App Name: Expresso
App ID: one.expresso.app
App Version: 1.0.0
Web Dir: dist
OS: darwin Darwin Kernel Version 25.6.0

Installed Dependencies:
  @capgo/cli: 8.50.3
  @capgo/capacitor-inappbrowser: 8.16.6
  @capgo/capacitor-native-navigation: 8.3.1
  @capgo/capacitor-navigation-bar: 8.2.7

Latest Dependencies:
  @capgo/cli: 8.50.3
  @capgo/capacitor-inappbrowser: 8.16.6
  @capgo/capacitor-native-navigation: 8.3.1
  @capgo/capacitor-navigation-bar: 8.2.7

✅ All dependencies are up to date

context(s)

ManualModel: false
AutoMode: false
CapgoCloud: false
OnPremise: false

Platform(s)

iOS (real device, iPhone 17 Pro, iOS 26). Not applicable to Android.

Current Behavior

Calling InAppBrowser.openSecureWindow(...) on iOS intermittently completes almost immediately with a rejection carrying ASWebAuthenticationSessionErrorCode.canceledLogin (error 1) or presentationContextInvalid (error 3), even though the user never touched or dismissed anything. This is most reliably reproduced right after a cold app launch, before the native view/window hierarchy has fully settled: session.start() returns true, the presentation window is valid, but the completion handler fires with an error roughly 100ms later — every single time on some launches.

Root cause: in openSecureWindow (InAppBrowserPlugin.swift), the ASWebAuthenticationSession instance is only referenced by a local let inside the closure that constructs it:

DispatchQueue.main.async {
    let session = ASWebAuthenticationSession(url: url, callbackURLScheme: callbackURLScheme) {
        callbackURL, error in
        // ...
    }
    session.prefersEphemeralWebBrowserSession = prefersEphemeral
    session.presentationContextProvider = self
    session.start()
}

session.start() is asynchronous and returns immediately. Nothing on self (the plugin instance) retains session after the enclosing closure returns, so ARC is free to deallocate it right away — sometimes before the sheet is even fully presented to the user.

Expected Behavior

openSecureWindow should only report a cancellation when the user actually dismisses the sheet or the request is genuinely invalid — not as a side effect of the session object being deallocated by ARC while still in use.

Retaining the session for the duration of its use fixes this. Add a class property, set it before start() is called, and clear it in the completion handler:

private var currentAuthSession: ASWebAuthenticationSession?

// ...
DispatchQueue.main.async {
    let session = ASWebAuthenticationSession(url: url, callbackURLScheme: callbackURLScheme) {
        [weak self] callbackURL, error in
        self?.currentAuthSession = nil
        // ... existing handling
    }
    session.prefersEphemeralWebBrowserSession = prefersEphemeral
    session.presentationContextProvider = self
    self.currentAuthSession = session   // must be set before start()
    session.start()
}

We applied exactly this fix locally via a pnpm patch on 8.16.6 and it fully resolved the spurious-cancel behavior on the real device — confirmed the same code path with NSLog instrumentation before and after: without the retain, the completion handler fires with an error ~100ms after start() returns on a cold launch; with it, the sheet presents normally and the completion handler only fires on a real user action or a real redirect. Checked the current main branch source on GitHub — the retain is still absent there as of this writing, so this is not yet fixed upstream.

Code Reproduction

Minimal repro shape (this is the exact call our app makes):

import { InAppBrowser } from '@capgo/capacitor-inappbrowser';

async function login() {
  const { redirectedUri } = await InAppBrowser.openSecureWindow({
    authEndpoint: 'https://your-oidc-provider.example/authorize?...',
    redirectUri: 'yourapp://callback',
  });
}

Call login() as early as possible after a cold app launch (e.g. from an app-level auto-login effect that fires on mount, before any user interaction) on a real iOS device. It is timing-dependent — it does not reproduce on every single launch, but reproduces often enough in practice that it broke our login flow's cold-boot path outright until we patched it.

Other Technical Details

npm --version output: 11.17.0

node --version output: v24.13.0

pod --version output (iOS issues only): 1.17.0

Additional Context

We are happy to open a PR with the fix above (already validated against 8.16.6 via pnpm patch) if that is useful — just let us know.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions