Skip to content

Fix: Migrate off legacy JS/HTML APIs #750

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

Merged
merged 10 commits into from
Feb 18, 2025
Merged
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
the connection, as defined in the gRPC spec.
* Upgrade to `package:lints` version 5.0.0 and Dart SDK version 3.5.0.
* Upgrade `example/grpc-web` code.
* Update xhr transport to migrate off legacy JS/HTML apis.

## 4.0.1

Expand Down
2 changes: 1 addition & 1 deletion lib/grpc_or_grpcweb.dart
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
// limitations under the License.

import 'src/client/grpc_or_grpcweb_channel_grpc.dart'
if (dart.library.html) 'src/client/grpc_or_grpcweb_channel_web.dart';
if (dart.library.js_interop) 'src/client/grpc_or_grpcweb_channel_web.dart';
import 'src/client/http2_channel.dart';
import 'src/client/options.dart';

Expand Down
154 changes: 132 additions & 22 deletions lib/src/client/transport/xhr_transport.dart
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,11 @@
// limitations under the License.

import 'dart:async';
// ignore: deprecated_member_use (#756)
import 'dart:html';
import 'dart:js_interop';
import 'dart:typed_data';

import 'package:meta/meta.dart';
import 'package:web/web.dart';

import '../../client/call.dart';
import '../../shared/message.dart';
Expand All @@ -31,7 +31,7 @@ import 'web_streams.dart';
const _contentTypeKey = 'Content-Type';

class XhrTransportStream implements GrpcTransportStream {
final HttpRequest _request;
final IXMLHttpRequest _request;
final ErrorHandler _onError;
final Function(XhrTransportStream stream) _onDone;
bool _headersReceived = false;
Expand All @@ -50,19 +50,20 @@ class XhrTransportStream implements GrpcTransportStream {
{required ErrorHandler onError, required onDone})
: _onError = onError,
_onDone = onDone {
_outgoingMessages.stream
.map(frame)
.listen((data) => _request.send(data), cancelOnError: true);
_outgoingMessages.stream.map(frame).listen(
(data) => _request.send(Uint8List.fromList(data).toJS),
cancelOnError: true,
onError: _onError);

_request.onReadyStateChange.listen((data) {
_request.onReadyStateChange.listen((_) {
if (_incomingProcessor.isClosed) {
return;
}
switch (_request.readyState) {
case HttpRequest.HEADERS_RECEIVED:
case XMLHttpRequest.HEADERS_RECEIVED:
_onHeadersReceived();
break;
case HttpRequest.DONE:
case XMLHttpRequest.DONE:
_onRequestDone();
_close();
break;
Expand All @@ -82,13 +83,11 @@ class XhrTransportStream implements GrpcTransportStream {
if (_incomingProcessor.isClosed) {
return;
}
// Use response over responseText as most browsers don't support
// using responseText during an onProgress event.
final responseString = _request.response as String;
final responseText = _request.responseText;
final bytes = Uint8List.fromList(
responseString.substring(_requestBytesRead).codeUnits)
responseText.substring(_requestBytesRead).codeUnits)
.buffer;
_requestBytesRead = responseString.length;
_requestBytesRead = responseText.length;
_incomingProcessor.add(bytes);
});

Expand Down Expand Up @@ -123,9 +122,11 @@ class XhrTransportStream implements GrpcTransportStream {
if (!_headersReceived && !_validateResponseState()) {
return;
}
if (_request.response == null) {
if (_request.status != 200) {
_onError(
GrpcError.unavailable('XhrConnection request null response', null,
GrpcError.unavailable(
'Request failed with status: ${_request.status}',
null,
_request.responseText),
StackTrace.current);
return;
Expand All @@ -145,6 +146,110 @@ class XhrTransportStream implements GrpcTransportStream {
}
}

// XMLHttpRequest is an extension type and can't be extended or implemented.
// This interface is used to allow for mocking XMLHttpRequest in tests of
// XhrClientConnection.
@visibleForTesting
abstract interface class IXMLHttpRequest {
Stream<Event> get onReadyStateChange;
Stream<ProgressEvent> get onProgress;
Stream<ProgressEvent> get onError;
int get readyState;
JSAny? get response;
String get responseText;
Map<String, String> get responseHeaders;
int get status;

set responseType(String responseType);
set withCredentials(bool withCredentials);

void abort();
void open(
String method,
String url, [
// external default is true
bool async = true,
String? username,
String? password,
]);
void overrideMimeType(String mimeType);
void send([JSAny? body]);
void setRequestHeader(String header, String value);

// This method should only be used in production code.
XMLHttpRequest toXMLHttpRequest();
}

// IXMLHttpRequest that delegates to a real XMLHttpRequest.
class XMLHttpRequestImpl implements IXMLHttpRequest {
final XMLHttpRequest _xhr = XMLHttpRequest();

XMLHttpRequestImpl();

@override
Stream<Event> get onReadyStateChange => _xhr.onReadyStateChange;
@override
Stream<ProgressEvent> get onProgress => _xhr.onProgress;
@override
Stream<ProgressEvent> get onError => _xhr.onError;
@override
int get readyState => _xhr.readyState;
@override
Map<String, String> get responseHeaders => _xhr.responseHeaders;
@override
JSAny? get response => _xhr.response;
@override
String get responseText => _xhr.responseText;
@override
int get status => _xhr.status;

@override
set responseType(String responseType) {
_xhr.responseType = responseType;
}

@override
set withCredentials(bool withCredentials) {
_xhr.withCredentials = withCredentials;
}

@override
void abort() {
_xhr.abort();
}

@override
void open(
String method,
String url, [
bool async = true,
String? username,
String? password,
]) {
_xhr.open(method, url, async, username, password);
}

@override
void overrideMimeType(String mimeType) {
_xhr.overrideMimeType(mimeType);
}

@override
void setRequestHeader(String header, String value) {
_xhr.setRequestHeader(header, value);
}

@override
void send([JSAny? body]) {
_xhr.send(body);
}

@override
XMLHttpRequest toXMLHttpRequest() {
return _xhr;
}
}

class XhrClientConnection implements ClientConnection {
final Uri uri;

Expand All @@ -154,20 +259,20 @@ class XhrClientConnection implements ClientConnection {

@override
String get authority => uri.authority;

@override
String get scheme => uri.scheme;

void _initializeRequest(HttpRequest request, Map<String, String> metadata) {
for (final header in metadata.keys) {
request.setRequestHeader(header, metadata[header]!);
}
void _initializeRequest(
IXMLHttpRequest request, Map<String, String> metadata) {
metadata.forEach(request.setRequestHeader);
// Overriding the mimetype allows us to stream and parse the data
request.overrideMimeType('text/plain; charset=x-user-defined');
request.responseType = 'text';
}

@visibleForTesting
HttpRequest createHttpRequest() => HttpRequest();
IXMLHttpRequest createHttpRequest() => XMLHttpRequestImpl();

@override
GrpcTransportStream makeRequest(String path, Duration? timeout,
Expand Down Expand Up @@ -195,11 +300,16 @@ class XhrClientConnection implements ClientConnection {
_initializeRequest(request, metadata);

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

XhrTransportStream _createXhrTransportStream(IXMLHttpRequest request,
ErrorHandler onError, void Function(XhrTransportStream stream) onDone) {
return XhrTransportStream(request, onError: onError, onDone: onDone);
}

void _removeStream(XhrTransportStream stream) {
_requests.remove(stream);
}
Expand Down
1 change: 1 addition & 0 deletions pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ dependencies:
http2: ^2.2.0
protobuf: '>=2.0.0 <4.0.0'
clock: ^1.1.1
web: ^1.1.0

dev_dependencies:
build_runner: ^2.0.0
Expand Down
Loading