Skip to content

Commit 6a5be51

Browse files
fix(supabase_flutter): Safely check if conn is not null to avoid null check operator (#1178)
* safely check if realtime.conn is not null to avoid null check operator used on null value * fix: ensure disconnecting state is followed by disconnected * test: add tests for disconnect behavior --------- Co-authored-by: Vinzent <[email protected]>
1 parent cc058e7 commit 6a5be51

File tree

3 files changed

+58
-13
lines changed

3 files changed

+58
-13
lines changed

packages/realtime_client/lib/src/realtime_client.dart

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -222,18 +222,22 @@ class RealtimeClient {
222222
final conn = this.conn;
223223
if (conn != null) {
224224
final oldState = connState;
225-
connState = SocketStates.disconnecting;
226-
log('transport', 'disconnecting', {'code': code, 'reason': reason},
227-
Level.FINE);
225+
final shouldCloseSink =
226+
oldState == SocketStates.open || oldState == SocketStates.connecting;
227+
if (shouldCloseSink) {
228+
// Don't set the state to `disconnecting` if the connection is already closed.
229+
connState = SocketStates.disconnecting;
230+
log('transport', 'disconnecting', {'code': code, 'reason': reason},
231+
Level.FINE);
232+
}
228233

229234
// Connection cannot be closed while it's still connecting. Wait for connection to
230235
// be ready and then close it.
231236
if (oldState == SocketStates.connecting) {
232237
await conn.ready.catchError((_) {});
233238
}
234239

235-
if (oldState == SocketStates.open ||
236-
oldState == SocketStates.connecting) {
240+
if (shouldCloseSink) {
237241
if (code != null) {
238242
await conn.sink.close(code, reason ?? '');
239243
} else {
@@ -319,7 +323,7 @@ class RealtimeClient {
319323
}
320324
}
321325

322-
/// Retuns `true` is the connection is open.
326+
/// Returns `true` is the connection is open.
323327
bool get isConnected => connState == SocketStates.open;
324328

325329
/// Removes a subscription from the socket.

packages/realtime_client/test/socket_test.dart

Lines changed: 46 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -51,11 +51,15 @@ void main() {
5151

5252
setUp(() async {
5353
mockServer = await HttpServer.bind('localhost', 0);
54+
WebSocketChannel? channel;
55+
5456
mockServer.transform(WebSocketTransformer()).listen((webSocket) {
55-
final channel = IOWebSocketChannel(webSocket);
56-
channel.stream.listen((request) {
57-
channel.sink.add(request);
57+
channel = IOWebSocketChannel(webSocket);
58+
channel!.stream.listen((request) {
59+
channel!.sink.add(request);
5860
});
61+
}, onDone: () {
62+
channel?.sink.close();
5963
});
6064
});
6165

@@ -170,11 +174,15 @@ void main() {
170174
socket.disconnect();
171175
});
172176

173-
test('establishes websocket connection with endpoint', () {
174-
socket.connect();
177+
test('establishes websocket connection with endpoint', () async {
178+
final connFuture = socket.connect();
179+
expect(socket.connState, SocketStates.connecting);
175180

176181
final conn = socket.conn;
177182

183+
await connFuture;
184+
expect(socket.connState, SocketStates.open);
185+
178186
expect(conn, isA<IOWebSocketChannel>());
179187
//! Not verifying connection url
180188
});
@@ -239,9 +247,11 @@ void main() {
239247

240248
test('removes existing connection', () async {
241249
await socket.connect();
250+
251+
expect(socket.conn, isNotNull);
242252
await socket.disconnect();
243253

244-
expect(socket.conn, null);
254+
expect(socket.conn, isNull);
245255
});
246256

247257
test('calls callback', () async {
@@ -284,6 +294,36 @@ void main() {
284294
).called(1);
285295
});
286296

297+
test('disconnecting a closed connections stays closed', () async {
298+
await socket.connect();
299+
expect(socket.connState, SocketStates.open);
300+
await mockServer.close();
301+
await Future.delayed(const Duration(milliseconds: 200));
302+
expect(socket.connState, SocketStates.closed);
303+
expect(socket.conn, isNotNull);
304+
305+
final disconnectFuture = socket.disconnect();
306+
307+
// `connState` stays `closed` during disconnect
308+
expect(socket.connState, SocketStates.closed);
309+
await disconnectFuture;
310+
expect(socket.connState, SocketStates.closed);
311+
expect(socket.conn, isNull);
312+
});
313+
314+
test('disconnecting an open connection', () async {
315+
await socket.connect();
316+
expect(socket.connState, SocketStates.open);
317+
318+
final disconnectFuture = socket.disconnect();
319+
320+
// `connState` stays `closed` during disconnect
321+
expect(socket.connState, SocketStates.disconnecting);
322+
await disconnectFuture;
323+
expect(socket.connState, SocketStates.disconnected);
324+
expect(socket.conn, isNull);
325+
});
326+
287327
test('does not throw when no connection', () {
288328
expect(() => socket.disconnect(), returnsNormally);
289329
});

packages/supabase_flutter/lib/src/supabase.dart

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -237,7 +237,8 @@ class Supabase with WidgetsBindingObserver {
237237
Future<void> onResumed() async {
238238
final realtime = Supabase.instance.client.realtime;
239239
if (realtime.channels.isNotEmpty) {
240-
if (realtime.connState == SocketStates.disconnecting) {
240+
if (realtime.connState == SocketStates.disconnecting &&
241+
realtime.conn != null) {
241242
// If the socket is still disconnecting from e.g.
242243
// [AppLifecycleState.paused] we should wait for it to finish before
243244
// reconnecting.

0 commit comments

Comments
 (0)