Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Does not work on Web #31

Open
Dampfwalze opened this issue Apr 17, 2023 · 5 comments
Open

Does not work on Web #31

Dampfwalze opened this issue Apr 17, 2023 · 5 comments

Comments

@Dampfwalze
Copy link

Dampfwalze commented Apr 17, 2023

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: dart-lang/http#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.

@stevenroose
Copy link
Owner

stevenroose commented Apr 19, 2023

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());

@Dampfwalze
Copy link
Author

Dampfwalze commented Apr 24, 2023

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));
}));

@stevenroose
Copy link
Owner

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?

@mikemoore13
Copy link

mikemoore13 commented Aug 23, 2023

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);
  }
}

@yossriK
Copy link

yossriK commented Dec 7, 2023

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

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

No branches or pull requests

4 participants