# socket.io-client-dart is a Dart port of the JavaScript socket.io client

> This package brings the socket.io client protocol to Dart and Flutter, with the same server compatibility table, the same option builder and the same event names as the JavaScript original. It also carries a migration story of its own, since the HTTP client adapter is deprecated and the custom WebSocket connector is taking over.

**rikulo/socket.io-client-dart** — socket.io-client-dart: Dartlang port of socket.io-client https://github.com/socketio/socket.io-client

- Repository: https://github.com/rikulo/socket.io-client-dart
- Website: https://quire.io
- Stars: 698 · Forks: 200
- Language: Dart
- License: MIT
- Published: 2026-10-21 · Updated: 2026-10-21 · Language: en
- Canonical page: https://hysenlabs.com/projects/rikulo-socket-io-client-dart

## A port, with a compatibility table instead of a promise

The package is described as a port of the Socket.io-client v2 through v4 JavaScript library, in Dart, and the README's first substantive section is a version table that maps your package version to the server versions it works with.

The `v0.9.*` through `v1.*` line of the package speaks to socket.io server `v2.*`. The `v2.*` line speaks to `v3.*` through `v4.6.*`. And the `v3.*` line speaks to `v4.7.*` and later `v4.*` releases.

That table is the whole adoption contract. The port is not version agnostic, and it is not version tolerant either. A Dart client on `v3.x` talking to a server on `v4.6` is outside the documented range, and the table does not say what happens there, only which combinations are considered supported. This is normal for a port of a protocol that is still moving, and it is worth checking before you upgrade either side.

The package is MIT licensed, the primary language is Dart, and the repository topics list Dart, Flutter, quire, socket-io, socket-io-client and websocket. The homepage field points at quire.io, which is not obviously connected to this project.

## The client surface mirrors the JavaScript original

The Dart client example in the README is short enough to read in full, and it is recognisably the socket.io client API with Dart syntax:

```dart
main() {
  // Dart client
  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(_));
}
```

The import is `package:socket_io_client/socket_io_client.dart` with the `IO` prefix, which is the convention that keeps the name out of your way in a codebase that also uses other socket libraries.

The behaviour is the same set of primitives you would use from JavaScript. `IO.io` creates the socket, `onConnect` and `onDisconnect` are the typed lifecycle callbacks, `on` takes an event name and a handler for arbitrary events, and `emit` sends data to the server. The server example above it shows the mirror image, with `io.of('/some')` for namespaces and a `connection` handler on the namespace.

The port also includes a Dart server implementation, in `package:socket_io/socket_io.dart`, so a Dart process can act as the socket.io server in tests or in a mixed stack.

## autoConnect is on by default, and connect() breaks it

One footgun is called out explicitly, and it has an issue number attached.

To connect the socket manually you set the option `autoConnect: false` and call `.connect()`. The note that follows is the important part: `.connect()` should not be called if `autoConnect: true`, and `autoConnect` is enabled to true by default, because this will cause all event handlers to get registered and fired twice. The README links issue 33.

That failure mode is nasty in practice. A duplicated handler does not throw, it prints twice, so the symptom is duplicate messages or duplicate state updates that appear only in some code paths, and the doubling appears and disappears depending on whether the code path reaches `connect()`.

The reason the option exists is the same reason the package has an option builder at all. A caller who needs to attach a custom transport, set extra headers or point the socket at a URL that is not known until later needs to defer the connection, and `autoConnect: false` is how you say that. The rule is simple once you know it: choose one or the other.

The extra headers can also be changed after the socket exists, with the caveat that you have to reconnect for the change to take effect:

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

## setWebSocketConnector replaces the deprecated HTTP client adapter

This is the most substantive piece of engineering in the package, and it is also where the recent churn is.

The `setWebSocketConnector` option lets you plug in a custom WebSocket implementation, and the README names the possibilities: `CupertinoWebSocket` and `OkHttpWebSocket` from `package:cupertino_http` and `package:cronet_http`, or a `dart:io` `HttpClient` with custom TLS and proxy settings. The connector receives the `extraHeaders` you set via `setExtraHeaders`, forwarded on native. Browsers are the exception, and the README is clear that browsers cannot send custom headers on the WebSocket handshake, so they are ignored there.

```dart
Socket socket = io('http://localhost:3000',
    OptionBuilder()
      .setTransports(['websocket'])
      .setWebSocketConnector((uri, {protocols, headers}) {
        final config = URLSessionConfiguration.defaultSessionConfiguration();
        if (headers != null) config.httpAdditionalHeaders = headers;
        return CupertinoWebSocket.connect(uri, protocols: protocols, config: config);
      })
      .build());
```

The old way is on its way out. `HttpClientAdapter` and `OptionBuilder.setHttpClientAdapter()` are deprecated since 3.1.6 and will be removed in 4.0.0, and the README explains an awkward episode behind that: they were unintentionally dropped in 3.1.5, issue 442, and restored as deprecated shims to keep `^3.1.x` builds compiling.

If you need a custom `dart:io` `HttpClient` on native, for a self-signed certificate or a proxy, the migration is to add `web_socket` to your dependencies and wrap the socket with `IOWebSocket.fromWebSocket`:

```dart
Socket socket = io('http://localhost:3000',
    OptionBuilder()
      .setTransports(['websocket'])
      .setWebSocketConnector((uri, {protocols, headers}) async {
        final raw = await WebSocket.connect(
          uri.toString(),
          protocols: protocols,
          headers: headers,
          customClient: myHttpClient, // your custom dart:io HttpClient
        );
        return IOWebSocket.fromWebSocket(raw);
      })
      .build());
```

If you are on 3.1.5, that is the version to read the release notes for.

## Flutter needs the websocket transport set by hand

There is a platform caveat that catches people out, and it is stated in its own section.

In a Flutter environment, but not in the Flutter Web environment, it only works with the `dart:io` websocket, not with the `dart:html` websocket or Ajax (XHR), so you have to add `setTransports(['websocket'])` when you create the socket instance:

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

The failure without it is not always a clean error. Because the default transports include the HTTP long-polling and XHR options, a Flutter application that does not set the transport explicitly can end up attempting a transport the platform does not support, and the symptom appears as a connection that never establishes rather than a message about the transport list.

The comment in the example, for Flutter or Dart VM, is the useful part to remember. On the web the browser handles the handshake and the header restrictions described earlier apply, so the custom connector path is a native-only concern.

## Events, acknowledgements and the reply pattern

The event list in the README is the full set you can listen on, and it is the JavaScript list unchanged: `connect`, `connect_error`, `disconnect`, `error`, `reconnect`, `reconnect_attempt`, `reconnect_failed`, `reconnect_error`, `ping` and `pong`.

```dart
const List EVENTS = [
  'connect',
  'connect_error',
  'disconnect',
  'error',
  'reconnect',
  'reconnect_attempt',
  'reconnect_failed',
  'reconnect_error',
  'ping',
  'pong'
];
```

The `onConnect` helper in the examples is a convenience wrapper over the `connect` event, and the README's note says to replace `onConnect` with any of the above events.

Acknowledgements work in both directions. The client side uses `emitWithAck`, which takes the event name, the payload and an `ack` callback that receives the server's reply:

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

And on the server side the acknowledgement callback is the last element of the received data list, which the handler extracts and calls:

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

The `as List` and `as Function` casts are a reminder of what this library is: a thin, faithful port rather than an idiomatic Dart abstraction, which is exactly what you want when the server is a stock socket.io server and nothing else.

## Streams for Flutter state, and what the repository holds

The README closes with a Flutter pattern for folding socket events into widget state, and it is the longest example in the document. A `StreamController` holds the incoming data, a `connectAndListen` function creates the socket and pushes events into the stream, and a `StreamBuilder` widget rebuilds from it. The pattern is worth reading because it shows the shape the package expects: the socket is imperative, and the bridge into Flutter is a stream you own.

The repository itself is a Dart package with `pubspec.yaml`, `lib/`, `test/`, a `test_nodejs/` directory, a `web/` directory, an `example/` folder, `analysis_options.yaml`, a `CHANGELOG.md`, `LICENSE` and a `contributing.md` file. The presence of a Node.js test directory suggests the port is checked against the original implementation's behaviour, which is the only sensible way to keep a protocol port honest, and the `web/` directory is the Dart web build, which is where the transport limitation from the Flutter section applies.

On maintenance, the repository is not archived and the last push was on 2026-06-13, the same day as the v3.1.6 release. The releases before it are v3.1.5 on 2026-06-07 and v3.1.4 on 2026-01-16. The 3.1.5 release is the one to read the notes for, because that is where the adapter removal and restoration happened, and the 4.0.0 removal of the deprecated shims is the next boundary on the horizon.

## Conclusion

Adopt socket.io-client-dart when your Dart or Flutter client has to talk to a socket.io server and you want the JavaScript client's protocol behaviour rather than a plain WebSocket. Do not adopt it as a general WebSocket library, because the transport is fixed to the socket.io handshake unless you restrict it to raw websocket transport, and do not stay on the deprecated HTTP client adapter, since it is scheduled for removal in 4.0.0. Verify first that your server version falls in the range the table lists for your package version, that on Flutter you have set `setTransports(['websocket'])` because the dart:html transport and XHR do not work there, and that you are not calling `connect()` while `autoConnect` is still enabled, which registers every handler twice.

## FAQ

### Which socket.io server versions work with socket.io-client-dart?

The README table maps package versions to server versions: v0.9.* through v1.* works with server v2.*, v2.* works with server v3.* through v4.6.*, and v3.* works with server v4.7.* and later v4.* releases.

### Why should I not call connect() on a socket.io-client-dart socket?

Because autoConnect is enabled to true by default, and calling `.connect()` on top of it causes all event handlers to get registered and fired twice. Set the option `autoConnect: false` first if you want to control when the connection is made, as the README explains with a link to issue 33.

### How do I use a custom WebSocket implementation in socket.io-client-dart?

Use `setWebSocketConnector` in the OptionBuilder, for example with `CupertinoWebSocket.connect` from package:cupertino_http, and it receives the extra headers you set on native. Browsers ignore custom headers on the WebSocket handshake, so this path matters on native and on Flutter.

### What is deprecated in socket.io-client-dart 3.1.6?

`HttpClientAdapter` and `OptionBuilder.setHttpClientAdapter()` are deprecated since 3.1.6 and will be removed in 4.0.0. They were unintentionally dropped in 3.1.5 and restored as deprecated shims to keep `^3.1.x` builds compiling, and the replacement is `setWebSocketConnector`.

## Sources

- [License: MIT](https://github.com/rikulo/socket.io-client-dart/blob/master/LICENSE)
- [Project website](https://quire.io)
- [README](https://github.com/rikulo/socket.io-client-dart/blob/master/README.md)
- [Releases](https://github.com/rikulo/socket.io-client-dart/releases)
- [rikulo/socket.io-client-dart on GitHub](https://github.com/rikulo/socket.io-client-dart)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/rikulo-socket-io-client-dart
