Fix spurious logouts by deduplicating concurrent token renewals#17858
Fix spurious logouts by deduplicating concurrent token renewals#17858FelixMalfait merged 2 commits intomainfrom
Conversation
Greptile OverviewGreptile SummaryAdds token renewal deduplication to prevent multiple concurrent GraphQL queries from each triggering their own Key Changes:
Issue Found:
Confidence Score: 3/5
Important Files Changed
Sequence DiagramsequenceDiagram
participant C1 as Concurrent Request 1
participant C2 as Concurrent Request 2
participant C3 as Concurrent Request 3
participant Handler as handleTokenRenewal
participant Server as Auth Server
Note over C1,C3: Access tokens expired, all fail with UNAUTHENTICATED
C1->>Handler: UNAUTHENTICATED error
activate Handler
Note over Handler: renewalPromise = null, create new promise
Handler->>Server: renewToken(refreshToken)
activate Server
C2->>Handler: UNAUTHENTICATED error
Note over Handler: renewalPromise exists, reuse same promise
C3->>Handler: UNAUTHENTICATED error
Note over Handler: renewalPromise exists, reuse same promise
Server-->>Handler: Return new tokens
deactivate Server
Handler->>Handler: Update tokens in cookie/state
Handler->>Handler: renewalPromise = null (finally)
deactivate Handler
Handler-->>C1: forward(operation) with new tokens
Handler-->>C2: forward(operation) with new tokens
Handler-->>C3: forward(operation) with new tokens
Note over C1,C3: All requests retry successfully with refreshed tokens
|
Additional Comments (1)
|
|
🚀 Preview Environment Ready! Your preview environment is available at: http://bore.pub:58501 This environment will automatically shut down when the PR is closed or after 5 hours. |
When returning to the app after idle (expired access token), multiple GraphQL queries fail with UNAUTHENTICATED simultaneously. Each one independently triggered a renewToken call with the same refresh token. If any single renewal failed (e.g. server briefly slow after a deploy), the catch handler would clear the session and redirect to sign-in, even if another concurrent renewal had already succeeded. This was compounded by having two separate ApolloFactory instances (/graphql and /metadata), each racing to renew the same refresh token. The /metadata client's renewal could also fail because the RenewToken mutation may not be exposed on that endpoint. Fix: lift renewalPromise to module scope so all ApolloFactory instances share a single in-flight renewal, and always send the RenewToken mutation to /graphql. Co-authored-by: Cursor <cursoragent@cursor.com>
9107511 to
bc2099b
Compare
|
Hey @FelixMalfait! After you've done the QA of your Pull Request, you can mark it as done here. Thank you! |
1 similar comment
|
Hey @FelixMalfait! After you've done the QA of your Pull Request, you can mark it as done here. Thank you! |
Summary
UNAUTHENTICATED. Previously, each failure independently triggered its ownrenewTokencall with the same refresh token. If any single renewal failed (e.g. server briefly slow after a deploy), thecatchhandler would nuke the session and redirect to sign-in — even if another concurrent renewal had already succeeded and written valid tokens.renewalPromiseso that only the firstUNAUTHENTICATEDerror triggers a server-side renewal. All concurrent callers await the same promise and replay their operations once it resolves. This eliminates redundant refresh token rotation on the server and removes the race condition where a straggling failure could log out an already-renewed session.Test plan
renewTokenmutation is sent (instead of N)onUnauthenticatedErrorcall)Made with Cursor