Skip to content

bug(ios): one transient CoreLocation error permanently kills location delivery — all later getCurrentPosition calls time out until app relaunch #91

Description

@berlansk

Bug description

On iOS, after one transient CoreLocation failure, every subsequent Geolocation.getCurrentPosition() call in the app session fails with OS-PLUG-GLOC-0010 (timeout) — even when the device gets a GPS fix — until the app process is killed and relaunched.

Root cause (traced through the plugin + IONGeolocationLib source)

  1. CLLocationManager.requestLocation() frequently fails with the transient kCLErrorLocationUnknown on a cold start / indoors with enableHighAccuracy: true. Apple's docs say this error should be ignored while the manager keeps trying.
  2. In IONGLOCManagerWrapper (ion-ios-geolocation), locationManager(_:didFailWithError:) sets currentLocation = nil, and the currentLocationPublisher pipeline turns that into a failure (tryMap throws IONGLOCLocationError.locationUnavailable).
  3. In GeolocationPlugin.swiftbindLocationPublisher(), that error is handled with:
    .catch { ... self?.callbackManager?.sendError(.positionUnavailable)
             return Empty<IONGLOCPositionModel, Never>().eraseToAnyPublisher() }
    Empty() completes the stream, so the Combine subscription (locationCancellable) is permanently finished.
  4. locationInitialized is never reset, so the guard in bindLocationPublisher() (guard !locationInitialized else { return }) prevents any re-subscription for the rest of the process lifetime.
  5. Net effect: every later getCurrentPosition() registers its callback and requestSingleLocation() runs — didUpdateLocations even fires with a valid fix — but there is no subscriber left to deliver it, so the only thing that ever resolves the call is the timeout timer (locationTimeoutPublisher has its own, still-alive subscription).

The only recovery paths are relaunching the app, or appDidBecomeActive — which re-binds only when watchCallbacks is non-empty, so plain getCurrentPosition() users never recover.

Steps to reproduce

  1. iOS device, app using @capacitor/geolocation 8.2.0, location permission granted.
  2. Trigger one transient CoreLocation failure (easiest: cold GPS start indoors with enableHighAccuracy: true and a short-ish timeout; first call after boot/standby often does it) → call rejects with OS-PLUG-GLOC-0002.
  3. Move somewhere with good GPS, call getCurrentPosition() repeatedly.
  4. Every call now rejects with OS-PLUG-GLOC-0010 ("Could not obtain location in time") regardless of signal, until the app is force-killed and reopened.

Expected behavior

  • A transient kCLErrorLocationUnknown should not permanently break location delivery — either filter it out (keep waiting, per Apple docs) or re-establish the subscription on the next request (e.g. reset locationInitialized in the .catch, or model errors as values so the stream never terminates).

Real-world impact

Field app for construction workers: mandatory-GPS clock-in. Workers with the app open since morning got "no GPS" on every attempt for minutes and only succeeded after force-killing the app. We shipped a workaround (cap the plugin timeout, then fall back to WKWebView navigator.geolocation, which works fine in these "broken" sessions since it bypasses the plugin), but the plugin itself stays unusable after the first transient error.

Plugin version

  • @capacitor/geolocation: 8.2.0 (also affects 7.x by code inspection — same architecture since the IONGeolocationLib rewrite)
  • Capacitor: 8.4.1
  • iOS (WKWebView, server.url remote)

Suggested fix

In bindLocationPublisher()'s .catch, reset locationInitialized = false (and drop locationCancellable) so the next request re-binds; or better, filter transient kCLErrorLocationUnknown in IONGLOCManagerWrapper.didFailWithError instead of nil-ing currentLocation, so the publisher never fails on a recoverable condition.

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