Skip to content

SDK-392 Fix Pending/Fulfill never resolving when auth retries exhausted - #1023

Merged
sumeruchat merged 1 commit into
masterfrom
fix/SDK-392-resolve-pending-on-auth-retry-exhaustion
Apr 2, 2026
Merged

SDK-392 Fix Pending/Fulfill never resolving when auth retries exhausted#1023
sumeruchat merged 1 commit into
masterfrom
fix/SDK-392-resolve-pending-on-auth-retry-exhaustion

Conversation

@sumeruchat

Copy link
Copy Markdown
Contributor

What

Fixes a bug where RequestProcessorUtil.sendRequest() leaves its result Fulfill permanently unresolved when a 401 JWT error triggers auth token retry and all retries are eventually exhausted. This causes any upstream caller chained on the Pending to hang indefinitely — including the IterableAPI.initialize2 callback used by the React Native SDK.

Reported by: CarGurus (SDK-392) — Iterable.initialize() promise hangs on React Native New Architecture.

Root Cause

When sendRequest() receives a 401 JWT error, it schedules an auth token refresh via AuthManager.scheduleAuthTokenRefreshTimer with a callback that calls attemptSend() to retry the request. The result Fulfill is only resolved inside attemptSend() on success or non-JWT error — it is never resolved in the 401-JWT branch itself.

The retry chain eventually terminates when AuthManager.requestNewAuthToken() detects retryCount >= maxRetry and returns early at line 45 — but it does so without invoking the onSuccess callback. This means:

  1. invokePendingCallbacks is never called
  2. The attemptSend callback queued by RequestProcessorUtil is orphaned
  3. The result Fulfill is never resolved (neither .resolve() nor .reject())
  4. The entire Pending chain upstream — fetcher.fetch()inAppManager.start()implementation.start()initialize2 callback — hangs forever

Similarly, scheduleAuthTokenRefreshTimer silently returns when shouldSkipTokenRefresh is true (auth paused), dropping the callback without invoking it.

Call chain (verified):

JS: Iterable.initialize()
  → IterableAPI.initialize2(callback:)
    → implementation.start().onSuccess { callback(true) }.onError { callback(false) }
      → inAppManager.start() → scheduleSync() → synchronize() → fetcher.fetch()
        → RequestProcessorUtil.sendRequest()
          → 401 JWT → scheduleAuthTokenRefreshTimer(successCallback: { attemptSend() })
            → timer fires → requestNewAuthToken()
              → retryCount >= maxRetry → returns early (onSuccess never called)
                → attemptSend never called → result Fulfill never resolved
                  → initialize2 callback never fires → JS promise hangs

Changes

  • AuthManager.requestNewAuthToken: When shouldPauseRetry returns true (retries exhausted or paused), invoke onSuccess?(nil) before returning so the caller chain is notified instead of silently abandoned.
  • AuthManager.scheduleAuthTokenRefreshTimer: When shouldSkipTokenRefresh returns true, invoke successCallback?(nil) before returning instead of dropping the callback.
  • RequestProcessorUtil.sendRequest: Check the token in the scheduleAuthTokenRefreshTimer callback — if nil (retries exhausted), call reportFailure to reject the result Fulfill instead of calling attemptSend() again.

Regression Risk Analysis

Low risk

  • Normal auth refresh flow (token obtained successfully): Unaffected. requestNewAuthToken only invokes onSuccess(nil) when shouldPauseRetry is true — the happy path where a valid token is returned goes through onAuthTokenReceivedinvokePendingCallbacks(with: token) as before. RequestProcessorUtil only calls reportFailure when token is nil.
  • Non-JWT errors (network failures, bad API key, etc.): Unaffected. These already go through reportFailure in RequestProcessorUtil.

Medium risk — review carefully

  • Auth retry behavior change: Previously, when retries were exhausted, the request would silently hang. Now it fails explicitly. Callers that were accidentally relying on the request "disappearing" (no success, no failure) will now receive a failure callback. This is the correct behavior but could surface previously-hidden auth configuration issues.
  • shouldSkipTokenRefresh path: When pauseAuthRetry is true and a non-scheduled refresh is requested, we now invoke the callback with nil. If any caller was scheduling a refresh while auth was paused and expecting it to simply be ignored (no callback), they will now receive a nil callback. Review callers of scheduleAuthTokenRefreshTimer to confirm this is safe.
  • scheduleAuthTokenRefreshTimer called from onAuthTokenReceived(nil) (line 204): When auth delegate returns nil, onAuthTokenReceived schedules another refresh with the same onSuccess. With this fix, if that refresh is skipped (e.g. retries paused), onSuccess(nil) is called, which propagates up to invokePendingCallbacks(nil)RequestProcessorUtil reports failure. Previously this would have silently stalled. Verify this matches expected behavior.

Not affected

  • Android SDK (completely separate codebase, sync init)
  • apply() method in RequestProcessorUtil (does not use scheduleAuthTokenRefreshTimer)
  • Normal in-app fetch without auth (no 401 JWT, no retry path)

Testing

How to test:

  1. Configure SDK with authDelegate that always returns nil (simulating token retrieval failure)
  2. Set a saved user email/userId in UserDefaults so auth is triggered during init
  3. Call IterableAPI.initialize2(callback:) and verify the callback fires with false after retries are exhausted (instead of hanging forever)
  4. Verify normal auth flow still works: configure authDelegate that returns a valid token, confirm requests succeed and callback fires with true

Edge cases:

  • Auth delegate returns nil intermittently (some retries succeed, some fail)
  • pauseAuthRetries(true) called during active retry cycle
  • Multiple concurrent requests hitting 401 simultaneously

When a 401 JWT error triggers auth token retry in RequestProcessorUtil,
the result Fulfill is never resolved if retries are eventually exhausted.
This causes upstream callers (e.g. the React Native SDK initialize
callback) to hang indefinitely.

Three changes:
- AuthManager.requestNewAuthToken: invoke onSuccess(nil) when
  shouldPauseRetry is true so pending callback chains are notified
- AuthManager.scheduleAuthTokenRefreshTimer: invoke successCallback(nil)
  when shouldSkipTokenRefresh is true instead of silently returning
- RequestProcessorUtil.sendRequest: check token in the retry callback
  and reportFailure when nil instead of calling attemptSend again

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@codecov

codecov Bot commented Apr 2, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 33.33333% with 12 lines in your changes missing coverage. Please review.
✅ Project coverage is 70.73%. Comparing base (864d2ba) to head (db854b5).
⚠️ Report is 1 commits behind head on master.

Files with missing lines Patch % Lines
swift-sdk/Internal/AuthManager.swift 20.00% 8 Missing ⚠️
swift-sdk/Internal/RequestProcessorUtil.swift 50.00% 4 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##           master    #1023      +/-   ##
==========================================
- Coverage   70.80%   70.73%   -0.08%     
==========================================
  Files         112      112              
  Lines        9201     9215      +14     
==========================================
+ Hits         6515     6518       +3     
- Misses       2686     2697      +11     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@franco-zalamena-iterable franco-zalamena-iterable left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a good patch for the issue. But i think we should tackle this problem in a more substantial way in the future, i think the caller should not need to handle null cases, it should be something that is dealt internally.

@sumeruchat
sumeruchat merged commit 83a279c into master Apr 2, 2026
14 of 15 checks passed
@sumeruchat
sumeruchat deleted the fix/SDK-392-resolve-pending-on-auth-retry-exhaustion branch April 2, 2026 12:46
@joaodordio
joaodordio restored the fix/SDK-392-resolve-pending-on-auth-retry-exhaustion branch April 2, 2026 14:46
@joaodordio
joaodordio deleted the fix/SDK-392-resolve-pending-on-auth-retry-exhaustion branch April 2, 2026 15:18
sumeruchat pushed a commit that referenced this pull request Apr 2, 2026
@joaodordio
joaodordio restored the fix/SDK-392-resolve-pending-on-auth-retry-exhaustion branch April 7, 2026 09:49
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants