Skip to content

Commit 6246f55

Browse files
committed
fix(realtime): eliminate race condition in custom JWT token authentication
1 parent 26a134d commit 6246f55

File tree

3 files changed

+37
-22
lines changed

3 files changed

+37
-22
lines changed

packages/core/realtime-js/src/RealtimeClient.ts

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -620,7 +620,19 @@ export default class RealtimeClient {
620620
private _onConnOpen() {
621621
this._setConnectionState('connected')
622622
this.log('transport', `connected to ${this.endpointURL()}`)
623-
this.flushSendBuffer()
623+
624+
// Wait for any pending auth operations before flushing send buffer
625+
// This ensures channel join messages include the correct access token
626+
this._waitForAuthIfNeeded()
627+
.then(() => {
628+
this.flushSendBuffer()
629+
})
630+
.catch((e) => {
631+
this.log('error', 'error waiting for auth on connect', e)
632+
// Proceed anyway to avoid hanging connections
633+
this.flushSendBuffer()
634+
})
635+
624636
this._clearTimer('reconnect')
625637

626638
if (!this.worker) {

packages/core/supabase-js/src/SupabaseClient.ts

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -157,11 +157,10 @@ export default class SupabaseClient<
157157
...settings.realtime,
158158
})
159159
if (this.accessToken) {
160-
setTimeout(() => {
161-
this.accessToken?.()
162-
?.then((token) => this.realtime.setAuth(token))
163-
.catch((e) => console.warn('Failed to set initial Realtime auth token:', e))
164-
}, 0)
160+
// Start auth immediately to avoid race condition with channel subscriptions
161+
this.accessToken()
162+
.then((token) => this.realtime.setAuth(token))
163+
.catch((e) => console.warn('Failed to set initial Realtime auth token:', e))
165164
}
166165

167166
this.rest = new PostgrestClient(new URL('rest/v1', baseUrl).href, {

packages/core/supabase-js/test/integration.test.ts

Lines changed: 20 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -367,23 +367,27 @@ describe('Custom JWT', () => {
367367
const supabaseWithCustomJwt = createClient(SUPABASE_URL, ANON_KEY, {
368368
accessToken: () => Promise.resolve(jwtToken),
369369
})
370-
await new Promise((resolve) => setTimeout(resolve, 100))
371-
expect(supabaseWithCustomJwt.realtime.accessTokenValue).toBe(jwtToken)
372-
let subscribed = false
373-
let attempts = 0
374-
supabaseWithCustomJwt.channel('test-channel').subscribe((status) => {
375-
if (status == 'SUBSCRIBED') subscribed = true
376-
})
377370

378-
// Wait for subscription
379-
while (!subscribed) {
380-
if (attempts > 50) throw new Error('Timeout waiting for subscription')
381-
await new Promise((resolve) => setTimeout(resolve, 100))
382-
attempts++
383-
}
371+
// Wait for subscription using Promise to avoid polling
372+
await new Promise<void>((resolve, reject) => {
373+
const timeout = setTimeout(() => {
374+
reject(new Error('Timeout waiting for subscription'))
375+
}, 10000)
376+
377+
supabaseWithCustomJwt.channel('test-channel').subscribe((status, err) => {
378+
if (status === 'SUBSCRIBED') {
379+
clearTimeout(timeout)
380+
// Verify token was set
381+
expect(supabaseWithCustomJwt.realtime.accessTokenValue).toBe(jwtToken)
382+
resolve()
383+
} else if (status === 'CHANNEL_ERROR' || status === 'TIMED_OUT') {
384+
clearTimeout(timeout)
385+
reject(err || new Error(`Subscription failed with status: ${status}`))
386+
}
387+
})
388+
})
384389

385-
expect(subscribed).toBe(true)
386-
//
387-
}, 10000)
390+
await supabaseWithCustomJwt.removeAllChannels()
391+
}, 15000)
388392
})
389393
})

0 commit comments

Comments
 (0)