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

Migrate off legacy JS/HTML apis #715

Open
minoic opened this issue May 23, 2024 · 7 comments
Open

Migrate off legacy JS/HTML apis #715

minoic opened this issue May 23, 2024 · 7 comments

Comments

@minoic
Copy link

minoic commented May 23, 2024

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
Copy link
Contributor

mosuem commented May 23, 2024

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

@kevmoo kevmoo changed the title Support for flutter web wasm Migrate off legacy JS/HTML apis May 23, 2024
@hyunw55
Copy link

hyunw55 commented May 27, 2024

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:

  • grpc-3.2.4/lib/src/client/transport/xhr_transport.dart
// Copyright (c) 2018, the gRPC project authors. Please see the AUTHORS file
// for details. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

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<ByteBuffer> _incomingProcessor = StreamController();
  final StreamController<GrpcMessage> _incomingMessages = StreamController();
  final StreamController<List<int>> _outgoingMessages = StreamController();

  @override
  Stream<GrpcMessage> get incomingMessages => _incomingMessages.stream;

  @override
  StreamSink<List<int>> 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<int> 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<void> terminate() async {
    _close();
    _request.abort();
  }
}

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

  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<void> terminate() async {
    for (var request in List.of(_requests)) {
      request.terminate();
    }
  }

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

  @override
  Future<void> 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
Copy link

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

@kevmoo
Copy link
Contributor

kevmoo commented Jun 11, 2024

Everyone is busy. Happy to accept a pull request!

@zs-dima
Copy link

zs-dima commented Jun 13, 2024

+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
Copy link
Author

minoic commented Jun 14, 2024

+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.

@wSedlacek
Copy link

wSedlacek commented Jul 1, 2024

+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.

I was able to compile this, and send requests, but for server side streams I am not getting responses.
(I did not confirm if I get responses from unary calls)

I think the issue comes down to this code here.
https://github.com/minoic/grpc-dart/blob/d619e536e993b6f2ff7a743c6b1401073c7ad804/lib/src/client/transport/xhr_transport.dart#L56

The await is probably waiting for the entire response to finish which doesn't occur with server side streams.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

7 participants