Skip to content

fix(ios): MM-70605 give each recreated session its own delegate in addSessionHeaders - #175

Merged
fmartingr merged 1 commit into
masterfrom
fix/ios-add-headers-shared-delegate
Oct 19, 2026
Merged

fmartingr merged 1 commit into
masterfrom
fix/ios-add-headers-shared-delegate

Conversation

@fmartingr

@fmartingr fmartingr commented Oct 17, 2026 •

Copy link
Copy Markdown
Contributor

Summary

SessionManager.addSessionHeaders crashes the iOS app with SIGTRAP when the old session's invalidation callback arrives after the new session exists.

  • addSessionHeaders invalidates the old Alamofire Session and creates a new one with the same SessionDelegate.
  • Session.init sets delegate.stateProvider to the new Session, and the new Session gets its own serial root queue that targets the old one.
  • The old URLSession then calls didBecomeInvalidWithError on the old root queue. The shared delegate forwards it to the new Session, and cancelRequestsForSessionInvalidation fails dispatchPrecondition(condition: .onQueue(rootQueue)).

The fix gives the recreated session a new ApiClientSessionDelegate. That class holds no state (it looks sessions up through SessionManager.default), so a new instance behaves the same. The old delegate keeps a weak reference to the old Session only.

Stack (Detox iOS E2E in mattermost-mobile#10149, and a local reproduction):

_dispatch_assert_queue_fail
_dispatchPreconditionTest(_:)
Alamofire.Session.cancelRequestsForSessionInvalidation(with:)   Session.swift:1453
Alamofire.SessionDelegate.urlSession(_:didBecomeInvalidWithError:)   SessionDelegate.swift:76
__NSCFURLSessionDelegateWrapper didBecomeInvalidWithError:

Testing

iOS 26.5 simulator, Release build of mattermost-mobile with this change applied via patch-package. A local server requires the X-Mattermost-Preauth-Secret header. Edit Server → change the pre-auth secret calls addHeaders.

Build Saves Crashes
Without fix, no delay 36 0 (timing-dependent)
Without fix, 0.5 s block on the old root queue before invalidateSession 3 3
With fix, same 0.5 s block 13 0
With fix, no delay 34 0

With the fix, the server log shows every request switch to the new header value after each save.

Android is not affected: OkHttp adds headers without recreating the session.

Ticket Link

https://mattermost.atlassian.net/browse/MM-70605

Change Impact: 🟡 Medium

Changes Breakdown:

+3 -1 Production Changes (100%)
+0 -0 Test Changes (0%)

Regression Risk: The change is limited to session recreation in SessionManager.addSessionHeaders, but it affects iOS session management. Test coverage for this path is not reported.

Generated by CodeRabbitAI

…dSessionHeaders

addSessionHeaders invalidated the old Alamofire Session and created the new one with the same SessionDelegate. Session.init points the delegate's stateProvider at the new Session, so the old URLSession's didBecomeInvalidWithError reached the new Session from the old root queue and failed the dispatchPrecondition in cancelRequestsForSessionInvalidation (SIGTRAP). ApiClientSessionDelegate holds no state, so a fresh instance behaves the same.
@fmartingr
fmartingr marked this pull request as ready for review October 17, 2026 16:14
fmartingr added a commit to mattermost/mattermost-mobile that referenced this pull request Oct 17, 2026
Temporary pin to mattermost/react-native-network-client#175 to check CI. Replace with the released version before merge.
@coderabbitai

coderabbitai Bot commented Oct 17, 2026

Copy link
Copy Markdown

Review in Change Stack →

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration
  • Configuration used: Organization UI
  • Review profile: CHILL
  • Plan: Essentials
  • Run ID: caab2617-5b57-454e-9e11-3dde89c91f44
📥 Commits

Reviewing files that changed from the base of the PR and between ba64ea4 and adff5a4.

📒 Files selected for processing: 1
  • ios/SessionManager.swift

Included review availability: This review used your included allowance. 4 included reviews remain after this review. Your included PR review attempts over the past 7 days set your current allowance at 5 reviews per hour.

📝 Walkthrough

addSessionHeaders now creates a new ApiClientSessionDelegate when it rebuilds a session after adding headers.

Changes: iOS session recreation

Layer / File(s) Summary
Refresh the session delegate
ios/SessionManager.swift
addSessionHeaders assigns a new ApiClientSessionDelegate to the replacement session instead of reusing the previous session’s delegate.

Priority: ➖ Normal

Estimated code review effort: 1 (Trivial) | ~3 minutes

Change: Bug fix

Merge Risk: ⚪ Minimal · up to adff5

A delayed invalidation callback remains associated with the old session instead of acting through its replacement. No unresolved product-facing risk is established.

Pre-merge checks: Passed 4 · Failed 1

Failed Failed checks: 1 warning

Check name Status Explanation Resolution
Docstring Coverage Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 1 functions across 1 files. Write docstrings for the functions missing them to satisfy the coverage threshold.
Passed Passed checks: 4 passed
Check name Status Explanation
Description Check Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check Passed The title clearly identifies the iOS fix and the main change: each recreated session receives its own delegate in addSessionHeaders.
Linked Issues check Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check Passed Check skipped because no linked issues were found for this pull request.
  • Fix all pre-merge checks with AI
✨ Finishing Touches: 💡 1
📝 Generate docstrings: 💡
  • Commit to this branch
  • Create a new PR
🧪 Generate unit tests (beta)
  • Commit to this branch
  • Create a new PR
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Comment @coderabbitai help to get the list of available commands.

@fmartingr
fmartingr requested a review from carlisgg October 17, 2026 16:22

@carlisgg carlisgg left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

lgtm

@fmartingr
fmartingr merged commit 9b9cbaa into master Oct 19, 2026
5 checks passed
@fmartingr
fmartingr deleted the fix/ios-add-headers-shared-delegate branch October 19, 2026 11:02
fmartingr added a commit to mattermost/mattermost-mobile that referenced this pull request Oct 23, 2026
* [MM-70605][MM-70607] Scope pre-auth secret storage per server

The pre-auth secret was written with react-native-keychain's generic-password
API using the `server` option, which those calls do not read. Every server
therefore resolved to one shared entry, and a falsy secret argument deleted it,
so ordinary operations such as building a client, refreshing the token header or
removing another server discarded the stored value.

Key the entry per server instead. On Android the alias namespace is shared with
internet credentials, so the service is prefixed to keep it away from the
session token; iOS uses the bare server URL, which is what the native reader in
Gekidou already queries. The account value is renamed to `preauth_secret` on
both sides. Writing the token no longer touches the secret: removal is explicit,
via removePreauthSecret.

A one-time migration adopts the previously shared entry when a single server
makes ownership unambiguous, and discards it otherwise. It only deletes the old
entry once the new one is confirmed stored, and defers when no server is active.

Also forward the secret through the login screen to SSO, and fix loginOptions to
count only enabled providers so the single-provider redirect works. Relands the
`.enabled` filter from #9585 by @pltnm239.

Edit Server now updates the live client's headers rather than invalidating the
session, and validates with the candidate secret so a value the server rejects
cannot be saved.

* [MM-70605] Harden preauth secret flows and fix Edit Server Detox on iOS

Address review feedback around migration markers, credential errors, and
keeping re-login secrets out of nav params. Focus the empty Authentication
secret field via Display Name return so iOS Detox can type into it.

Co-authored-by: Cursor <cursoragent@cursor.com>

* [MM-70605] Fix lines-around-comment in Edit Server Detox helper

Co-authored-by: Cursor <cursoragent@cursor.com>

* [MM-70605] Roll back preauth keychain when Edit Server client sync fails

After the secret is stored, header apply or createClient failures left the
new value in secure storage while UI state still held the old baseline.
Restore the previous secret (and live header) before surfacing the error.

Co-authored-by: Cursor <cursoragent@cursor.com>

* [MM-70605] Preserve exact preauth secret on Edit Server rollback

Avoid trimming during keychain restore so boundary whitespace from the
previously stored value is not altered.

Co-authored-by: Cursor <cursoragent@cursor.com>

* [MM-70605] Harden preauth migration ownership and secret handoff

Adopt the legacy secret only when a single server exists in the app DB,
keep add-server candidates in EphemeralStore instead of nav params, and
resync the live client to stored credentials when keychain rollback fails.

Co-authored-by: Cursor <cursoragent@cursor.com>

* [MM-70605] Discard the legacy preauth secret when no server can own it

* [MM-70605] Shorten the preauth migration docblock

* [MM-70605] Skip the Authorization header when there is no bearer token

* [MM-70605] Surface preauth secret rejections with a specific error message

* [MM-70605] Mock clearPendingPreauthSecret in session action tests

* [MM-70605] Remove the unreachable catch from restorePreviousPreauthSecret

setPreauthSecret and removePreauthSecret never reject, and the returned promise was not awaited, so the catch could not run.

* [MM-70605] Use network-client fix for the iOS addHeaders crash

Temporary pin to mattermost/react-native-network-client#175 to check CI. Replace with the released version before merge.

* [MM-70605] Bump react-native-network-client to 1.11.5

1.11.5 gives each recreated iOS session its own delegate in addHeaders, which fixes the SIGTRAP on pre-auth secret changes.

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
ux-builder pushed a commit to okrbest/okrbest-mobile that referenced this pull request Oct 27, 2026
* [MM-70605][MM-70607] Scope pre-auth secret storage per server

The pre-auth secret was written with react-native-keychain's generic-password
API using the `server` option, which those calls do not read. Every server
therefore resolved to one shared entry, and a falsy secret argument deleted it,
so ordinary operations such as building a client, refreshing the token header or
removing another server discarded the stored value.

Key the entry per server instead. On Android the alias namespace is shared with
internet credentials, so the service is prefixed to keep it away from the
session token; iOS uses the bare server URL, which is what the native reader in
Gekidou already queries. The account value is renamed to `preauth_secret` on
both sides. Writing the token no longer touches the secret: removal is explicit,
via removePreauthSecret.

A one-time migration adopts the previously shared entry when a single server
makes ownership unambiguous, and discards it otherwise. It only deletes the old
entry once the new one is confirmed stored, and defers when no server is active.

Also forward the secret through the login screen to SSO, and fix loginOptions to
count only enabled providers so the single-provider redirect works. Relands the
`.enabled` filter from #9585 by @pltnm239.

Edit Server now updates the live client's headers rather than invalidating the
session, and validates with the candidate secret so a value the server rejects
cannot be saved.

* [MM-70605] Harden preauth secret flows and fix Edit Server Detox on iOS

Address review feedback around migration markers, credential errors, and
keeping re-login secrets out of nav params. Focus the empty Authentication
secret field via Display Name return so iOS Detox can type into it.

Co-authored-by: Cursor <cursoragent@cursor.com>

* [MM-70605] Fix lines-around-comment in Edit Server Detox helper

Co-authored-by: Cursor <cursoragent@cursor.com>

* [MM-70605] Roll back preauth keychain when Edit Server client sync fails

After the secret is stored, header apply or createClient failures left the
new value in secure storage while UI state still held the old baseline.
Restore the previous secret (and live header) before surfacing the error.

Co-authored-by: Cursor <cursoragent@cursor.com>

* [MM-70605] Preserve exact preauth secret on Edit Server rollback

Avoid trimming during keychain restore so boundary whitespace from the
previously stored value is not altered.

Co-authored-by: Cursor <cursoragent@cursor.com>

* [MM-70605] Harden preauth migration ownership and secret handoff

Adopt the legacy secret only when a single server exists in the app DB,
keep add-server candidates in EphemeralStore instead of nav params, and
resync the live client to stored credentials when keychain rollback fails.

Co-authored-by: Cursor <cursoragent@cursor.com>

* [MM-70605] Discard the legacy preauth secret when no server can own it

* [MM-70605] Shorten the preauth migration docblock

* [MM-70605] Skip the Authorization header when there is no bearer token

* [MM-70605] Surface preauth secret rejections with a specific error message

* [MM-70605] Mock clearPendingPreauthSecret in session action tests

* [MM-70605] Remove the unreachable catch from restorePreviousPreauthSecret

setPreauthSecret and removePreauthSecret never reject, and the returned promise was not awaited, so the catch could not run.

* [MM-70605] Use network-client fix for the iOS addHeaders crash

Temporary pin to mattermost/react-native-network-client#175 to check CI. Replace with the released version before merge.

* [MM-70605] Bump react-native-network-client to 1.11.5

1.11.5 gives each recreated iOS session its own delegate in addHeaders, which fixes the SIGTRAP on pre-auth secret changes.

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
(cherry picked from commit 9ccfa0e)

Upstream: mattermost/mattermost-mobile@9ccfa0e
amyblais added a commit to mattermost/mattermost-mobile that referenced this pull request Oct 27, 2026
#10278)

* [MM-70605][MM-70607] Scope pre-auth secret storage per server

The pre-auth secret was written with react-native-keychain's generic-password
API using the `server` option, which those calls do not read. Every server
therefore resolved to one shared entry, and a falsy secret argument deleted it,
so ordinary operations such as building a client, refreshing the token header or
removing another server discarded the stored value.

Key the entry per server instead. On Android the alias namespace is shared with
internet credentials, so the service is prefixed to keep it away from the
session token; iOS uses the bare server URL, which is what the native reader in
Gekidou already queries. The account value is renamed to `preauth_secret` on
both sides. Writing the token no longer touches the secret: removal is explicit,
via removePreauthSecret.

A one-time migration adopts the previously shared entry when a single server
makes ownership unambiguous, and discards it otherwise. It only deletes the old
entry once the new one is confirmed stored, and defers when no server is active.

Also forward the secret through the login screen to SSO, and fix loginOptions to
count only enabled providers so the single-provider redirect works. Relands the
`.enabled` filter from #9585 by @pltnm239.

Edit Server now updates the live client's headers rather than invalidating the
session, and validates with the candidate secret so a value the server rejects
cannot be saved.

* [MM-70605] Harden preauth secret flows and fix Edit Server Detox on iOS

Address review feedback around migration markers, credential errors, and
keeping re-login secrets out of nav params. Focus the empty Authentication
secret field via Display Name return so iOS Detox can type into it.



* [MM-70605] Fix lines-around-comment in Edit Server Detox helper



* [MM-70605] Roll back preauth keychain when Edit Server client sync fails

After the secret is stored, header apply or createClient failures left the
new value in secure storage while UI state still held the old baseline.
Restore the previous secret (and live header) before surfacing the error.



* [MM-70605] Preserve exact preauth secret on Edit Server rollback

Avoid trimming during keychain restore so boundary whitespace from the
previously stored value is not altered.



* [MM-70605] Harden preauth migration ownership and secret handoff

Adopt the legacy secret only when a single server exists in the app DB,
keep add-server candidates in EphemeralStore instead of nav params, and
resync the live client to stored credentials when keychain rollback fails.



* [MM-70605] Discard the legacy preauth secret when no server can own it

* [MM-70605] Shorten the preauth migration docblock

* [MM-70605] Skip the Authorization header when there is no bearer token

* [MM-70605] Surface preauth secret rejections with a specific error message

* [MM-70605] Mock clearPendingPreauthSecret in session action tests

* [MM-70605] Remove the unreachable catch from restorePreviousPreauthSecret

setPreauthSecret and removePreauthSecret never reject, and the returned promise was not awaited, so the catch could not run.

* [MM-70605] Use network-client fix for the iOS addHeaders crash

Temporary pin to mattermost/react-native-network-client#175 to check CI. Replace with the released version before merge.

* [MM-70605] Bump react-native-network-client to 1.11.5

1.11.5 gives each recreated iOS session its own delegate in addHeaders, which fixes the SIGTRAP on pre-auth secret changes.

---------


(cherry picked from commit 9ccfa0e)

Co-authored-by: Felipe Martin <812088+fmartingr@users.noreply.github.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
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.

2 participants