-
Notifications
You must be signed in to change notification settings - Fork 493
Description
Bug report
When a user is offline or in standby mode, the access_token is not refreshed as it requires a POST request to auth/v1/token?grant_type=refresh_token (which obviously fails without a connection). When the user comes back online or is active again, Supabase doesn't automatically refetch the access_token and supply it to realtime channels. Instead, the realtime channels error and show the following message, because they try to connect with the old token:
Even if the access_token is refreshed afterwards, the channels don't seem to pick up the change. The only way to get the channels to work again is by removing and re-initializing them.
To Reproduce
- Set the JWT expiry time of the access_token (e. g. 60 = 1 minute) to a low value, to be able to better reproduce the case where a access_token needs a refresh.
- Subscribe to a realtime channel.
- Turn off WIFI or go into standby mode.
- Wait for the current access_token to expire (in this case after 1 minute).
- Turn the WIFI on again or disable standby.
The realtime channel tries to reconnect with the old token (because Supabase wasn't able to refetch it without a connection) and errors out. It doesn't pick up any new tokens once Supabase refreshes them again in the usual interval.
Expected behavior
If any realtime channels are still active after re-enabling WIFI or disabling standby, Supabase should refetch the access_token right away and supply it to the channels. This way, they won't error out.
System information
- OS: macOS
- Version of supabase-js: 2.39.1
Additional context
I've built a temporary workaround to the fix the issue, maybe it can help:
import { RealtimeChannel, SupabaseClient } from '@supabase/supabase-js'
export const realtime = (client: SupabaseClient) => {
let channel: RealtimeChannel | null = null
let subscribed = false
let reconnect = true
let reconnecting = false
const _subscribe = ({ table }) => {
if (channel) client.removeChannel(channel)
reconnecting = true
return (
callback: (payload: any) => void,
) => {
channel = client
.channel('realtime')
.on(
'postgres_changes',
{
event: '*',
schema: 'public',
table,
},
(payload) => {
callback(payload)
},
)
.subscribe()
// @ts-expect-error
.on('phx_reply', {}, async (payload) => {
if (
payload.status === 'error' &&
payload.response.reason.includes('Invalid token')
) {
await client.removeChannel(channel)
await client.auth.refreshSession()
subscribed = false
reconnecting = false
}
})
// @ts-expect-error
.on('system', {}, async (payload) => {
if (
payload.extension === 'postgres_changes' &&
payload.status === 'ok'
) {
subscribed = true
reconnecting = false
}
})
}
}
return {
from: (table: string) => {
return {
subscribe: (
callback: (payload: any) => void,
) => {
let timer: NodeJS.Timeout
const reconnectSubscription = () => {
clearTimeout(timer)
timer = setTimeout(() => {
reconnectSubscription()
if (
reconnect &&
!subscribed &&
!reconnecting &&
document.visibilityState === 'visible'
) {
return _subscribe({ table })(callback)
}
}, 1000)
}
reconnectSubscription()
return _subscribe({ table })(callback)
},
unsubscribe: async () => {
if (!channel) return
await client.removeChannel(channel)
channel = null
reconnect = false
},
}
},
}
}