Skip to content

fix(realtime_client): suppress InvalidJWTToken from setAuth in joinPush 'ok' handler#1365

Merged
spydon merged 2 commits into
supabase:mainfrom
theprantadutta:fix/realtime-channel-suppress-invalid-jwt-on-rejoin
Jun 8, 2026
Merged

fix(realtime_client): suppress InvalidJWTToken from setAuth in joinPush 'ok' handler#1365
spydon merged 2 commits into
supabase:mainfrom
theprantadutta:fix/realtime-channel-suppress-invalid-jwt-on-rejoin

Conversation

@theprantadutta

Copy link
Copy Markdown
Contributor

Summary

Fixes #1363.

RealtimeChannel.subscribe() registers an async 'ok' callback on joinPush that calls socket.setAuth(socket.accessToken) without a try/catch. When the cached access token has expired by the time the server replies 'ok' (e.g. the device woke from a long background sleep and the channel rejoined before the auth refresh fired), setAuth throws:

FormatException: InvalidJWTToken: Invalid value for JWT claim "exp" with value …

Because Push.trigger invokes the callback as a void-returning function, the resulting Future's error is discarded and surfaces in the zone error handler. Sentry / Crashlytics report it as a fatal even though the app keeps running and the next tokenRefreshed event recovers normally.

SupabaseClient._handleTokenChanged already filters this exact exception on the auth-state-change path:

https://github.com/supabase/supabase-flutter/blob/main/packages/supabase/lib/src/supabase_client.dart#L379-L389

This PR mirrors that filter on the rejoin path so the two re-auth code paths behave consistently.

Change

packages/realtime_client/lib/src/realtime_channel.dart — wrap the rejoin setAuth call in the same FormatException / InvalidJWTToken filter that SupabaseClient._handleTokenChanged uses. Other FormatExceptions still propagate.

if (socket.accessToken != null) {
  try {
    await socket.setAuth(socket.accessToken);
  } on FormatException catch (e) {
    if (!e.message.contains('InvalidJWTToken')) {
      rethrow;
    }
  }
}

Tests

Added two unit tests in packages/realtime_client/test/channel_test.dart:

  1. Positive case — when setAuth throws FormatException('InvalidJWTToken: …') on rejoin, the 'ok' handler still runs to completion and emits RealtimeSubscribeStatus.subscribed. Without the fix this test fails (the callback aborts at setAuth and the subscribed status is never emitted), confirming it genuinely validates the fix.
  2. Rethrow case — non-InvalidJWTToken FormatExceptions still abort the rejoin handler (subscribed is not emitted).

Both tests use a RealtimeClient subclass that overrides setAuth to throw a controlled exception and overrides connect to a no-op so transport-level async errors do not pollute the test runner zone.

Test plan

  • dart test packages/realtime_client/test/channel_test.dart — both new tests pass; pre-existing tests unchanged
  • dart analyze clean for realtime_client, supabase, and supabase_flutter
  • Verified the positive test fails when the source change is reverted (confirming it tests the right thing)

Note: httpSend throws error on non-202 status is flaky on main independently of this change (~40% fail rate locally on a clean checkout). Out of scope for this PR.

@Vinzent03

Copy link
Copy Markdown
Collaborator

If you could fix the formatting issue, I think this looks great. Thanks!

@theprantadutta

Copy link
Copy Markdown
Contributor Author

Thanks for the review @Vinzent03! Formatting fixed in 34c2f35 — re-ran dart format lib test -l 80 --set-exit-if-changed locally to match CI, clean now. Tests still pass.

@theprantadutta

Copy link
Copy Markdown
Contributor Author

Hi @Vinzent03 — friendly nudge on this one. It's been about a week since the approval and I want to make sure nothing's blocking on my side. Happy to rebase or address anything new if needed.

@Vinzent03

Copy link
Copy Markdown
Collaborator

I'm an external contributor and currently do not have the permission to merge without approval of a codeowner. The responsible Supabase member is currently OOF, but will surely be back soon. So there is nothing blocking from your side.

@spydon spydon left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Lgtm, thanks for your contribution!

…sh 'ok' handler

The 'ok' callback registered by RealtimeChannel.subscribe() calls
socket.setAuth(socket.accessToken) without a try/catch. When the cached
access token has expired by the time the channel rejoins (e.g. after
the device wakes from a long background sleep), setAuth throws
FormatException('InvalidJWTToken: Invalid value for JWT claim "exp"
with value ...'). The async callback discards the returned Future, so
the error escapes into the zone error handler and is reported by
Sentry/Crashlytics as a fatal — even though the app recovers fine on
the next tokenRefreshed event.

The same exception is already filtered in
SupabaseClient._handleTokenChanged for the auth-state-change path; this
mirrors that filter on the rejoin path so the two re-auth code paths
behave consistently.

Closes supabase#1363
Matches the formatter settings used by CI
(`dart format lib test -l 80 --set-exit-if-changed`).
@spydon
spydon force-pushed the fix/realtime-channel-suppress-invalid-jwt-on-rejoin branch from 34c2f35 to 90ca34c Compare June 8, 2026 14:33
@spydon
spydon merged commit 2fb3f1c into supabase:main Jun 8, 2026
15 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

realtime_client: setAuth call inside joinPush 'ok' callback can throw uncaught FormatException for expired token

3 participants