Skip to content

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

Open
theprantadutta wants to merge 2 commits into
supabase:mainfrom
theprantadutta:fix/realtime-channel-suppress-invalid-jwt-on-rejoin
Open

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

Conversation

@theprantadutta
Copy link
Copy Markdown

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.

…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
@Vinzent03
Copy link
Copy Markdown
Collaborator

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

Matches the formatter settings used by CI
(`dart format lib test -l 80 --set-exit-if-changed`).
@theprantadutta
Copy link
Copy Markdown
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.

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

2 participants