Version Number: 9.4.50-0
Reproducible in staging?: Needs verification
Reproducible in production?: Yes (observed on production)
If this was caught during regression testing, add the test name, ID and link from BrowserStack: N/A
Email or phone of affected tester (no customers): vit@expensify.com
Logs: https://stackoverflow.com/c/expensify/questions/4856
Expensify/Expensify Issue URL:
Issue reported by: @mountiny
Slack conversation (hyperlinked to channel name):
Action Performed:
- Sign in to New Expensify on web as an Expensify employee.
- Start a supportal session into another account from Expensify Classic.
- Watch the network tab (or backend request logs) while New Expensify finishes loading.
Expected Result:
Exactly one OpenApp request is sent for the newly opened session.
Actual Result:
Two OpenApp requests are sent back to back for the same session. Both run the full app-load payload, so the account pays the OpenApp cost twice.
Analysis
OpenApp has no in-flight deduplication, so two openApp() triggers inside the request window both reach the server.
openApp() sends the request through API.writeWithNoDuplicatesConflictAction, which resolves conflicts with resolveDuplicationConflictAction(persistedRequests, matcher):
https://github.com/Expensify/App/blob/main/src/libs/actions/RequestConflictUtils.ts#L60
That resolver only searches persistedRequests. processNextRequest() moves the request that is currently in flight out of persistedRequests and into ongoingRequest:
https://github.com/Expensify/App/blob/main/src/libs/actions/PersistedRequests.ts#L406
So while OpenApp #1 is in flight, the matcher finds nothing, the resolver returns push instead of replace, and OpenApp #2 is enqueued and sent.
ReconnectApp already closes this gap. writeWithNoDuplicatesReconnectConflictAction passes getOngoingRequest() into its resolver, so a ReconnectApp that arrives mid-flight is collapsed:
https://github.com/Expensify/App/blob/main/src/libs/API/index.ts#L71
OpenApp never got the equivalent treatment.
Is this supportal only?
No. The missing in-flight guard is generic — any account that produces two openApp() triggers close together sends two OpenApp requests. Supportal just raises the odds, because the flow signs the agent out, clears Onyx, signs in with the support auth token, and swaps the Public navigator for the Auth navigator. The openApp() trigger in AuthScreensInitHandler runs in a mount-only effect ([] deps), so every remount of AuthScreens fires another one:
https://github.com/Expensify/App/blob/main/src/libs/Navigation/AppNavigator/AuthScreensInitHandler.tsx#L177
The remount during a support login is already documented in signInWithSupportAuthToken, which added a token guard for the same reason:
https://github.com/Expensify/App/blob/main/src/libs/actions/Session/index.ts#L244
Other openApp() callers can stack the same way — PriorityModeHandler, SignInModal, useIsLoadingAppRecovery, and the delegate paths in Delegate.ts.
Prior art
Fix doubled OpenApp on delegate (copilot) account switch was the same class of bug for copilot. It was fixed by removing the two specific triggers, not by adding deduplication, so the underlying gap stayed open.
App hangs when OpenApp / ReconnectApp / GetMissingOnyxMessages fire in rapid stampedes tracks the broader stampede symptom.
Proposed fix
Give OpenApp the same in-flight awareness ReconnectApp has: feed getOngoingRequest() into the conflict resolver so a second OpenApp is dropped or collapsed while one is already running. Fixing it at the dedupe layer covers every trigger at once, instead of chasing each caller.
Workaround:
Yes, users can keep using Expensify. The duplicate request is invisible in the UI beyond a slower app load, and it is more noticeable on large accounts.
Platforms:
Select the officially supported platforms where the issue was reproduced:
Screenshots/Videos
View all open jobs on GitHub
Issue Owner
Current Issue Owner: @daledah
Version Number: 9.4.50-0
Reproducible in staging?: Needs verification
Reproducible in production?: Yes (observed on production)
If this was caught during regression testing, add the test name, ID and link from BrowserStack: N/A
Email or phone of affected tester (no customers): vit@expensify.com
Logs: https://stackoverflow.com/c/expensify/questions/4856
Expensify/Expensify Issue URL:
Issue reported by: @mountiny
Slack conversation (hyperlinked to channel name):
Action Performed:
Expected Result:
Exactly one
OpenApprequest is sent for the newly opened session.Actual Result:
Two
OpenApprequests are sent back to back for the same session. Both run the full app-load payload, so the account pays the OpenApp cost twice.Analysis
OpenApphas no in-flight deduplication, so twoopenApp()triggers inside the request window both reach the server.openApp()sends the request throughAPI.writeWithNoDuplicatesConflictAction, which resolves conflicts withresolveDuplicationConflictAction(persistedRequests, matcher):https://github.com/Expensify/App/blob/main/src/libs/actions/RequestConflictUtils.ts#L60
That resolver only searches
persistedRequests.processNextRequest()moves the request that is currently in flight out ofpersistedRequestsand intoongoingRequest:https://github.com/Expensify/App/blob/main/src/libs/actions/PersistedRequests.ts#L406
So while
OpenApp#1 is in flight, the matcher finds nothing, the resolver returnspushinstead ofreplace, andOpenApp#2 is enqueued and sent.ReconnectAppalready closes this gap.writeWithNoDuplicatesReconnectConflictActionpassesgetOngoingRequest()into its resolver, so aReconnectAppthat arrives mid-flight is collapsed:https://github.com/Expensify/App/blob/main/src/libs/API/index.ts#L71
OpenAppnever got the equivalent treatment.Is this supportal only?
No. The missing in-flight guard is generic — any account that produces two
openApp()triggers close together sends twoOpenApprequests. Supportal just raises the odds, because the flow signs the agent out, clears Onyx, signs in with the support auth token, and swaps the Public navigator for the Auth navigator. TheopenApp()trigger inAuthScreensInitHandlerruns in a mount-only effect ([]deps), so every remount ofAuthScreensfires another one:https://github.com/Expensify/App/blob/main/src/libs/Navigation/AppNavigator/AuthScreensInitHandler.tsx#L177
The remount during a support login is already documented in
signInWithSupportAuthToken, which added a token guard for the same reason:https://github.com/Expensify/App/blob/main/src/libs/actions/Session/index.ts#L244
Other
openApp()callers can stack the same way —PriorityModeHandler,SignInModal,useIsLoadingAppRecovery, and the delegate paths inDelegate.ts.Prior art
Fix doubled OpenApp on delegate (copilot) account switch was the same class of bug for copilot. It was fixed by removing the two specific triggers, not by adding deduplication, so the underlying gap stayed open.
App hangs when OpenApp / ReconnectApp / GetMissingOnyxMessages fire in rapid stampedes tracks the broader stampede symptom.
Proposed fix
Give
OpenAppthe same in-flight awarenessReconnectApphas: feedgetOngoingRequest()into the conflict resolver so a secondOpenAppis dropped or collapsed while one is already running. Fixing it at the dedupe layer covers every trigger at once, instead of chasing each caller.Workaround:
Yes, users can keep using Expensify. The duplicate request is invisible in the UI beyond a slower app load, and it is more noticeable on large accounts.
Platforms:
Select the officially supported platforms where the issue was reproduced:
Screenshots/Videos
View all open jobs on GitHub
Issue Owner
Current Issue Owner: @daledah