dart-eventsource icon indicating copy to clipboard operation
dart-eventsource copied to clipboard

Does not work on Web

Open Dampfwalze opened this issue 1 year ago • 5 comments

Sadly this package does not work on web, as it currently stands. This is not directly to blame to the package author, but has to do with the fact that the http package semantically implies that a StreamedResponse also works on web, which it doesn't. It will always return after the complete response is received, which obviously, breaks this package. There is an ongoing discussion though on the http package here: https://github.com/dart-lang/http/issues/595. There I posted an implementation of an http Client, that uses the fetch API to make the StreamedResponse work. You can pass it to EventSource.connect.

Additionally, I did not find any indication that this package does not work on web. You should indicate that in the README.

Dampfwalze avatar Apr 17 '23 12:04 Dampfwalze

Doesn't the README say that it does work on web?

// in browsers, you need to pass a http.BrowserClient:
EventSource eventSource = await EventSource.connect("http://example.com/events", 
    client: new http.BrowserClient());

stevenroose avatar Apr 19 '23 14:04 stevenroose

Well, no, it does not say it works. Your code snippet does not even make sense. The Client class from the http package is abstract and only an interface. The Client constructor is a factory constructor that delegates the creation of a client to a platform specific factory, using a platform switch:

dart-lang/http client.dart:32-37:

abstract class Client {
  /// Creates a new platform appropriate client.
  ///
  /// Creates an `IOClient` if `dart:io` is available and a `BrowserClient` if
  /// `dart:html` is available, otherwise it will throw an unsupported error.
  factory Client() => zoneClient ?? createClient();

dart-lang/http client.dart:14-16:

import 'client_stub.dart'
    if (dart.library.html) 'browser_client.dart'
    if (dart.library.io) 'io_client.dart';

dart-lang/http client_stub.dart:7-9:

/// Implemented in `browser_client.dart` and `io_client.dart`.
BaseClient createClient() => throw UnsupportedError(
    'Cannot create a client without dart:html or dart:io.');

dart-lang/http io_client.dart:12-21:

/// Create an [IOClient].
///
/// Used from conditional imports, matches the definition in `client_stub.dart`.
BaseClient createClient() {
  if (const bool.fromEnvironment('no_default_http_client')) {
    throw StateError('no_default_http_client was defined but runWithClient '
        'was not used to configure a Client implementation.');
  }
  return IOClient();
}

dart-lang/http browser_client.dart:15-24:

/// Create a [BrowserClient].
///
/// Used from conditional imports, matches the definition in `client_stub.dart`.
BaseClient createClient() {
  if (const bool.fromEnvironment('no_default_http_client')) {
    throw StateError('no_default_http_client was defined but runWithClient '
        'was not used to configure a Client implementation.');
  }
  return BrowserClient();
}

One can see that it will automatically create a BrowserClient on web and your code snippet won't change anything!

One can also prove this with the following code:

final client = http.Client();
print(client.runtimeType);

Output on desktop:

IOClient

Output on web:

BrowserClient

And it would not even compile on another platform, when one would try to import a platform specific implementation:

import 'package:http/browser_client.dart' as http; // => compiler error on desktop
import 'package:http/http.dart' as http;
// ...
final client = kIsWeb ? http.BrowserClient() : http.Client();

The problem, why your package does not work on web, lies in the BrowserClient itself and the fact, that it uses the old XMLHttpRequest javascript API, that simply does not provide the needed functionality.

In the following snippet, one can see, how the response is read till the end and stored as a Uint8List, which is just an array and not a stream, just to create a new stream from it, that emits all its data at once, using the ByteStream.fromBytes constructor:

dart-lang/http browser_client.dart:66-74:

unawaited(xhr.onLoad.first.then((_) {
  var body = (xhr.response as ByteBuffer).asUint8List();
  completer.complete(StreamedResponse(
      ByteStream.fromBytes(body), xhr.status!,
      contentLength: body.length,
      request: request,
      headers: xhr.responseHeaders,
      reasonPhrase: xhr.statusText));
}));

Dampfwalze avatar Apr 24 '23 12:04 Dampfwalze

About your first point, that must be a change in the http package, at the time this lib was written, you had to manually create a BrowserClient like the example snippet does. Feel free to fix that.

But if you're right that the http package internally doesn't allow for open http streams on web, that's an issue. Is there no way around that? How do WebSockets work then?

stevenroose avatar Apr 30 '23 14:04 stevenroose

i created a client that seems to work well on web, you can use it instead of BrowserClient :

class HttpRequestClient extends BaseClient {
  @override
  Future<StreamedResponse> send(BaseRequest request) async {
    final httpRequest = HttpRequest();
    final streamController = StreamController<List<int>>();
    httpRequest.open(request.method, request.url.toString());
    request.headers.forEach((key, value) {
      httpRequest.setRequestHeader(key, value);
    });
    int progress = 0;

    httpRequest.onProgress.listen((event) {
      final data = httpRequest.responseText!.substring(progress);
      progress += data.length;
      streamController.add(data.codeUnits);
    });

    httpRequest.onAbort.listen((event) {
      httpRequest.abort();
      streamController.close();
    });

    httpRequest.onLoadEnd.listen((event) {
      httpRequest.abort();
      streamController.close();
    });
    httpRequest.onError.listen((event) {
      streamController.addError(
        httpRequest.responseText ?? httpRequest.status ?? 'err',
      );
    });
    httpRequest.send();
    return StreamedResponse(streamController.stream, 200);
  }
}

mikemoore13 avatar Aug 23 '23 21:08 mikemoore13

I had got this to work on web by passing FetchClient from [package:fetch_client](https://pub.dev/packages/fetch_client]

yossriK avatar Dec 07 '23 20:12 yossriK