grpc / grpc-dart

The Dart language implementation of gRPC.
https://pub.dev/packages/grpc
Apache License 2.0
835 stars 256 forks source link

Migrate off legacy JS/HTML apis #715

Open minoic opened 1 month ago

minoic commented 1 month ago

Wasm support is now stable since flutter 3.22 published, i got UNAVAILABLE error while testing my web app with wasm.

Version of the grpc-dart packages used: v3.2.4 and grpc/grpc-dart master branch.

Repro steps

  1. Create a grpc channel using GrpcOrGrpcWebClientChannel.toSeparateEndpoints, write some request code.
  2. Build: flutter build web --wasm.
  3. To run a Flutter app that has been compiled to Wasm, follow Support for WebAssembly (Wasm).

Expected result: The request successfully sent as canvaskit mode does.

Actual result:

gRPC Error (code: 14, codeName: UNAVAILABLE, message: Error connecting: Unsupported operation: SecurityContext constructor, details: null, rawResponse: null, trailers: {})

Details

This error could be caused by two points:

  1. Wrong package imported in grpc_or_grpcweb.dart. Because of wasm has no html package, conditional import recognized wasm as non-web platform.
  2. Once correct platform recognized, xhr_transport.dart will be used to transport data, which uses html package, but "dart:html is being replaced with package:web. Package maintainers should migrate to package:web as soon as possible to be compatible with Wasm. Read the Migrate to package:web page for guidance."

I tried to fix it in https://github.com/minoic/grpc-dart, it worked in my case but more test should be performed later.

mosuem commented 1 month ago

Thanks for reporting this, we should migrate package:grpc off dart:html. cc @kevmoo

hyunw55 commented 1 month ago

I received a suggestion for an alternative solution using GPT-4 through gpt4o. This approach has been working well for me, but it still needs more testing. I hope everyone can review and provide feedback.

original: https://github.com/grpc/grpc-dart/blob/master/lib/src/client/transport/xhr_transport.dart

Please find the suggested solution below:

import 'dart:async'; import 'dart:typed_data'; import 'package:web/web.dart'; import 'package:meta/meta.dart'; import '../../client/call.dart'; import '../../shared/message.dart'; import '../../shared/status.dart'; import '../connection.dart'; import 'cors.dart' as cors; import 'transport.dart'; import 'web_streams.dart'; import 'dart:js_interop';

@JS('Uint8Array') @staticInterop class JSUint8Array { external factory JSUint8Array(JSAny data); }

const _contentTypeKey = 'Content-Type';

class XhrTransportStream implements GrpcTransportStream { final XMLHttpRequest _request; final ErrorHandler _onError; final Function(XhrTransportStream stream) _onDone; bool _headersReceived = false; int _requestBytesRead = 0; final StreamController _incomingProcessor = StreamController(); final StreamController _incomingMessages = StreamController(); final StreamController<List> _outgoingMessages = StreamController();

@override Stream get incomingMessages => _incomingMessages.stream;

@override StreamSink<List> get outgoingMessages => _outgoingMessages.sink;

XhrTransportStream(this._request, {required ErrorHandler onError, required onDone}) : _onError = onError, _onDone = onDone { _outgoingMessages.stream.map(frame).listen((data) { _sendRequest(data); }, cancelOnError: true);

_request.onReadyStateChange.listen((_) {
  if (_incomingProcessor.isClosed) {
    return;
  }
  switch (_request.readyState) {
    case 2:
      _onHeadersReceived();
      break;
    case 4:
      _onRequestDone();
      _close();
      break;
  }
});

_request.onError.listen((ProgressEvent event) {
  if (_incomingProcessor.isClosed) {
    return;
  }
  _onError(GrpcError.unavailable('XhrConnection connection-error'),
      StackTrace.current);
  terminate();
});

_request.onProgress.listen((_) {
  if (_incomingProcessor.isClosed) {
    return;
  }
  final responseText = _request.responseText;
  final bytes = Uint8List.fromList(
          responseText.substring(_requestBytesRead).codeUnits)
      .buffer;
  _requestBytesRead = responseText.length;
  _incomingProcessor.add(bytes);
});

_incomingProcessor.stream
    .transform(GrpcWebDecoder())
    .transform(grpcDecompressor())
    .listen(_incomingMessages.add,
        onError: _onError, onDone: _incomingMessages.close);

}

void _sendRequest(List data) { try { if (data.isEmpty) { data = List.filled(5, 0); } final uint8Data = Int8List.fromList(data).toJS; // 변환을 사용 _request.send(uint8Data); } catch (e) { _onError(e, StackTrace.current); } }

void _onHeadersReceived() { _headersReceived = true; final responseHeaders = _request.getAllResponseHeaders(); final headersMap = parseHeaders(responseHeaders); final metadata = GrpcMetadata(headersMap); _incomingMessages.add(metadata); }

void _onRequestDone() { if (!_headersReceived) { _onHeadersReceived(); } if (_request.status != 200) { _onError( GrpcError.unavailable( 'Request failed with status: ${_request.status}', null, _request.responseText), StackTrace.current); } }

bool _validateResponseState() { try { final headersMap = parseHeaders(_request.getAllResponseHeaders()); validateHttpStatusAndContentType(_request.status, headersMap, rawResponse: _request.responseText); return true; } catch (e, st) { _onError(e, st); return false; } }

void _close() { _incomingProcessor.close(); _outgoingMessages.close(); _onDone(this); }

@override Future terminate() async { _close(); _request.abort(); } }

class XhrClientConnection implements ClientConnection { final Uri uri; final _requests = {};

XhrClientConnection(this.uri);

@override String get authority => uri.authority; @override String get scheme => uri.scheme;

void _initializeRequest( XMLHttpRequest request, Map<String, String> metadata) { metadata.forEach((key, value) { request.setRequestHeader(key, value); }); request.overrideMimeType('text/plain; charset=x-user-defined'); request.responseType = 'text'; }

@visibleForTesting XMLHttpRequest createHttpRequest() => XMLHttpRequest();

@override GrpcTransportStream makeRequest(String path, Duration? timeout, Map<String, String> metadata, ErrorHandler onError, {CallOptions? callOptions}) { if (_getContentTypeHeader(metadata) == null) { metadata['Content-Type'] = 'application/grpc-web+proto'; metadata['X-User-Agent'] = 'grpc-web-dart/0.1'; metadata['X-Grpc-Web'] = '1'; }

var requestUri = uri.resolve(path);

if (callOptions is WebCallOptions &&
    callOptions.bypassCorsPreflight == true) {
  requestUri = cors.moveHttpHeadersToQueryParam(metadata, requestUri);
}

final request = createHttpRequest();
request.open('POST', requestUri.toString());

if (callOptions is WebCallOptions && callOptions.withCredentials == true) {
  request.withCredentials = true;
}

_initializeRequest(request, metadata);

final transportStream =
    XhrTransportStream(request, onError: onError, onDone: _removeStream);
_requests.add(transportStream);
return transportStream;

}

void _removeStream(XhrTransportStream stream) { _requests.remove(stream); }

@override Future terminate() async { for (var request in List.of(_requests)) { request.terminate(); } }

@override void dispatchCall(ClientCall call) { call.onConnectionReady(this); }

@override Future shutdown() async {}

@override set onStateChanged(void Function(ConnectionState) cb) { // Do nothing. } }

MapEntry<String, String>? _getContentTypeHeader(Map<String, String> metadata) { for (var entry in metadata.entries) { if (entry.key.toLowerCase() == _contentTypeKey.toLowerCase()) { return entry; } } return null; }

Map<String, String> parseHeaders(String rawHeaders) { final headers = <String, String>{}; final lines = rawHeaders.split('\r\n'); for (var line in lines) { final index = line.indexOf(': '); if (index != -1) { final key = line.substring(0, index); final value = line.substring(index + 2); headers[key] = value; } } return headers; }

r-durao-pvotal commented 2 weeks ago

Is there any ETA on this @mosuem , @kevmoo ?

kevmoo commented 2 weeks ago

Everyone is busy. Happy to accept a pull request!

zs-dima commented 2 weeks ago

+1 Error: /home/flutter/.pub-cache/hosted/pub.dev/grpc-3.2.4/lib/src/client/transport/xhr_transport.dart:17:8: Error: Dart library 'dart:html' is not available on this platform.

minoic commented 2 weeks ago

+1 Error: /home/flutter/.pub-cache/hosted/pub.dev/grpc-3.2.4/lib/src/client/transport/xhr_transport.dart:17:8: Error: Dart library 'dart:html' is not available on this platform.

You can try my fixed version by editing pubspec.yml file:

dependencies:
  grpc:
    git:
      url: https://github.com/minoic/grpc-dart.git

It seems to work but I can't guarantee its stability.