socket_io_client_new

A fixed fork of socket_io_client that resolves the port 0 issue affecting WebSocket connections.

Pub Version License

Version Info

socket_io_client_new Socket.io Server
v0.9.* ~ v1.* v2.*
v2.* v3.* ~ v4.6.*
v3.* v4.7.* ~ v4.*

Note: If connecting to a Socket.IO v2.x server, you need to add allowEIO3: true in your server configuration:

const io = require('socket.io')(server, {
  allowEIO3: true  // Required for older clients
});

The Problem (Original Package Bug)

The original socket_io_client package on pub.flutter-io.cn has a critical bug:

When Dart's Uri.parse() parses a URL without an explicit port (like https://api.example.com), it returns port = 0 instead of the default port (443 for HTTPS, 80 for HTTP).

The original package uses this port directly, generating invalid WebSocket URLs:

// What you expect:
wss://api.example.com/socket.io/?EIO=4&transport=websocket

// What original package generates (BROKEN):
wss://api.example.com:0/socket.io/?EIO=4&transport=websocket
                      ^^
                      This :0 breaks the connection!

This causes WebSocket connection failed errors in production when connecting to servers without explicit ports.

The Fix

This package fixes the issue by:

  1. Detecting when Uri.parse() returns port = 0
  2. Setting the correct default port (443 for HTTPS/WSS, 80 for HTTP/WS)
  3. Always including a valid port in the WebSocket URL
// This package generates correctly:
wss://api.example.com:443/socket.io/?EIO=4&transport=websocket

Installation

dependencies:
  socket_io_client_new: ^1.0.0

Usage

Dart Client

import 'package:socket_io_client_new/socket_io_client_new.dart' as IO;

void main() {
  IO.Socket socket = IO.io('http://localhost:3000');

  socket.onConnect((_) {
    print('connect');
    socket.emit('msg', 'test');
  });

  socket.on('event', (data) => print(data));
  socket.onDisconnect((_) => print('disconnect'));
  socket.on('fromServer', (_) => print(_));
}

Connect Manually

To connect the socket manually, set disableAutoConnect() and call .connect().

IO.Socket socket = IO.io(
  'http://localhost:3000',
  IO.OptionBuilder()
      .setTransports(['websocket']) // for Flutter or Dart VM
      .disableAutoConnect()  // disable auto-connection
      .setExtraHeaders({'foo': 'bar'}) // optional
      .build()
);
socket.connect();

Note: .connect() should not be called if autoConnect is enabled (default), as this will cause all event handlers to get registered/fired twice. See Issue #33.

Update Extra Headers

IO.Socket socket = ... // Create socket.
socket.io.options['extraHeaders'] = {'foo': 'bar'}; // Update extra headers.
socket.io..disconnect()..connect(); // Reconnect the socket manually.

Emit with Acknowledgement

socket.onConnect((_) {
  print('connect');
  socket.emitWithAck('msg', 'init', ack: (data) {
    print('ack $data');
    if (data != null) {
      print('from server $data');
    } else {
      print('Null');
    }
  });
});

Socket Connection Events

These events can be listened on:

socket.onConnect((_) => print('connect'));
socket.onConnectError((err) => print('connect_error: $err'));
socket.onDisconnect((_) => print('disconnect'));
socket.onError((err) => print('error: $err'));
socket.onReconnect((_) => print('reconnect'));
socket.onReconnectAttempt((attempt) => print('reconnect_attempt: $attempt'));
socket.onReconnectFailed((_) => print('reconnect_failed'));
socket.onReconnectError((err) => print('reconnect_error: $err'));
socket.onPing((_) => print('ping'));
socket.onPong((_) => print('pong'));

Acknowledge with the Server

socket.on('eventName', (data) {
  final dataList = data as List;
  final ack = dataList.last as Function;
  ack(null);
});

Usage (Flutter)

In Flutter (not Flutter Web), it only works with dart:io websocket, not with dart:html websocket or Ajax (XHR). So you must use setTransports(['websocket']):

IO.Socket socket = IO.io(
  'http://localhost:3000',
  IO.OptionBuilder()
      .setTransports(['websocket']) // for Flutter or Dart VM
      .setExtraHeaders({'foo': 'bar'}) // optional
      .build()
);

Usage with Stream and StreamBuilder

import 'dart:async';
import 'package:socket_io_client_new/socket_io_client_new.dart' as IO;

// STEP 1: Stream setup
class StreamSocket {
  final _socketResponse = StreamController<String>();

  void Function(String) get addResponse => _socketResponse.sink.add;
  Stream<String> get getResponse => _socketResponse.stream;

  void dispose() {
    _socketResponse.close();
  }
}

StreamSocket streamSocket = StreamSocket();

// STEP 2: Connect and listen
void connectAndListen() {
  IO.Socket socket = IO.io(
    'http://localhost:3000',
    IO.OptionBuilder().setTransports(['websocket']).build()
  );

  socket.onConnect((_) {
    print('connect');
    socket.emit('msg', 'test');
  });

  // When event received, add data to stream
  socket.on('event', (data) => streamSocket.addResponse(data));
  socket.onDisconnect((_) => print('disconnect'));
}

// STEP 3: Build widgets with StreamBuilder
class BuildWithSocketStream extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return StreamBuilder(
      stream: streamSocket.getResponse,
      builder: (BuildContext context, AsyncSnapshot<String> snapshot) {
        return Text(snapshot.data ?? '');
      },
    );
  }
}

Important: Socket Cache and Subsequent Lookups

There is a design decision related to caching of socket instances.

Lookup Protocol: We reuse the existing instance based on same scheme/port/host.

Both dispose() and destroy() do NOT remove a host from the cache. This can cause subsequent connections to ignore parts of your configuration.

Example

_socket = IO.io(
  host,
  IO.OptionBuilder()
      .setTransports(['websocket'])
      .setExtraHeaders({'id': userId, 'name': username})
      .disableAutoConnect()
      .enableReconnection()
      .build()
);

If you try to create the same connection with updated extraHeaders, the old connection would be returned with old headers.

Solutions:

  • Use enableForceNew() or disableMultiplex() in OptionBuilder
  • Or update extra headers manually (see "Update Extra Headers" section above)

Socket Service Pattern

A recommended pattern for managing socket connections:

import 'dart:async';
import 'package:socket_io_client_new/socket_io_client_new.dart' as IO;

class SocketService {
  static SocketService? _instance;
  IO.Socket? _socket;
  bool _isConnected = false;

  final _connectionController = StreamController<bool>.broadcast();
  Stream<bool> get connectionStream => _connectionController.stream;
  bool get isConnected => _isConnected;

  factory SocketService() {
    _instance ??= SocketService._internal();
    return _instance!;
  }

  SocketService._internal();

  void connect(String url) {
    if (_isConnected) return;

    _socket = IO.io(
      url,
      IO.OptionBuilder()
          .setTransports(['websocket', 'polling'])
          .enableAutoConnect()
          .enableReconnection()
          .setReconnectionAttempts(5)
          .build(),
    );

    _socket!.onConnect((_) {
      _isConnected = true;
      _connectionController.add(true);
      print('Connected');
    });

    _socket!.onDisconnect((_) {
      _isConnected = false;
      _connectionController.add(false);
      print('Disconnected');
    });

    _socket!.onConnectError((error) {
      print('Connection error: $error');
    });
  }

  void emit(String event, dynamic data) {
    _socket?.emit(event, data);
  }

  void on(String event, Function(dynamic) handler) {
    _socket?.on(event, handler);
  }

  void off(String event) {
    _socket?.off(event);
  }

  void disconnect() {
    _socket?.disconnect();
    _socket?.dispose();
    _socket = null;
    _isConnected = false;
  }

  void dispose() {
    disconnect();
    _connectionController.close();
  }
}

Migration from socket_io_client

Simply change your import:

// Before
import 'package:socket_io_client/socket_io_client.dart' as IO;

// After
import 'package:socket_io_client_new/socket_io_client_new.dart' as IO;

No other code changes required - the API is identical!

Troubleshooting

Cannot connect to "https" server or self-signed certificate

Refer to dart-lang/sdk#34284. Workaround:

class MyHttpOverrides extends HttpOverrides {
  @override
  HttpClient createHttpClient(SecurityContext? context) {
    return super.createHttpClient(context)
      ..badCertificateCallback =
          (X509Certificate cert, String host, int port) => true;
  }
}

void main() {
  HttpOverrides.global = MyHttpOverrides();
  runApp(MyApp());
}

Memory leak issues in iOS when closing socket

Refer to rikulo/socket.io-client-dart#108.

Use socket.dispose() instead of socket.close() or socket.disconnect() to solve the memory leak issue on iOS.

Connect_error on MacOS with SocketException: Connection failed

Refer to flutter/flutter#47606.

Add to macos/Runner/*.entitlements:

<key>com.apple.security.network.client</key>
<true/>

For more details: Flutter Desktop Entitlements

Can't connect on Flutter with Insecure HTTP

HTTP connections are disabled by default on iOS and Android. See Flutter network policy for workarounds.

Changes from Original Package

  • Fixed: Port 0 being incorrectly appended to URLs without explicit ports

License

MIT License - see LICENSE file.

Credits

This package is a fork of socket_io_client originally created by Potix Corporation.

Contributors

Libraries

socket_io_client_new
socket_io_client_new.dart