-
-
Notifications
You must be signed in to change notification settings - Fork 134
Delete Pending requests on switch account / logout #2565
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
How did you test this?
It does not work, see video:
2025-09-21.16-32-03.mp4
I will add this video to the issue because this is the same test I did to demonstrate that #2406 (review) does not work.
I also added a new possible hint how to fix this in the issue.
lib/auth.js
Outdated
export const clearAuthCookies = () => { | ||
const expiredDate = 'Thu, 01 Jan 1970 00:00:00 GMT' | ||
document.cookie = `${SESSION_COOKIE}=; path=/; expires=${expiredDate}` | ||
document.cookie = `${MULTI_AUTH_POINTER}=; path=/; expires=${expiredDate}` | ||
} | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think JS can clear httpOnly cookies in production
lib/apollo.js
Outdated
function getClient (uri) { | ||
const authContextLink = setContext((_, { headers }) => { | ||
if (SSR) return { headers } | ||
if (typeof window !== 'undefined' && window.logoutInProgress) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
headers
is always undefined here
components/nav/common.js
Outdated
window.logoutInProgress = true | ||
clearAuthCookies() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
we might not hit this line during logout because we get switched to another account, see code above
the code above is also prone to the race condition
components/account.js
Outdated
// prevent navigation | ||
e.preventDefault() | ||
|
||
if (typeof window !== 'undefined') window.logoutInProgress = true |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
is the assumption that this variable is reset when the page is reloaded?
i tested using something like this: |
This is fetching every 5 seconds, but the bug is about requests that take 5 seconds to finish |
After spending some time in the documentations you suggested, I implemented a custom Apollo Link that injects 2025-09-21-19-56-11.mp4 |
Mhh, for some reason this still doesn't work for me. A request that happens after the page already reloaded still includes cookies that get subsequently refreshed even though the browser already cleared the cookies 🤔 I thought it might be related to our retry link or the order of the logout vs retry link, but even if I remove the retry link, it still happens. I even tried to fix this by keeping a logout flag around (stored in local storage) for a few seconds after page reload but it still happens 😕 patchdiff --git a/components/me.js b/components/me.js
index 7a82485d..38e140da 100644
--- a/components/me.js
+++ b/components/me.js
@@ -1,7 +1,8 @@
-import React, { useContext } from 'react'
+import React, { useContext, useEffect } from 'react'
import { useQuery } from '@apollo/client'
import { ME } from '@/fragments/users'
import { FAST_POLL_INTERVAL_MS, SSR } from '@/lib/constants'
+import { resetLogoutFlag, isLoggingOut, LOGOUT_FLAG_TIMEOUT_MS } from '@/lib/logout'
export const MeContext = React.createContext({
me: null
@@ -14,6 +15,15 @@ export function MeProvider ({ me, children }) {
// which was passed during page load which (visually) breaks switching to anon
const futureMe = data?.me ?? (data?.me === null ? null : me)
+ useEffect(() => {
+ if (!isLoggingOut()) {
+ return
+ }
+
+ const timeout = setTimeout(resetLogoutFlag, LOGOUT_FLAG_TIMEOUT_MS)
+ return () => clearTimeout(timeout)
+ }, [])
+
return (
<MeContext.Provider value={{ me: futureMe, refreshMe: refetch }}>
{children}
diff --git a/components/nav/common.js b/components/nav/common.js
index 3c9f6197..034c1764 100644
--- a/components/nav/common.js
+++ b/components/nav/common.js
@@ -22,6 +22,7 @@ import { useWalletIndicator } from '@/wallets/client/hooks'
import SwitchAccountList, { nextAccount, useAccounts } from '@/components/account'
import { useShowModal } from '@/components/modal'
import { numWithUnits } from '@/lib/format'
+import { setLogoutFlag } from '@/lib/logout'
export function Brand ({ className }) {
return (
@@ -306,6 +307,8 @@ function LogoutObstacle ({ onClose }) {
return
}
+ setLogoutFlag()
+
// order is important because we need to be logged in to delete push subscription on server
const pushSubscription = await swRegistration?.pushManager.getSubscription()
if (pushSubscription) {
diff --git a/lib/apollo.js b/lib/apollo.js
index b05d1a94..917c492e 100644
--- a/lib/apollo.js
+++ b/lib/apollo.js
@@ -4,6 +4,7 @@ import { decodeCursor, LIMIT } from './cursor'
import { COMMENTS_LIMIT, SSR } from './constants'
import { RetryLink } from '@apollo/client/link/retry'
import { isMutationOperation, isQueryOperation } from '@apollo/client/utilities'
+import logoutLink from './logout'
function isFirstPage (cursor, existingThings, limit = LIMIT) {
if (cursor) {
@@ -47,7 +48,9 @@ const retryLink = new RetryLink({
function getClient (uri) {
const link = from([
+ // TODO: how to order retry and logout link?
retryLink,
+ logoutLink,
split(
// batch zaps if wallet is enabled so they can be executed serially in a single request
operation => operation.operationName === 'act' && operation.variables.act === 'TIP' && operation.getContext().batch,
diff --git a/lib/logout.js b/lib/logout.js
new file mode 100644
index 00000000..6b48650e
--- /dev/null
+++ b/lib/logout.js
@@ -0,0 +1,70 @@
+import { ApolloLink } from '@apollo/client'
+import { SSR } from '@/lib/constants'
+
+// we want to cancel all API requests when the user logs out
+// since slow responses can cause the user to get back logged in
+// see https://github.com/stackernews/stacker.news/issues/2366
+
+const LOGOUT_FLAG = 'logout' // "logout flag" is saved in local storage
+
+// how long we will keep logout flag around after page reload
+// (this is required because for some reason, requests can still
+// contain cookies after the browser already deleted them and the page was reloaded)
+export const LOGOUT_FLAG_TIMEOUT_MS = 3000
+
+function getLogoutController () {
+ console.log('creating new logout controller?', !window.logoutController)
+ window.logoutController ||= new AbortController()
+ if (window.localStorage.getItem(LOGOUT_FLAG)) {
+ console.log('logout flag set, abort request immediately')
+ window.logoutController.abort('logout in progress')
+ }
+ console.log('request immediately aborted?', window.logoutController.signal.aborted)
+ return window.logoutController
+}
+
+export function setLogoutFlag () {
+ const controller = getLogoutController()
+ controller.abort('logout in progress')
+ console.log('setting logout flag')
+ // TODO: when to reset controller? after page reload?
+ // can we depend on always reloading the page before issuing new requests?
+ // window.logoutController = null
+ //
+ // UPDATE: it seems to be the case that a request can still contain cookies
+ // even after page load so we lose any window state ...
+ //
+ // Afaict, I have the following options:
+ // a) remove cookies while logout is in progress (even beyond page reload)
+ // b) find out why there are still cookies in the first place and remove if possible
+ window.localStorage.setItem(LOGOUT_FLAG, 'true')
+}
+
+export function resetLogoutFlag () {
+ window.localStorage.removeItem(LOGOUT_FLAG)
+ window.logoutController = null
+}
+
+export function isLoggingOut () {
+ return window.localStorage.getItem(LOGOUT_FLAG) === 'true'
+}
+
+const logoutLink = new ApolloLink((operation, forward) => {
+ if (SSR) {
+ // no logout controller required for requests on server
+ return forward(operation)
+ }
+
+ const controller = getLogoutController()
+ operation.setContext(context => ({
+ ...context,
+ fetchOptions: {
+ ...context.fetchOptions,
+ signal: controller.signal
+ }
+ }))
+
+ return forward(operation)
+})
+
+export default logoutLink This makes no sense to me. After page reload, we should be creating a new Apollo client instance. How can this new Apollo client instance access cookies that have been used in a canceled request of a previous instance and have already been cleared? |
Description
fix #2366
Fixed race condition:
Screenshots
NaN
Additional Context
Uses Apollo Client's setContext link for clean request interception, clearAuthCookies() function can be reused for other logout scenarios
Checklist
Are your changes backward compatible? Please answer below:
Yes
On a scale of 1-10 how well and how have you QA'd this change and any features it might affect? Please answer below:
8/10
For frontend changes: Tested on mobile, light and dark mode? Please answer below:
NaN
Did you introduce any new environment variables? If so, call them out explicitly here:
NaN
Did you use AI for this? If so, how much did it assist you?
I used AI to locate interested files and to test the implementations