This commit is contained in:
Devon Carew 2025-06-11 09:19:36 -07:00
parent 86f2dac45a
commit d75a3ef7c4
72 changed files with 2752 additions and 1721 deletions

View File

@ -27,7 +27,7 @@ jobs:
- name: Install dependencies
run: dart pub get
- name: Check formatting (using dev dartfmt release)
if: ${{ matrix.sdk == 'dev' }}
if: ${{ matrix.sdk == 'stable' }}
run: dart format --output=none --set-exit-if-changed .
- name: Analyze code (introp and examples)
run: |

View File

@ -1,6 +1,7 @@
## 4.1.1-wip
- Require Dart 3.8.
- Dart format all files for the new 3.8 formatter.
- Require package:googleapis_auth
- Require package:http 1.4.0
- Require package:lints 6.0.0

View File

@ -25,8 +25,10 @@ import 'package:grpc/grpc.dart';
Future<void> main() async {
final serviceAccountFile = File('logging-service-account.json');
if (!serviceAccountFile.existsSync()) {
print('File logging-service-account.json not found. Please follow the '
'steps in README.md to create it.');
print(
'File logging-service-account.json not found. Please follow the '
'steps in README.md to create it.',
);
exit(-1);
}
@ -36,19 +38,25 @@ Future<void> main() async {
];
final authenticator = ServiceAccountAuthenticator(
serviceAccountFile.readAsStringSync(), scopes);
serviceAccountFile.readAsStringSync(),
scopes,
);
final projectId = authenticator.projectId;
final channel = ClientChannel('logging.googleapis.com');
final logging =
LoggingServiceV2Client(channel, options: authenticator.toCallOptions);
final logging = LoggingServiceV2Client(
channel,
options: authenticator.toCallOptions,
);
final request = WriteLogEntriesRequest()
..entries.add(LogEntry()
..logName = 'projects/$projectId/logs/example'
..severity = LogSeverity.INFO
..resource = (MonitoredResource()..type = 'global')
..textPayload = 'This is a log entry!');
..entries.add(
LogEntry()
..logName = 'projects/$projectId/logs/example'
..severity = LogSeverity.INFO
..resource = (MonitoredResource()..type = 'global')
..textPayload = 'This is a log entry!',
);
await logging.writeLogEntries(request);
await channel.shutdown();

View File

@ -39,11 +39,17 @@ class EchoApp {
..message = message
..messageCount = count
..messageInterval = 500;
_service.serverStreamingEcho(request).listen((response) {
_addRightMessage(response.message);
}, onError: (error) {
_addRightMessage(error.toString());
}, onDone: () => print('Closed connection to server.'));
_service
.serverStreamingEcho(request)
.listen(
(response) {
_addRightMessage(response.message);
},
onError: (error) {
_addRightMessage(error.toString());
},
onDone: () => print('Closed connection to server.'),
);
}
void _addLeftMessage(String message) {
@ -55,13 +61,20 @@ class EchoApp {
}
void _addMessage(String message, String cssClass) {
document.querySelector('#first')!.after(HTMLDivElement()
..classList.add('row')
..append(HTMLHeadingElement.h2()
..append(HTMLSpanElement()
..classList.add('label')
..classList.addAll(cssClass)
..textContent = message)));
document
.querySelector('#first')!
.after(
HTMLDivElement()
..classList.add('row')
..append(
HTMLHeadingElement.h2()..append(
HTMLSpanElement()
..classList.add('label')
..classList.addAll(cssClass)
..textContent = message,
),
),
);
}
}

View File

@ -23,8 +23,9 @@ Future<void> main(List<String> args) async {
port: 50051,
options: ChannelOptions(
credentials: ChannelCredentials.insecure(),
codecRegistry:
CodecRegistry(codecs: const [GzipCodec(), IdentityCodec()]),
codecRegistry: CodecRegistry(
codecs: const [GzipCodec(), IdentityCodec()],
),
),
);
final stub = GreeterClient(channel);

View File

@ -20,8 +20,10 @@ import 'package:helloworld/src/generated/helloworld.pbgrpc.dart';
/// Dart implementation of the gRPC helloworld.Greeter client.
Future<void> main(List<String> args) async {
final udsAddress =
InternetAddress('localhost', type: InternetAddressType.unix);
final udsAddress = InternetAddress(
'localhost',
type: InternetAddressType.unix,
);
final channel = ClientChannel(
udsAddress,
port: 0,

View File

@ -27,8 +27,10 @@ class GreeterService extends GreeterServiceBase {
}
Future<void> main(List<String> args) async {
final udsAddress =
InternetAddress('localhost', type: InternetAddressType.unix);
final udsAddress = InternetAddress(
'localhost',
type: InternetAddressType.unix,
);
final server = Server.create(services: [GreeterService()]);
await server.serve(address: udsAddress);
print('Start UNIX Server @localhost...');

View File

@ -24,10 +24,11 @@ class Client {
late MetadataClient stub;
Future<void> main(List<String> args) async {
channel = ClientChannel('127.0.0.1',
port: 8080,
options:
const ChannelOptions(credentials: ChannelCredentials.insecure()));
channel = ClientChannel(
'127.0.0.1',
port: 8080,
options: const ChannelOptions(credentials: ChannelCredentials.insecure()),
);
stub = MetadataClient(channel);
// Run all of the demos in order.
await runEcho();
@ -44,8 +45,10 @@ class Client {
/// metadata.
Future<void> runEcho() async {
final request = Record()..value = 'Kaj';
final call =
stub.echo(request, options: CallOptions(metadata: {'peer': 'Verner'}));
final call = stub.echo(
request,
options: CallOptions(metadata: {'peer': 'Verner'}),
);
call.headers.then((headers) {
print('Received header metadata: $headers');
});
@ -62,11 +65,15 @@ class Client {
/// well as a per-call metadata. The server will delay the response for the
/// requested duration, during which the client will cancel the RPC.
Future<void> runEchoDelayCancel() async {
final stubWithCustomOptions = MetadataClient(channel,
options: CallOptions(metadata: {'peer': 'Verner'}));
final stubWithCustomOptions = MetadataClient(
channel,
options: CallOptions(metadata: {'peer': 'Verner'}),
);
final request = Record()..value = 'Kaj';
final call = stubWithCustomOptions.echo(request,
options: CallOptions(metadata: {'delay': '1'}));
final call = stubWithCustomOptions.echo(
request,
options: CallOptions(metadata: {'delay': '1'}),
);
call.headers.then((headers) {
print('Received header metadata: $headers');
});
@ -89,8 +96,9 @@ class Client {
/// receiving 3 responses.
Future<void> runAddOneCancel() async {
final numbers = StreamController<int>();
final call =
stub.addOne(numbers.stream.map((value) => Number()..value = value));
final call = stub.addOne(
numbers.stream.map((value) => Number()..value = value),
);
final receivedThree = Completer<bool>();
final sub = call.listen((number) {
print('AddOneCancel: Received ${number.value}');
@ -133,8 +141,10 @@ class Client {
/// Call an RPC that returns a stream of Fibonacci numbers, and specify an RPC
/// timeout of 2 seconds.
Future<void> runFibonacciTimeout() async {
final call = stub.fibonacci(Empty(),
options: CallOptions(timeout: Duration(seconds: 2)));
final call = stub.fibonacci(
Empty(),
options: CallOptions(timeout: Duration(seconds: 2)),
);
var count = 0;
try {
await for (var number in call) {

View File

@ -24,12 +24,15 @@ class Client {
late RouteGuideClient stub;
Future<void> main(List<String> args) async {
final channel = ClientChannel('127.0.0.1',
port: 8080,
options:
const ChannelOptions(credentials: ChannelCredentials.insecure()));
stub = RouteGuideClient(channel,
options: CallOptions(timeout: Duration(seconds: 30)));
final channel = ClientChannel(
'127.0.0.1',
port: 8080,
options: const ChannelOptions(credentials: ChannelCredentials.insecure()),
);
stub = RouteGuideClient(
channel,
options: CallOptions(timeout: Duration(seconds: 30)),
);
// Run all of the demos in order.
try {
await runGetFeature();
@ -49,7 +52,8 @@ class Client {
? 'no feature'
: 'feature called "${feature.name}"';
print(
'Found $name at ${latitude / coordFactor}, ${longitude / coordFactor}');
'Found $name at ${latitude / coordFactor}, ${longitude / coordFactor}',
);
}
/// Run the getFeature demo. Calls getFeature with a point known to have a
@ -96,7 +100,8 @@ class Client {
for (var i = 0; i < count; i++) {
final point = featuresDb[random.nextInt(featuresDb.length)].location;
print(
'Visiting point ${point.latitude / coordFactor}, ${point.longitude / coordFactor}');
'Visiting point ${point.latitude / coordFactor}, ${point.longitude / coordFactor}',
);
yield point;
await Future.delayed(Duration(milliseconds: 200 + random.nextInt(100)));
}
@ -132,8 +137,10 @@ class Client {
for (final note in notes) {
// Short delay to simulate some other interaction.
await Future.delayed(Duration(milliseconds: 10));
print('Sending message ${note.message} at ${note.location.latitude}, '
'${note.location.longitude}');
print(
'Sending message ${note.message} at ${note.location.latitude}, '
'${note.location.longitude}',
);
yield note;
}
}
@ -141,7 +148,8 @@ class Client {
final call = stub.routeChat(outgoingNotes());
await for (var note in call) {
print(
'Got message ${note.message} at ${note.location.latitude}, ${note.location.longitude}');
'Got message ${note.message} at ${note.location.latitude}, ${note.location.longitude}',
);
}
}
}

View File

@ -28,8 +28,10 @@ class RouteGuideService extends RouteGuideServiceBase {
/// The [context] object provides access to client metadata, cancellation, etc.
@override
Future<Feature> getFeature(grpc.ServiceCall call, Point request) async {
return featuresDb.firstWhere((f) => f.location == request,
orElse: () => Feature()..location = request);
return featuresDb.firstWhere(
(f) => f.location == request,
orElse: () => Feature()..location = request,
);
}
Rectangle _normalize(Rectangle r) {
@ -57,7 +59,9 @@ class RouteGuideService extends RouteGuideServiceBase {
/// rectangle.
@override
Stream<Feature> listFeatures(
grpc.ServiceCall call, Rectangle request) async* {
grpc.ServiceCall call,
Rectangle request,
) async* {
final normalizedRectangle = _normalize(request);
// For each feature, check if it is in the given bounding box
for (var feature in featuresDb) {
@ -74,7 +78,9 @@ class RouteGuideService extends RouteGuideServiceBase {
/// total distance traveled, and total time spent.
@override
Future<RouteSummary> recordRoute(
grpc.ServiceCall call, Stream<Point> request) async {
grpc.ServiceCall call,
Stream<Point> request,
) async {
var pointCount = 0;
var featureCount = 0;
var distance = 0.0;
@ -84,8 +90,9 @@ class RouteGuideService extends RouteGuideServiceBase {
await for (var location in request) {
if (!timer.isRunning) timer.start();
pointCount++;
final feature =
featuresDb.firstWhereOrNull((f) => f.location == location);
final feature = featuresDb.firstWhereOrNull(
(f) => f.location == location,
);
if (feature != null) {
featureCount++;
}
@ -107,7 +114,9 @@ class RouteGuideService extends RouteGuideServiceBase {
/// locations.
@override
Stream<RouteNote> routeChat(
grpc.ServiceCall call, Stream<RouteNote> request) async* {
grpc.ServiceCall call,
Stream<RouteNote> request,
) async* {
await for (var note in request) {
final notes = routeNotes.putIfAbsent(note.location, () => <RouteNote>[]);
for (var note in notes) {
@ -134,7 +143,8 @@ class RouteGuideService extends RouteGuideServiceBase {
final dLat = toRadians(lat2 - lat1);
final dLon = toRadians(lon2 - lon1);
final a = sin(dLat / 2) * sin(dLat / 2) +
final a =
sin(dLat / 2) * sin(dLat / 2) +
cos(phi1) * cos(phi2) * sin(dLon / 2) * sin(dLon / 2);
final c = 2 * atan2(sqrt(a), sqrt(1 - a));

View File

@ -61,50 +61,77 @@ const _serviceAccountKeyFileArgument = 'service_account_key_file';
/// checking.
Future<int> main(List<String> args) async {
final argumentParser = ArgParser();
argumentParser.addOption(_serverHostArgument,
help: 'The server host to connect to. For example, "localhost" or '
'"127.0.0.1".');
argumentParser.addOption(_serverHostOverrideArgument,
help: 'The server host to claim to be connecting to, for use in TLS and '
'HTTP/2 :authority header. If unspecified, the value of '
'--server_host will be used.');
argumentParser.addOption(_serverPortArgument,
help: 'The server port to connect to. For example, "8080".');
argumentParser.addOption(_testCaseArgument,
help:
'The name of the test case to execute. For example, "empty_unary".');
argumentParser.addOption(_useTLSArgument,
defaultsTo: 'false',
help: 'Whether to use a plaintext or encrypted connection.');
argumentParser.addOption(_useTestCAArgument,
defaultsTo: 'false',
help: 'Whether to replace platform root CAs with ca.pem as the CA root.');
argumentParser.addOption(_defaultServiceAccountArgument,
help: 'Email of the GCE default service account.');
argumentParser.addOption(_oauthScopeArgument,
help: 'OAuth scope. For example, '
'"https://www.googleapis.com/auth/xapi.zoo".');
argumentParser.addOption(_serviceAccountKeyFileArgument,
help: 'The path to the service account JSON key file generated from GCE '
'developer console.');
argumentParser.addOption(
_serverHostArgument,
help:
'The server host to connect to. For example, "localhost" or '
'"127.0.0.1".',
);
argumentParser.addOption(
_serverHostOverrideArgument,
help:
'The server host to claim to be connecting to, for use in TLS and '
'HTTP/2 :authority header. If unspecified, the value of '
'--server_host will be used.',
);
argumentParser.addOption(
_serverPortArgument,
help: 'The server port to connect to. For example, "8080".',
);
argumentParser.addOption(
_testCaseArgument,
help: 'The name of the test case to execute. For example, "empty_unary".',
);
argumentParser.addOption(
_useTLSArgument,
defaultsTo: 'false',
help: 'Whether to use a plaintext or encrypted connection.',
);
argumentParser.addOption(
_useTestCAArgument,
defaultsTo: 'false',
help: 'Whether to replace platform root CAs with ca.pem as the CA root.',
);
argumentParser.addOption(
_defaultServiceAccountArgument,
help: 'Email of the GCE default service account.',
);
argumentParser.addOption(
_oauthScopeArgument,
help:
'OAuth scope. For example, '
'"https://www.googleapis.com/auth/xapi.zoo".',
);
argumentParser.addOption(
_serviceAccountKeyFileArgument,
help:
'The path to the service account JSON key file generated from GCE '
'developer console.',
);
final arguments = argumentParser.parse(args);
late Tester testClient;
try {
testClient = Tester(
serverHost: arguments[_serverHostArgument] ??
(throw 'Must specify --$_serverHostArgument'),
serverHostOverride: arguments[_serverHostOverrideArgument],
serverPort: int.tryParse(arguments[_serverPortArgument] ??
(throw 'Must specify --$_serverPortArgument')) ??
(throw 'Invalid port "${arguments[_serverPortArgument]}"'),
testCase: arguments[_testCaseArgument] ??
(throw 'Must specify --$_testCaseArgument'),
useTls: arguments[_useTLSArgument] == 'true',
useTestCA: arguments[_useTestCAArgument] == 'true',
defaultServiceAccount: arguments[_defaultServiceAccountArgument],
oauthScope: arguments[_oauthScopeArgument],
serviceAccountKeyFile: arguments[_serviceAccountKeyFileArgument]);
serverHost:
arguments[_serverHostArgument] ??
(throw 'Must specify --$_serverHostArgument'),
serverHostOverride: arguments[_serverHostOverrideArgument],
serverPort:
int.tryParse(
arguments[_serverPortArgument] ??
(throw 'Must specify --$_serverPortArgument'),
) ??
(throw 'Invalid port "${arguments[_serverPortArgument]}"'),
testCase:
arguments[_testCaseArgument] ??
(throw 'Must specify --$_testCaseArgument'),
useTls: arguments[_useTLSArgument] == 'true',
useTestCA: arguments[_useTestCAArgument] == 'true',
defaultServiceAccount: arguments[_defaultServiceAccountArgument],
oauthScope: arguments[_oauthScopeArgument],
serviceAccountKeyFile: arguments[_serviceAccountKeyFileArgument],
);
} catch (e) {
print(e);
print(argumentParser.usage);

View File

@ -47,10 +47,14 @@ class TestService extends TestServiceBase {
@override
Future<SimpleResponse> unaryCall(
ServiceCall call, SimpleRequest request) async {
ServiceCall call,
SimpleRequest request,
) async {
if (request.responseStatus.code != 0) {
throw GrpcError.custom(
request.responseStatus.code, request.responseStatus.message);
request.responseStatus.code,
request.responseStatus.message,
);
}
final payload = Payload()..body = List.filled(request.responseSize, 0);
return SimpleResponse()..payload = payload;
@ -58,7 +62,9 @@ class TestService extends TestServiceBase {
@override
Future<SimpleResponse> cacheableUnaryCall(
ServiceCall call, SimpleRequest request) async {
ServiceCall call,
SimpleRequest request,
) async {
final timestamp = DateTime.now().microsecond * 1000;
final responsePayload = Payload()..body = ascii.encode('$timestamp');
return SimpleResponse()..payload = responsePayload;
@ -66,9 +72,13 @@ class TestService extends TestServiceBase {
@override
Future<StreamingInputCallResponse> streamingInputCall(
ServiceCall call, Stream<StreamingInputCallRequest> request) async {
ServiceCall call,
Stream<StreamingInputCallRequest> request,
) async {
final aggregatedPayloadSize = await request.fold<int>(
0, (size, message) => size + message.payload.body.length);
0,
(size, message) => size + message.payload.body.length,
);
return StreamingInputCallResponse()
..aggregatedPayloadSize = aggregatedPayloadSize;
}
@ -78,7 +88,9 @@ class TestService extends TestServiceBase {
@override
Stream<StreamingOutputCallResponse> streamingOutputCall(
ServiceCall call, StreamingOutputCallRequest request) async* {
ServiceCall call,
StreamingOutputCallRequest request,
) async* {
for (final entry in request.responseParameters) {
if (entry.intervalUs > 0) {
await Future.delayed(Duration(microseconds: entry.intervalUs));
@ -88,10 +100,13 @@ class TestService extends TestServiceBase {
}
StreamingOutputCallResponse _responseForRequest(
StreamingOutputCallRequest request) {
StreamingOutputCallRequest request,
) {
if (request.responseStatus.code != 0) {
throw GrpcError.custom(
request.responseStatus.code, request.responseStatus.message);
request.responseStatus.code,
request.responseStatus.message,
);
}
final response = StreamingOutputCallResponse();
if (request.responseParameters.isNotEmpty) {
@ -102,13 +117,17 @@ class TestService extends TestServiceBase {
@override
Stream<StreamingOutputCallResponse> fullDuplexCall(
ServiceCall call, Stream<StreamingOutputCallRequest> request) async* {
ServiceCall call,
Stream<StreamingOutputCallRequest> request,
) async* {
yield* request.map(_responseForRequest);
}
@override
Stream<StreamingOutputCallResponse> halfDuplexCall(
ServiceCall call, Stream<StreamingOutputCallRequest> request) async* {
ServiceCall call,
Stream<StreamingOutputCallRequest> request,
) async* {
final bufferedResponses = await request.map(_responseForRequest).toList();
yield* Stream.fromIterable(bufferedResponses);
}
@ -138,7 +157,9 @@ Future<void> main(List<String> args) async {
final certificate = File(arguments['tls_cert_file']).readAsBytes();
final privateKey = File(arguments['tls_key_file']).readAsBytes();
tlsCredentials = ServerTlsCredentials(
certificate: await certificate, privateKey: await privateKey);
certificate: await certificate,
privateKey: await privateKey,
);
}
await server.serve(port: port, security: tlsCredentials);
print('Server listening on port ${server.port}...');

View File

@ -42,16 +42,17 @@ class Tester {
final String? serviceAccountKeyFile;
String? _serviceAccountJson;
Tester(
{required this.serverHost,
required this.serverHostOverride,
required this.serverPort,
required this.testCase,
required this.useTls,
required this.useTestCA,
required this.defaultServiceAccount,
required this.oauthScope,
required this.serviceAccountKeyFile});
Tester({
required this.serverHost,
required this.serverHostOverride,
required this.serverPort,
required this.testCase,
required this.useTls,
required this.useTestCA,
required this.defaultServiceAccount,
required this.oauthScope,
required this.serviceAccountKeyFile,
});
String get serviceAccountJson =>
_serviceAccountJson ??= _readServiceAccountJson();
@ -75,7 +76,9 @@ class Tester {
trustedRoot = File('ca.pem').readAsBytesSync();
}
credentials = ChannelCredentials.secure(
certificates: trustedRoot, authority: serverHostOverride);
certificates: trustedRoot,
authority: serverHostOverride,
);
} else {
credentials = const ChannelCredentials.insecure();
}
@ -438,14 +441,16 @@ class Tester {
final request = StreamingOutputCallRequest()
..responseParameters.addAll(
expectedResponses.map((size) => ResponseParameters()..size = size));
expectedResponses.map((size) => ResponseParameters()..size = size),
);
final responses = await client.streamingOutputCall(request).toList();
if (responses.length != 4) {
throw 'Incorrect number of responses (${responses.length}).';
}
final responseLengths =
responses.map((response) => response.payload.body.length).toList();
final responseLengths = responses
.map((response) => response.payload.body.length)
.toList();
if (!ListEquality().equals(responseLengths, expectedResponses)) {
throw 'Incorrect response lengths received (${responseLengths.join(', ')} != ${expectedResponses.join(', ')})';
@ -549,8 +554,9 @@ class Tester {
final payload = Payload()..body = Uint8List(requestSizes[index]);
final request = StreamingOutputCallRequest()
..payload = payload
..responseParameters
.add(ResponseParameters()..size = expectedResponses[index]);
..responseParameters.add(
ResponseParameters()..size = expectedResponses[index],
);
return request;
}
@ -629,11 +635,16 @@ class Tester {
/// zero and comparing the entire response message against a golden response
Future<void> computeEngineCreds() async {
final credentials = ComputeEngineAuthenticator();
final clientWithCredentials =
TestServiceClient(channel, options: credentials.toCallOptions);
final clientWithCredentials = TestServiceClient(
channel,
options: credentials.toCallOptions,
);
final response = await _sendSimpleRequestForAuth(clientWithCredentials,
fillUsername: true, fillOauthScope: true);
final response = await _sendSimpleRequestForAuth(
clientWithCredentials,
fillUsername: true,
fillOauthScope: true,
);
final user = response.username;
final oauth = response.oauthScope;
@ -719,11 +730,15 @@ class Tester {
/// zero and comparing the entire response message against a golden response
Future<void> jwtTokenCreds() async {
final credentials = JwtServiceAccountAuthenticator(serviceAccountJson);
final clientWithCredentials =
TestServiceClient(channel, options: credentials.toCallOptions);
final clientWithCredentials = TestServiceClient(
channel,
options: credentials.toCallOptions,
);
final response = await _sendSimpleRequestForAuth(clientWithCredentials,
fillUsername: true);
final response = await _sendSimpleRequestForAuth(
clientWithCredentials,
fillUsername: true,
);
final username = response.username;
if (username.isEmpty) {
throw 'Username not received.';
@ -773,13 +788,19 @@ class Tester {
/// check against the json key file or GCE default service account email.
/// * received SimpleResponse.oauth_scope is in `--oauth_scope`
Future<void> oauth2AuthToken() async {
final credentials =
ServiceAccountAuthenticator(serviceAccountJson, [oauthScope!]);
final clientWithCredentials =
TestServiceClient(channel, options: credentials.toCallOptions);
final credentials = ServiceAccountAuthenticator(serviceAccountJson, [
oauthScope!,
]);
final clientWithCredentials = TestServiceClient(
channel,
options: credentials.toCallOptions,
);
final response = await _sendSimpleRequestForAuth(clientWithCredentials,
fillUsername: true, fillOauthScope: true);
final response = await _sendSimpleRequestForAuth(
clientWithCredentials,
fillUsername: true,
fillOauthScope: true,
);
final user = response.username;
final oauth = response.oauthScope;
@ -829,13 +850,16 @@ class Tester {
/// file used by the auth library. The client can optionally check the
/// username matches the email address in the key file.
Future<void> perRpcCreds() async {
final credentials =
ServiceAccountAuthenticator(serviceAccountJson, [oauthScope!]);
final credentials = ServiceAccountAuthenticator(serviceAccountJson, [
oauthScope!,
]);
final response = await _sendSimpleRequestForAuth(client,
fillUsername: true,
fillOauthScope: true,
options: credentials.toCallOptions);
final response = await _sendSimpleRequestForAuth(
client,
fillUsername: true,
fillOauthScope: true,
options: credentials.toCallOptions,
);
final user = response.username;
final oauth = response.oauthScope;
@ -855,10 +879,12 @@ class Tester {
}
}
Future<SimpleResponse> _sendSimpleRequestForAuth(TestServiceClient client,
{bool fillUsername = false,
bool fillOauthScope = false,
CallOptions? options}) async {
Future<SimpleResponse> _sendSimpleRequestForAuth(
TestServiceClient client, {
bool fillUsername = false,
bool fillOauthScope = false,
CallOptions? options,
}) async {
final payload = Payload()..body = Uint8List(271828);
final request = SimpleRequest()
..responseSize = 314159
@ -922,15 +948,18 @@ class Tester {
}
}
final options = CallOptions(metadata: {
_headerEchoKey: _headerEchoData,
_trailerEchoKey: _trailerEchoData,
});
final options = CallOptions(
metadata: {
_headerEchoKey: _headerEchoData,
_trailerEchoKey: _trailerEchoData,
},
);
final unaryCall = client.unaryCall(
SimpleRequest()
..responseSize = 314159
..payload = (Payload()..body = Uint8List(271828)),
options: options);
SimpleRequest()
..responseSize = 314159
..payload = (Payload()..body = Uint8List(271828)),
options: options,
);
var headers = await unaryCall.headers;
var trailers = await unaryCall.trailers;
await unaryCall;
@ -1096,33 +1125,42 @@ class Tester {
final completer = Completer();
var receivedResponse = false;
call.listen((response) {
if (receivedResponse) {
completer.completeError('Received too many responses.');
return;
}
receivedResponse = true;
if (response.payload.body.length != 31415) {
completer.completeError('Invalid response length: '
'${response.payload.body.length} != 31415.');
}
call.cancel();
}, onError: (e) {
if (e is! GrpcError) {
completer.completeError('Unexpected error: $e.');
} else if (e.code != StatusCode.cancelled) {
completer
.completeError('Unexpected status code ${e.code}: ${e.message}.');
} else {
completer.complete(true);
}
}, onDone: () {
if (!completer.isCompleted) completer.completeError('Expected error.');
});
call.listen(
(response) {
if (receivedResponse) {
completer.completeError('Received too many responses.');
return;
}
receivedResponse = true;
if (response.payload.body.length != 31415) {
completer.completeError(
'Invalid response length: '
'${response.payload.body.length} != 31415.',
);
}
call.cancel();
},
onError: (e) {
if (e is! GrpcError) {
completer.completeError('Unexpected error: $e.');
} else if (e.code != StatusCode.cancelled) {
completer.completeError(
'Unexpected status code ${e.code}: ${e.message}.',
);
} else {
completer.complete(true);
}
},
onDone: () {
if (!completer.isCompleted) completer.completeError('Expected error.');
},
);
requests.add(StreamingOutputCallRequest()
..responseParameters.add(ResponseParameters()..size = 31415)
..payload = (Payload()..body = Uint8List(27182)));
requests.add(
StreamingOutputCallRequest()
..responseParameters.add(ResponseParameters()..size = 31415)
..payload = (Payload()..body = Uint8List(27182)),
);
await completer.future;
requests.close();
}
@ -1145,10 +1183,14 @@ class Tester {
/// * Call completed with status DEADLINE_EXCEEDED.
Future<void> timeoutOnSleepingServer() async {
final requests = StreamController<StreamingOutputCallRequest>();
final call = client.fullDuplexCall(requests.stream,
options: CallOptions(timeout: Duration(milliseconds: 1)));
requests.add(StreamingOutputCallRequest()
..payload = (Payload()..body = Uint8List(27182)));
final call = client.fullDuplexCall(
requests.stream,
options: CallOptions(timeout: Duration(milliseconds: 1)),
);
requests.add(
StreamingOutputCallRequest()
..payload = (Payload()..body = Uint8List(27182)),
);
try {
await for (final _ in call) {
throw 'Unexpected response received.';

View File

@ -48,23 +48,20 @@ class GrpcOrGrpcWebClientChannel extends GrpcOrGrpcWebClientChannelInternal {
required super.grpcTransportSecure,
required super.grpcWebPort,
required super.grpcWebTransportSecure,
}) : super(
grpcHost: host,
grpcWebHost: host,
);
}) : super(grpcHost: host, grpcWebHost: host);
GrpcOrGrpcWebClientChannel.toSingleEndpoint({
required String host,
required int port,
required bool transportSecure,
}) : super(
grpcHost: host,
grpcPort: port,
grpcTransportSecure: transportSecure,
grpcWebHost: host,
grpcWebPort: port,
grpcWebTransportSecure: transportSecure,
);
grpcHost: host,
grpcPort: port,
grpcTransportSecure: transportSecure,
grpcWebHost: host,
grpcWebPort: port,
grpcWebTransportSecure: transportSecure,
);
GrpcOrGrpcWebClientChannel.grpc(
super.host, {

View File

@ -74,7 +74,9 @@ abstract class HttpBasedAuthenticator extends BaseAuthenticator {
}
Future<auth.AccessCredentials> obtainCredentialsWithClient(
http.Client client, String uri);
http.Client client,
String uri,
);
}
class JwtServiceAccountAuthenticator extends BaseAuthenticator {
@ -83,15 +85,17 @@ class JwtServiceAccountAuthenticator extends BaseAuthenticator {
String? _keyId;
JwtServiceAccountAuthenticator.fromJson(
Map<String, dynamic> serviceAccountJson)
: _serviceAccountCredentials =
auth.ServiceAccountCredentials.fromJson(serviceAccountJson),
_projectId = serviceAccountJson['project_id'],
_keyId = serviceAccountJson['private_key_id'];
Map<String, dynamic> serviceAccountJson,
) : _serviceAccountCredentials = auth.ServiceAccountCredentials.fromJson(
serviceAccountJson,
),
_projectId = serviceAccountJson['project_id'],
_keyId = serviceAccountJson['private_key_id'];
factory JwtServiceAccountAuthenticator(String serviceAccountJsonString) =>
JwtServiceAccountAuthenticator.fromJson(
jsonDecode(serviceAccountJsonString));
jsonDecode(serviceAccountJsonString),
);
String? get projectId => _projectId;
@ -103,8 +107,12 @@ class JwtServiceAccountAuthenticator extends BaseAuthenticator {
// TODO(jakobr): Expose in googleapis_auth.
auth.AccessToken _jwtTokenFor(
auth.ServiceAccountCredentials credentials, String? keyId, String uri,
{String? user, List<String>? scopes}) {
auth.ServiceAccountCredentials credentials,
String? keyId,
String uri, {
String? user,
List<String>? scopes,
}) {
// Subtracting 20 seconds from current timestamp to allow for clock skew among
// servers.
final timestamp =
@ -121,7 +129,7 @@ auth.AccessToken _jwtTokenFor(
'aud': uri,
'exp': expiry,
'iat': timestamp,
'sub': user ?? credentials.email
'sub': user ?? credentials.email,
};
if (scopes != null) {
claims['scope'] = scopes.join(' ');
@ -135,14 +143,27 @@ auth.AccessToken _jwtTokenFor(
final key = credentials.privateRSAKey;
// We convert to our internal version of RSAPrivateKey. See rsa.dart for more
// explanation.
final signer = RS256Signer(RSAPrivateKey(
key.n, key.e, key.d, key.p, key.q, key.dmp1, key.dmq1, key.coeff));
final signer = RS256Signer(
RSAPrivateKey(
key.n,
key.e,
key.d,
key.p,
key.q,
key.dmp1,
key.dmq1,
key.coeff,
),
);
final signature = signer.sign(ascii.encode(data));
final jwt = '$data.${_base64url(signature)}';
return auth.AccessToken('Bearer', jwt,
DateTime.fromMillisecondsSinceEpoch(expiry * 1000, isUtc: true));
return auth.AccessToken(
'Bearer',
jwt,
DateTime.fromMillisecondsSinceEpoch(expiry * 1000, isUtc: true),
);
}
String _base64url(List<int> bytes) {

View File

@ -24,8 +24,9 @@ import 'auth.dart';
class ComputeEngineAuthenticator extends HttpBasedAuthenticator {
@override
Future<auth.AccessCredentials> obtainCredentialsWithClient(
http.Client client, String uri) =>
auth.obtainAccessCredentialsViaMetadataServer(client);
http.Client client,
String uri,
) => auth.obtainAccessCredentialsViaMetadataServer(client);
}
class ServiceAccountAuthenticator extends HttpBasedAuthenticator {
@ -34,23 +35,32 @@ class ServiceAccountAuthenticator extends HttpBasedAuthenticator {
String? _projectId;
ServiceAccountAuthenticator.fromJson(
Map<String, dynamic> serviceAccountJson, this._scopes)
: _serviceAccountCredentials =
auth.ServiceAccountCredentials.fromJson(serviceAccountJson),
_projectId = serviceAccountJson['project_id'];
Map<String, dynamic> serviceAccountJson,
this._scopes,
) : _serviceAccountCredentials = auth.ServiceAccountCredentials.fromJson(
serviceAccountJson,
),
_projectId = serviceAccountJson['project_id'];
factory ServiceAccountAuthenticator(
String serviceAccountJsonString, List<String> scopes) =>
ServiceAccountAuthenticator.fromJson(
jsonDecode(serviceAccountJsonString), scopes);
String serviceAccountJsonString,
List<String> scopes,
) => ServiceAccountAuthenticator.fromJson(
jsonDecode(serviceAccountJsonString),
scopes,
);
String? get projectId => _projectId;
@override
Future<auth.AccessCredentials> obtainCredentialsWithClient(
http.Client client, String uri) =>
auth.obtainAccessCredentialsViaServiceAccount(
_serviceAccountCredentials, _scopes, client);
http.Client client,
String uri,
) => auth.obtainAccessCredentialsViaServiceAccount(
_serviceAccountCredentials,
_scopes,
client,
);
}
class _CredentialsRefreshingAuthenticator extends HttpBasedAuthenticator {
@ -119,11 +129,17 @@ Future<HttpBasedAuthenticator> applicationDefaultCredentialsAuthenticator(
// Attempt to use file created by `gcloud auth application-default login`
File gcloudAdcFile;
if (Platform.isWindows) {
gcloudAdcFile = File.fromUri(Uri.directory(Platform.environment['APPDATA']!)
.resolve('gcloud/application_default_credentials.json'));
gcloudAdcFile = File.fromUri(
Uri.directory(
Platform.environment['APPDATA']!,
).resolve('gcloud/application_default_credentials.json'),
);
} else {
gcloudAdcFile = File.fromUri(Uri.directory(Platform.environment['HOME']!)
.resolve('.config/gcloud/application_default_credentials.json'));
gcloudAdcFile = File.fromUri(
Uri.directory(
Platform.environment['HOME']!,
).resolve('.config/gcloud/application_default_credentials.json'),
);
}
// Only try to load from gcloudAdcFile if it exists.
if (credFile == null && await gcloudAdcFile.exists()) {
@ -137,9 +153,7 @@ Future<HttpBasedAuthenticator> applicationDefaultCredentialsAuthenticator(
try {
credentials = json.decode(await credFile.readAsString());
} on IOException {
throw Exception(
'Failed to read credentials file from $fileSource',
);
throw Exception('Failed to read credentials file from $fileSource');
} on FormatException {
throw Exception(
'Failed to parse JSON from credentials file from $fileSource',

View File

@ -41,7 +41,7 @@ class RS256Signer {
0x03,
0x04,
0x02,
0x01
0x01,
];
final RSAPrivateKey _rsaKey;
@ -69,7 +69,8 @@ class RS256Signer {
// }
var offset = 0;
final digestInfo = Uint8List(
2 + 2 + _rsaSha256AlgorithmIdentifier.length + 2 + 2 + hash.length);
2 + 2 + _rsaSha256AlgorithmIdentifier.length + 2 + 2 + hash.length,
);
{
// DigestInfo
digestInfo[offset++] = ASN1Parser.sequenceTag;
@ -183,7 +184,8 @@ class ASN1Parser {
return ASN1Sequence(objects);
default:
invalidFormat(
'Unexpected tag $tag at offset ${offset - 1} (end: $end).');
'Unexpected tag $tag at offset ${offset - 1} (end: $end).',
);
}
}
@ -249,7 +251,15 @@ class RSAPrivateKey {
int get bitLength => n.bitLength;
RSAPrivateKey(
this.n, this.e, this.d, this.p, this.q, this.dmp1, this.dmq1, this.coeff);
this.n,
this.e,
this.d,
this.p,
this.q,
this.dmp1,
this.dmq1,
this.coeff,
);
}
/// Provides a [encrypt] method for encrypting messages with a [RSAPrivateKey].
@ -261,7 +271,10 @@ abstract class RSAAlgorithm {
/// The [intendedLength] argument specifies the number of bytes in which the
/// result should be encoded. Zero bytes will be used for padding.
static List<int> encrypt(
RSAPrivateKey key, List<int> bytes, int intendedLength) {
RSAPrivateKey key,
List<int> bytes,
int intendedLength,
) {
final message = bytes2BigInt(bytes);
final encryptedMessage = _encryptInteger(key, message);
return integer2Bytes(encryptedMessage, intendedLength);

View File

@ -44,8 +44,8 @@ const _reservedHeaders = [
/// by previous metadata providers) and the [uri] that is being called, and is
/// expected to modify the map before returning or before completing the
/// returned [Future].
typedef MetadataProvider = FutureOr<void> Function(
Map<String, String> metadata, String uri);
typedef MetadataProvider =
FutureOr<void> Function(Map<String, String> metadata, String uri);
/// Runtime options for an RPC.
class CallOptions {
@ -125,26 +125,33 @@ class WebCallOptions extends CallOptions {
final bool? withCredentials;
// TODO(mightyvoice): add a list of extra QueryParameter for gRPC.
WebCallOptions._(Map<String, String> metadata, Duration? timeout,
List<MetadataProvider> metadataProviders,
{this.bypassCorsPreflight, this.withCredentials})
: super._(metadata, timeout, metadataProviders, null);
WebCallOptions._(
Map<String, String> metadata,
Duration? timeout,
List<MetadataProvider> metadataProviders, {
this.bypassCorsPreflight,
this.withCredentials,
}) : super._(metadata, timeout, metadataProviders, null);
/// Creates a [WebCallOptions] object.
///
/// [WebCallOptions] can specify static [metadata], [timeout],
/// metadata [providers] of [CallOptions], [bypassCorsPreflight] and
/// [withCredentials] for CORS request.
factory WebCallOptions(
{Map<String, String>? metadata,
Duration? timeout,
List<MetadataProvider>? providers,
bool? bypassCorsPreflight,
bool? withCredentials}) {
return WebCallOptions._(Map.unmodifiable(metadata ?? {}), timeout,
List.unmodifiable(providers ?? []),
bypassCorsPreflight: bypassCorsPreflight ?? false,
withCredentials: withCredentials ?? false);
factory WebCallOptions({
Map<String, String>? metadata,
Duration? timeout,
List<MetadataProvider>? providers,
bool? bypassCorsPreflight,
bool? withCredentials,
}) {
return WebCallOptions._(
Map.unmodifiable(metadata ?? {}),
timeout,
List.unmodifiable(providers ?? []),
bypassCorsPreflight: bypassCorsPreflight ?? false,
withCredentials: withCredentials ?? false,
);
}
@override
@ -157,19 +164,25 @@ class WebCallOptions extends CallOptions {
..addAll(other.metadataProviders);
if (other is! WebCallOptions) {
return WebCallOptions._(Map.unmodifiable(mergedMetadata), mergedTimeout,
List.unmodifiable(mergedProviders),
bypassCorsPreflight: bypassCorsPreflight,
withCredentials: withCredentials);
return WebCallOptions._(
Map.unmodifiable(mergedMetadata),
mergedTimeout,
List.unmodifiable(mergedProviders),
bypassCorsPreflight: bypassCorsPreflight,
withCredentials: withCredentials,
);
}
final mergedBypassCorsPreflight =
other.bypassCorsPreflight ?? bypassCorsPreflight;
final mergedWithCredentials = other.withCredentials ?? withCredentials;
return WebCallOptions._(Map.unmodifiable(mergedMetadata), mergedTimeout,
List.unmodifiable(mergedProviders),
bypassCorsPreflight: mergedBypassCorsPreflight,
withCredentials: mergedWithCredentials);
return WebCallOptions._(
Map.unmodifiable(mergedMetadata),
mergedTimeout,
List.unmodifiable(mergedProviders),
bypassCorsPreflight: mergedBypassCorsPreflight,
withCredentials: mergedWithCredentials,
);
}
}
@ -196,12 +209,19 @@ class ClientCall<Q, R> implements Response {
final TimelineTask? _requestTimeline;
TimelineTask? _responseTimeline;
ClientCall(this._method, this._requests, this.options,
[this._requestTimeline]) {
_requestTimeline?.start('gRPC Request: ${_method.path}', arguments: {
'method': _method.path,
'timeout': options.timeout?.toString(),
});
ClientCall(
this._method,
this._requests,
this.options, [
this._requestTimeline,
]) {
_requestTimeline?.start(
'gRPC Request: ${_method.path}',
arguments: {
'method': _method.path,
'timeout': options.timeout?.toString(),
},
);
_responses.onListen = _onResponseListen;
if (options.timeout != null) {
_timeoutTimer = Timer(options.timeout!, _onTimedOut);
@ -213,8 +233,9 @@ class ClientCall<Q, R> implements Response {
}
void _terminateWithError(Object e) {
final error =
e is GrpcError ? e : GrpcError.unavailable('Error making call: $e');
final error = e is GrpcError
? e
: GrpcError.unavailable('Error making call: $e');
_finishTimelineWithError(error, _requestTimeline);
_responses.addErrorIfNotClosed(error);
_safeTerminate();
@ -232,7 +253,7 @@ class ClientCall<Q, R> implements Response {
return sanitizedMetadata;
}
// TODO(sigurdm): Find out why we do this.
// TODO(sigurdm): Find out why we do this.
static String audiencePath(ClientMethod method) {
final lastSlashPos = method.path.lastIndexOf('/');
return lastSlashPos == -1
@ -248,9 +269,12 @@ class ClientCall<Q, R> implements Response {
} else {
final metadata = Map<String, String>.of(options.metadata);
Future.forEach(
options.metadataProviders,
(MetadataProvider provider) => provider(metadata,
'${connection.scheme}://${connection.authority}${audiencePath(_method)}'))
options.metadataProviders,
(MetadataProvider provider) => provider(
metadata,
'${connection.scheme}://${connection.authority}${audiencePath(_method)}',
),
)
.then((_) => _sendRequest(connection, _sanitizeMetadata(metadata)))
.catchError(_terminateWithError);
}
@ -270,22 +294,26 @@ class ClientCall<Q, R> implements Response {
_terminateWithError(e);
return;
}
_requestTimeline?.instant('Request sent', arguments: {
'metadata': metadata,
});
_requestTimeline?.instant(
'Request sent',
arguments: {'metadata': metadata},
);
_requestSubscription = _requests
.map((data) {
_requestTimeline?.instant('Data sent', arguments: {
'data': data.toString(),
});
_requestTimeline?.instant(
'Data sent',
arguments: {'data': data.toString()},
);
_requestTimeline?.finish();
return _method.requestSerializer(data);
})
.handleError(_onRequestError)
.listen(stream.outgoingMessages.add,
onError: stream.outgoingMessages.addError,
onDone: stream.outgoingMessages.close,
cancelOnError: true);
.listen(
stream.outgoingMessages.add,
onError: stream.outgoingMessages.addError,
onDone: stream.outgoingMessages.close,
cancelOnError: true,
);
_stream = stream;
// The response stream might have been listened to before _stream was ready,
// so try setting up the subscription here as well.
@ -293,9 +321,7 @@ class ClientCall<Q, R> implements Response {
}
void _finishTimelineWithError(GrpcError error, TimelineTask? timeline) {
timeline?.finish(arguments: {
'error': error.toString(),
});
timeline?.finish(arguments: {'error': error.toString()});
}
void _onTimedOut() {
@ -312,10 +338,12 @@ class ClientCall<Q, R> implements Response {
_responses.hasListener &&
_responseSubscription == null) {
// ignore: cancel_subscriptions
final subscription = _stream!.incomingMessages.listen(_onResponseData,
onError: _onResponseError,
onDone: _onResponseDone,
cancelOnError: true);
final subscription = _stream!.incomingMessages.listen(
_onResponseData,
onError: _onResponseError,
onDone: _onResponseDone,
cancelOnError: true,
);
if (_responses.isPaused) {
subscription.pause();
}
@ -360,9 +388,10 @@ class ClientCall<Q, R> implements Response {
}
try {
final decodedData = _method.responseDeserializer(data.data);
_responseTimeline?.instant('Data received', arguments: {
'data': decodedData.toString(),
});
_responseTimeline?.instant(
'Data received',
arguments: {'data': decodedData.toString()},
);
_responses.add(decodedData);
_hasReceivedResponses = true;
} catch (e, s) {
@ -378,9 +407,10 @@ class ClientCall<Q, R> implements Response {
);
}
_responseTimeline?.start('gRPC Response');
_responseTimeline?.instant('Metadata received', arguments: {
'headers': _headerMetadata.toString(),
});
_responseTimeline?.instant(
'Metadata received',
arguments: {'headers': _headerMetadata.toString()},
);
_headers.complete(_headerMetadata);
return;
}
@ -389,9 +419,10 @@ class ClientCall<Q, R> implements Response {
return;
}
final metadata = data.metadata;
_responseTimeline?.instant('Metadata received', arguments: {
'trailers': metadata.toString(),
});
_responseTimeline?.instant(
'Metadata received',
arguments: {'trailers': metadata.toString()},
);
_trailers.complete(metadata);
/// Process status error if necessary

View File

@ -39,7 +39,10 @@ abstract class ClientChannel {
/// Initiates a new RPC on this connection.
ClientCall<Q, R> createCall<Q, R>(
ClientMethod<Q, R> method, Stream<Q> requests, CallOptions options);
ClientMethod<Q, R> method,
Stream<Q> requests,
CallOptions options,
);
/// Stream of connection state changes
///
@ -59,7 +62,7 @@ abstract class ClientChannelBase implements ClientChannel {
final void Function()? _channelShutdownHandler;
ClientChannelBase({void Function()? channelShutdownHandler})
: _channelShutdownHandler = channelShutdownHandler;
: _channelShutdownHandler = channelShutdownHandler;
@override
Future<void> shutdown() async {
@ -104,14 +107,18 @@ abstract class ClientChannelBase implements ClientChannel {
@override
ClientCall<Q, R> createCall<Q, R>(
ClientMethod<Q, R> method, Stream<Q> requests, CallOptions options) {
ClientMethod<Q, R> method,
Stream<Q> requests,
CallOptions options,
) {
final call = ClientCall(
method,
requests,
options,
isTimelineLoggingEnabled
? TimelineTask(filterKey: clientTimelineFilterKey)
: null);
method,
requests,
options,
isTimelineLoggingEnabled
? TimelineTask(filterKey: clientTimelineFilterKey)
: null,
);
getConnection().then((connection) {
if (call.isCancelled) return;
connection.dispatchCall(call);

View File

@ -26,10 +26,12 @@ class Client {
final List<ClientInterceptor> _interceptors;
/// Interceptors will be applied in direct order before making a request.
Client(this._channel,
{CallOptions? options, Iterable<ClientInterceptor>? interceptors})
: _options = options ?? CallOptions(),
_interceptors = List.unmodifiable(interceptors ?? Iterable.empty());
Client(
this._channel, {
CallOptions? options,
Iterable<ClientInterceptor>? interceptors,
}) : _options = options ?? CallOptions(),
_interceptors = List.unmodifiable(interceptors ?? Iterable.empty());
@Deprecated(r'''This method does not invoke interceptors and is superseded
by $createStreamingCall and $createUnaryCall which invoke interceptors.
@ -38,15 +40,21 @@ If you are getting this warning in autogenerated protobuf client stubs,
regenerate these stubs using protobuf compiler plugin version 19.2.0 or newer.
''')
ClientCall<Q, R> $createCall<Q, R>(
ClientMethod<Q, R> method, Stream<Q> requests,
{CallOptions? options}) {
ClientMethod<Q, R> method,
Stream<Q> requests, {
CallOptions? options,
}) {
return _channel.createCall(method, requests, _options.mergedWith(options));
}
ResponseFuture<R> $createUnaryCall<Q, R>(ClientMethod<Q, R> method, Q request,
{CallOptions? options}) {
ResponseFuture<R> $createUnaryCall<Q, R>(
ClientMethod<Q, R> method,
Q request, {
CallOptions? options,
}) {
var invoker = (method, request, options) => ResponseFuture<R>(
_channel.createCall<Q, R>(method, Stream.value(request), options));
_channel.createCall<Q, R>(method, Stream.value(request), options),
);
for (final interceptor in _interceptors.reversed) {
final delegate = invoker;
@ -58,8 +66,10 @@ regenerate these stubs using protobuf compiler plugin version 19.2.0 or newer.
}
ResponseStream<R> $createStreamingCall<Q, R>(
ClientMethod<Q, R> method, Stream<Q> requests,
{CallOptions? options}) {
ClientMethod<Q, R> method,
Stream<Q> requests, {
CallOptions? options,
}) {
var invoker = (method, requests, options) =>
ResponseStream<R>(_channel.createCall<Q, R>(method, requests, options));

View File

@ -61,8 +61,8 @@ final class Idle extends KeepAliveState {
final Stopwatch timeSinceFrame;
Idle([this.pingTimer, Stopwatch? stopwatch])
: timeSinceFrame = stopwatch ?? clock.stopwatch()
..start();
: timeSinceFrame = stopwatch ?? clock.stopwatch()
..start();
@override
KeepAliveState? onEvent(KeepAliveEvent event, ClientKeepAlive manager) {
@ -71,9 +71,12 @@ final class Idle extends KeepAliveState {
// When the transport goes active, we do not reset the nextKeepaliveTime.
// This allows us to quickly check whether the connection is still
// working.
final timer = pingTimer ??
Timer(manager._pingInterval - timeSinceFrame.elapsed,
manager.sendPing);
final timer =
pingTimer ??
Timer(
manager._pingInterval - timeSinceFrame.elapsed,
manager.sendPing,
);
return PingScheduled(timer, timeSinceFrame);
default:
return null;
@ -91,8 +94,8 @@ final class PingScheduled extends KeepAliveState {
final Stopwatch timeSinceFrame;
PingScheduled(this.pingTimer, [Stopwatch? stopwatch])
: timeSinceFrame = stopwatch ?? clock.stopwatch()
..start();
: timeSinceFrame = stopwatch ?? clock.stopwatch()
..start();
@override
KeepAliveState? onEvent(KeepAliveEvent event, ClientKeepAlive manager) {

View File

@ -60,9 +60,11 @@ class ResponseFuture<R> extends DelegatingFuture<R>
}
ResponseFuture(this._call)
: super(_call.response
: super(
_call.response
.fold<R?>(null, _ensureOnlyOneResponse)
.then(_ensureOneResponse));
.then(_ensureOneResponse),
);
}
/// A gRPC response producing a stream of values.

View File

@ -31,7 +31,7 @@ enum ConnectionState {
idle,
/// Shutting down, no further RPCs allowed.
shutdown
shutdown,
}
abstract class ClientConnection {
@ -42,9 +42,13 @@ abstract class ClientConnection {
void dispatchCall(ClientCall call);
/// Start a request for [path] with [metadata].
GrpcTransportStream makeRequest(String path, Duration? timeout,
Map<String, String> metadata, ErrorHandler onRequestFailure,
{required CallOptions callOptions});
GrpcTransportStream makeRequest(
String path,
Duration? timeout,
Map<String, String> metadata,
ErrorHandler onRequestFailure, {
required CallOptions callOptions,
});
/// Shuts down this connection.
///

View File

@ -26,14 +26,14 @@ class GrpcOrGrpcWebClientChannelInternal extends ClientChannel {
required int grpcWebPort,
required bool grpcWebTransportSecure,
}) : super(
grpcHost,
port: grpcPort,
options: ChannelOptions(
credentials: grpcTransportSecure
? ChannelCredentials.secure()
: ChannelCredentials.insecure(),
),
);
grpcHost,
port: grpcPort,
options: ChannelOptions(
credentials: grpcTransportSecure
? ChannelCredentials.secure()
: ChannelCredentials.insecure(),
),
);
GrpcOrGrpcWebClientChannelInternal.grpc(
super.host, {

View File

@ -24,22 +24,25 @@ class GrpcOrGrpcWebClientChannelInternal extends GrpcWebClientChannel {
required String grpcWebHost,
required int grpcWebPort,
required bool grpcWebTransportSecure,
}) : super.xhr(Uri(
host: grpcWebHost,
port: grpcWebPort,
scheme: grpcWebTransportSecure ? 'https' : 'http',
));
}) : super.xhr(
Uri(
host: grpcWebHost,
port: grpcWebPort,
scheme: grpcWebTransportSecure ? 'https' : 'http',
),
);
GrpcOrGrpcWebClientChannelInternal.grpc(
Object host, {
required int port,
required ChannelOptions options,
}) : super.xhr(
Uri(
host: host.toString(),
port: port,
scheme: options.credentials.isSecure ? 'https' : 'http'),
) {
Uri(
host: host.toString(),
port: port,
scheme: options.credentials.isSecure ? 'https' : 'http',
),
) {
// Do not silently ignore options as caller may expect them to have effects.
throw UnsupportedError('not supported by gRPC-web');
}

View File

@ -48,11 +48,15 @@ class ClientTransportConnectorChannel extends ClientChannelBase {
final ClientTransportConnector transportConnector;
final ChannelOptions options;
ClientTransportConnectorChannel(this.transportConnector,
{this.options = const ChannelOptions()});
ClientTransportConnectorChannel(
this.transportConnector, {
this.options = const ChannelOptions(),
});
@override
ClientConnection createConnection() =>
Http2ClientConnection.fromClientTransportConnector(
transportConnector, options);
transportConnector,
options,
);
}

View File

@ -37,8 +37,10 @@ class Http2ClientConnection implements connection.ClientConnection {
static final _methodPost = Header.ascii(':method', 'POST');
static final _schemeHttp = Header.ascii(':scheme', 'http');
static final _schemeHttps = Header.ascii(':scheme', 'https');
static final _contentTypeGrpc =
Header.ascii('content-type', 'application/grpc');
static final _contentTypeGrpc = Header.ascii(
'content-type',
'application/grpc',
);
static final _teTrailers = Header.ascii('te', 'trailers');
final ChannelOptions options;
@ -63,10 +65,12 @@ class Http2ClientConnection implements connection.ClientConnection {
ClientKeepAlive? keepAliveManager;
Http2ClientConnection(Object host, int port, this.options)
: _transportConnector = SocketTransportConnector(host, port, options);
: _transportConnector = SocketTransportConnector(host, port, options);
Http2ClientConnection.fromClientTransportConnector(
this._transportConnector, this.options);
this._transportConnector,
this.options,
);
ChannelCredentials get credentials => options.credentials;
@ -102,35 +106,38 @@ class Http2ClientConnection implements connection.ClientConnection {
return;
}
_setState(ConnectionState.connecting);
connectTransport().then<void>((transport) async {
_currentReconnectDelay = null;
_transportConnection = transport;
if (options.keepAlive.shouldSendPings) {
keepAliveManager = ClientKeepAlive(
options: options.keepAlive,
ping: () {
if (transport.isOpen) {
transport.ping();
}
},
onPingTimeout: () => transport.finish(),
);
transport.onFrameReceived
.listen((_) => keepAliveManager?.onFrameReceived());
}
_connectionLifeTimer
..reset()
..start();
transport.onActiveStateChanged = _handleActiveStateChanged;
_setState(ConnectionState.ready);
connectTransport()
.then<void>((transport) async {
_currentReconnectDelay = null;
_transportConnection = transport;
if (options.keepAlive.shouldSendPings) {
keepAliveManager = ClientKeepAlive(
options: options.keepAlive,
ping: () {
if (transport.isOpen) {
transport.ping();
}
},
onPingTimeout: () => transport.finish(),
);
transport.onFrameReceived.listen(
(_) => keepAliveManager?.onFrameReceived(),
);
}
_connectionLifeTimer
..reset()
..start();
transport.onActiveStateChanged = _handleActiveStateChanged;
_setState(ConnectionState.ready);
if (_hasPendingCalls()) {
// Take all pending calls out, and reschedule.
final pendingCalls = _pendingCalls.toList();
_pendingCalls.clear();
pendingCalls.forEach(dispatchCall);
}
}).catchError(_handleConnectionFailure);
if (_hasPendingCalls()) {
// Take all pending calls out, and reschedule.
final pendingCalls = _pendingCalls.toList();
_pendingCalls.clear();
pendingCalls.forEach(dispatchCall);
}
})
.catchError(_handleConnectionFailure);
}
/// Abandons the current connection if it is unhealthy or has been open for
@ -171,9 +178,13 @@ class Http2ClientConnection implements connection.ClientConnection {
}
@override
GrpcTransportStream makeRequest(String path, Duration? timeout,
Map<String, String> metadata, ErrorHandler onRequestFailure,
{CallOptions? callOptions}) {
GrpcTransportStream makeRequest(
String path,
Duration? timeout,
Map<String, String> metadata,
ErrorHandler onRequestFailure, {
CallOptions? callOptions,
}) {
final compressionCodec = callOptions?.compression;
final headers = createCallHeaders(
credentials.isSecure,
@ -185,7 +196,7 @@ class Http2ClientConnection implements connection.ClientConnection {
userAgent: options.userAgent,
grpcAcceptEncodings:
(callOptions?.metadata ?? const {})['grpc-accept-encoding'] ??
options.codecRegistry?.supportedEncodings,
options.codecRegistry?.supportedEncodings,
);
final stream = _transportConnection!.makeRequest(headers);
return Http2TransportStream(
@ -240,9 +251,9 @@ class Http2ClientConnection implements connection.ClientConnection {
void _handleIdleTimeout() {
if (_timer == null || _state != ConnectionState.ready) return;
_cancelTimer();
_transportConnection
?.finish()
.catchError((_) {}); // TODO(jakobr): Log error.
_transportConnection?.finish().catchError(
(_) {},
); // TODO(jakobr): Log error.
keepAliveManager?.onTransportTermination();
_disconnect();
_setState(ConnectionState.idle);
@ -344,7 +355,7 @@ class Http2ClientConnection implements connection.ClientConnection {
if (grpcAcceptEncodings != null)
Header.ascii('grpc-accept-encoding', grpcAcceptEncodings),
if (compressionCodec != null)
Header.ascii('grpc-encoding', compressionCodec.encodingName)
Header.ascii('grpc-encoding', compressionCodec.encodingName),
];
metadata?.forEach((key, value) {
headers.add(Header(ascii.encode(key), utf8.encode(value)));
@ -365,7 +376,7 @@ class SocketTransportConnector implements ClientTransportConnector {
int get port => proxy == null ? _port : proxy!.port;
SocketTransportConnector(this._host, this._port, this._options)
: assert(_host is InternetAddress || _host is String);
: assert(_host is InternetAddress || _host is String);
@override
Future<ClientTransportConnection> connect() async {
@ -500,7 +511,8 @@ class SocketTransportConnector implements ClientTransportConnector {
completer.complete();
} else {
throw TransportException(
'Error establishing proxy connection: $response');
'Error establishing proxy connection: $response',
);
}
}
}

View File

@ -17,11 +17,19 @@ import 'call.dart';
import 'common.dart';
import 'method.dart';
typedef ClientUnaryInvoker<Q, R> = ResponseFuture<R> Function(
ClientMethod<Q, R> method, Q request, CallOptions options);
typedef ClientUnaryInvoker<Q, R> =
ResponseFuture<R> Function(
ClientMethod<Q, R> method,
Q request,
CallOptions options,
);
typedef ClientStreamingInvoker<Q, R> = ResponseStream<R> Function(
ClientMethod<Q, R> method, Stream<Q> requests, CallOptions options);
typedef ClientStreamingInvoker<Q, R> =
ResponseStream<R> Function(
ClientMethod<Q, R> method,
Stream<Q> requests,
CallOptions options,
);
/// ClientInterceptors intercepts client calls before they are executed.
///
@ -30,18 +38,23 @@ typedef ClientStreamingInvoker<Q, R> = ResponseStream<R> Function(
abstract class ClientInterceptor {
// Intercept unary call.
// This method is called when client sends single request and receives single response.
ResponseFuture<R> interceptUnary<Q, R>(ClientMethod<Q, R> method, Q request,
CallOptions options, ClientUnaryInvoker<Q, R> invoker) {
ResponseFuture<R> interceptUnary<Q, R>(
ClientMethod<Q, R> method,
Q request,
CallOptions options,
ClientUnaryInvoker<Q, R> invoker,
) {
return invoker(method, request, options);
}
// Intercept streaming call.
// This method is called when client sends either request or response stream.
ResponseStream<R> interceptStreaming<Q, R>(
ClientMethod<Q, R> method,
Stream<Q> requests,
CallOptions options,
ClientStreamingInvoker<Q, R> invoker) {
ClientMethod<Q, R> method,
Stream<Q> requests,
CallOptions options,
ClientStreamingInvoker<Q, R> invoker,
) {
return invoker(method, requests, options);
}
}

View File

@ -42,7 +42,7 @@ class QueryParameter implements Comparable<QueryParameter> {
/// This is not the default constructor since the single-value case is the
/// most common.
QueryParameter.multi(this.key, List<String> values)
: values = values..sort() {
: values = values..sort() {
ArgumentError.checkNotNull(key);
if (key.trim().isEmpty) {
throw ArgumentError(key);

View File

@ -40,8 +40,9 @@ Uri moveHttpHeadersToQueryParam(Map<String, String> metadata, Uri requestUri) {
final paramValue = _generateHttpHeadersOverwriteParam(metadata);
metadata.clear();
return requestUri.replace(
queryParameters: Map.of(requestUri.queryParameters)
..[_httpHeadersParamName] = paramValue);
queryParameters: Map.of(requestUri.queryParameters)
..[_httpHeadersParamName] = paramValue,
);
}
/// Generates the URL parameter value with custom headers encoded as

View File

@ -21,8 +21,8 @@ import '../../shared/security.dart';
/// returns `true`, the bad certificate is allowed, and the TLS handshake can
/// continue. If the handler returns `false`, the TLS handshake fails, and the
/// connection is aborted.
typedef BadCertificateHandler = bool Function(
X509Certificate certificate, String host);
typedef BadCertificateHandler =
bool Function(X509Certificate certificate, String host);
/// Bad certificate handler that disables all certificate checks.
/// DO NOT USE IN PRODUCTION!
@ -38,28 +38,34 @@ class ChannelCredentials {
final String? _certificatePassword;
final BadCertificateHandler? onBadCertificate;
const ChannelCredentials._(this.isSecure, this._certificateBytes,
this._certificatePassword, this.authority, this.onBadCertificate);
const ChannelCredentials._(
this.isSecure,
this._certificateBytes,
this._certificatePassword,
this.authority,
this.onBadCertificate,
);
/// Disable TLS. RPCs are sent in clear text.
const ChannelCredentials.insecure({String? authority})
: this._(false, null, null, authority, null);
: this._(false, null, null, authority, null);
/// Enable TLS and optionally specify the [certificates] to trust. If
/// [certificates] is not provided, the default trust store is used.
const ChannelCredentials.secure(
{List<int>? certificates,
String? password,
String? authority,
BadCertificateHandler? onBadCertificate})
: this._(true, certificates, password, authority, onBadCertificate);
const ChannelCredentials.secure({
List<int>? certificates,
String? password,
String? authority,
BadCertificateHandler? onBadCertificate,
}) : this._(true, certificates, password, authority, onBadCertificate);
SecurityContext? get securityContext {
if (!isSecure) return null;
if (_certificateBytes != null) {
return createSecurityContext(false)
..setTrustedCertificatesBytes(_certificateBytes,
password: _certificatePassword);
return createSecurityContext(false)..setTrustedCertificatesBytes(
_certificateBytes,
password: _certificatePassword,
);
}
final context = SecurityContext(withTrustedRoots: true);
context.setAlpnProtocols(supportedAlpnProtocols, false);

View File

@ -39,16 +39,18 @@ class Http2TransportStream extends GrpcTransportStream {
CodecRegistry? codecRegistry,
Codec? compression,
) : incomingMessages = _transportStream.incomingMessages
.transform(GrpcHttpDecoder(forResponse: true))
.transform(grpcDecompressor(codecRegistry: codecRegistry)) {
.transform(GrpcHttpDecoder(forResponse: true))
.transform(grpcDecompressor(codecRegistry: codecRegistry)) {
_outgoingMessages.stream
.map((payload) => frame(payload, compression))
.map<StreamMessage>((bytes) => DataStreamMessage(bytes))
.handleError(_onError)
.listen(_transportStream.outgoingMessages.add,
onError: _transportStream.outgoingMessages.addError,
onDone: _transportStream.outgoingMessages.close,
cancelOnError: true);
.listen(
_transportStream.outgoingMessages.add,
onError: _transportStream.outgoingMessages.addError,
onDone: _transportStream.outgoingMessages.close,
cancelOnError: true,
);
}
@override

View File

@ -71,7 +71,11 @@ class _GrpcWebConversionSink implements ChunkedConversionSink<ByteBuffer> {
final chunkRemaining = chunkLength - _chunkOffset;
final toCopy = min(headerRemaining, chunkRemaining);
_dataHeader.setRange(
_dataOffset, _dataOffset + toCopy, chunkData, _chunkOffset);
_dataOffset,
_dataOffset + toCopy,
chunkData,
_chunkOffset,
);
_dataOffset += toCopy;
_chunkOffset += toCopy;
if (_dataOffset == _dataHeader.lengthInBytes) {
@ -91,8 +95,12 @@ class _GrpcWebConversionSink implements ChunkedConversionSink<ByteBuffer> {
if (dataRemaining > 0) {
final chunkRemaining = chunkData.length - _chunkOffset;
final toCopy = min(dataRemaining, chunkRemaining);
_data!
.setRange(_dataOffset, _dataOffset + toCopy, chunkData, _chunkOffset);
_data!.setRange(
_dataOffset,
_dataOffset + toCopy,
chunkData,
_chunkOffset,
);
_dataOffset += toCopy;
_chunkOffset += toCopy;
}

View File

@ -46,14 +46,19 @@ class XhrTransportStream implements GrpcTransportStream {
@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) => _request.send(Uint8List.fromList(data).toJS),
cancelOnError: true,
onError: _onError);
XhrTransportStream(
this._request, {
required ErrorHandler onError,
required onDone,
}) : _onError = onError,
_onDone = onDone {
_outgoingMessages.stream
.map(frame)
.listen(
(data) => _request.send(Uint8List.fromList(data).toJS),
cancelOnError: true,
onError: _onError,
);
_request.onReadyStateChange.listen((_) {
if (_incomingProcessor.isClosed) {
@ -74,8 +79,10 @@ class XhrTransportStream implements GrpcTransportStream {
if (_incomingProcessor.isClosed) {
return;
}
_onError(GrpcError.unavailable('XhrConnection connection-error'),
StackTrace.current);
_onError(
GrpcError.unavailable('XhrConnection connection-error'),
StackTrace.current,
);
terminate();
});
@ -85,8 +92,8 @@ class XhrTransportStream implements GrpcTransportStream {
}
final responseText = _request.responseText;
final bytes = Uint8List.fromList(
responseText.substring(_requestBytesRead).codeUnits)
.buffer;
responseText.substring(_requestBytesRead).codeUnits,
).buffer;
_requestBytesRead = responseText.length;
_incomingProcessor.add(bytes);
});
@ -94,15 +101,20 @@ class XhrTransportStream implements GrpcTransportStream {
_incomingProcessor.stream
.transform(GrpcWebDecoder())
.transform(grpcDecompressor())
.listen(_incomingMessages.add,
onError: _onError, onDone: _incomingMessages.close);
.listen(
_incomingMessages.add,
onError: _onError,
onDone: _incomingMessages.close,
);
}
bool _validateResponseState() {
try {
validateHttpStatusAndContentType(
_request.status, _request.responseHeaders,
rawResponse: _request.responseText);
_request.status,
_request.responseHeaders,
rawResponse: _request.responseText,
);
return true;
} catch (e, st) {
_onError(e, st);
@ -124,11 +136,13 @@ class XhrTransportStream implements GrpcTransportStream {
}
if (_request.status != 200) {
_onError(
GrpcError.unavailable(
'Request failed with status: ${_request.status}',
null,
_request.responseText),
StackTrace.current);
GrpcError.unavailable(
'Request failed with status: ${_request.status}',
null,
_request.responseText,
),
StackTrace.current,
);
return;
}
}
@ -264,7 +278,9 @@ class XhrClientConnection implements ClientConnection {
String get scheme => uri.scheme;
void _initializeRequest(
IXMLHttpRequest request, Map<String, String> metadata) {
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');
@ -275,9 +291,13 @@ class XhrClientConnection implements ClientConnection {
IXMLHttpRequest createHttpRequest() => XMLHttpRequestImpl();
@override
GrpcTransportStream makeRequest(String path, Duration? timeout,
Map<String, String> metadata, ErrorHandler onError,
{CallOptions? callOptions}) {
GrpcTransportStream makeRequest(
String path,
Duration? timeout,
Map<String, String> metadata,
ErrorHandler onError, {
CallOptions? callOptions,
}) {
// gRPC-web headers.
if (_getContentTypeHeader(metadata) == null) {
metadata['Content-Type'] = 'application/grpc-web+proto';
@ -299,14 +319,20 @@ class XhrClientConnection implements ClientConnection {
// Must set headers after calling open().
_initializeRequest(request, metadata);
final transportStream =
_createXhrTransportStream(request, onError, _removeStream);
final transportStream = _createXhrTransportStream(
request,
onError,
_removeStream,
);
_requests.add(transportStream);
return transportStream;
}
XhrTransportStream _createXhrTransportStream(IXMLHttpRequest request,
ErrorHandler onError, void Function(XhrTransportStream stream) onDone) {
XhrTransportStream _createXhrTransportStream(
IXMLHttpRequest request,
ErrorHandler onError,
void Function(XhrTransportStream stream) onDone,
) {
return XhrTransportStream(request, onError: onError, onDone: onDone);
}

View File

@ -90,14 +90,14 @@ class ServerHandler extends ServiceCall {
InternetAddress? remoteAddress,
GrpcErrorHandler? errorHandler,
this.onDataReceived,
}) : _stream = stream,
_serviceLookup = serviceLookup,
_interceptors = interceptors,
_codecRegistry = codecRegistry,
_clientCertificate = clientCertificate,
_remoteAddress = remoteAddress,
_errorHandler = errorHandler,
_serverInterceptors = serverInterceptors;
}) : _stream = stream,
_serviceLookup = serviceLookup,
_interceptors = interceptors,
_codecRegistry = codecRegistry,
_clientCertificate = clientCertificate,
_remoteAddress = remoteAddress,
_errorHandler = errorHandler,
_serverInterceptors = serverInterceptors;
@override
DateTime? get deadline => _deadline;
@ -244,10 +244,12 @@ class ServerHandler extends ServiceCall {
_responses = _descriptor.handle(this, requests.stream, _serverInterceptors);
_responseSubscription = _responses.listen(_onResponse,
onError: _onResponseError,
onDone: _onResponseDone,
cancelOnError: true);
_responseSubscription = _responses.listen(
_onResponse,
onError: _onResponseError,
onDone: _onResponseDone,
cancelOnError: true,
);
_incomingSubscription!.onData(_onDataActive);
_incomingSubscription!.onDone(_onDoneExpected);
@ -298,8 +300,9 @@ class ServerHandler extends ServiceCall {
try {
request = _descriptor.deserialize(data.data);
} catch (error, trace) {
final grpcError =
GrpcError.internal('Error deserializing request: $error');
final grpcError = GrpcError.internal(
'Error deserializing request: $error',
);
_sendError(grpcError, trace);
_requests!
..addError(grpcError, trace)
@ -364,8 +367,10 @@ class ServerHandler extends ServiceCall {
_customHeaders = null;
final outgoingHeaders = <Header>[];
outgoingHeadersMap.forEach((key, value) =>
outgoingHeaders.add(Header(ascii.encode(key), utf8.encode(value))));
outgoingHeadersMap.forEach(
(key, value) =>
outgoingHeaders.add(Header(ascii.encode(key), utf8.encode(value))),
);
_stream.sendHeaders(outgoingHeaders);
_headersSent = true;
}
@ -398,16 +403,19 @@ class ServerHandler extends ServiceCall {
_customTrailers = null;
outgoingTrailersMap['grpc-status'] = status.toString();
if (message != null) {
outgoingTrailersMap['grpc-message'] =
Uri.encodeFull(message).replaceAll('%20', ' ');
outgoingTrailersMap['grpc-message'] = Uri.encodeFull(
message,
).replaceAll('%20', ' ');
}
if (errorTrailers != null) {
outgoingTrailersMap.addAll(errorTrailers);
}
final outgoingTrailers = <Header>[];
outgoingTrailersMap.forEach((key, value) =>
outgoingTrailers.add(Header(ascii.encode(key), utf8.encode(value))));
outgoingTrailersMap.forEach(
(key, value) =>
outgoingTrailers.add(Header(ascii.encode(key), utf8.encode(value))),
);
_stream.sendHeaders(outgoingTrailers, endStream: true);
// We're done!
_cancelResponseSubscription();

View File

@ -25,11 +25,15 @@ import 'service.dart';
/// If the interceptor returns a [GrpcError], the error will be returned as a response and [ServiceMethod] wouldn't be called.
/// If the interceptor throws [Exception], [GrpcError.internal] with exception.toString() will be returned.
/// If the interceptor returns null, the corresponding [ServiceMethod] of [Service] will be called.
typedef Interceptor = FutureOr<GrpcError?> Function(
ServiceCall call, ServiceMethod method);
typedef Interceptor =
FutureOr<GrpcError?> Function(ServiceCall call, ServiceMethod method);
typedef ServerStreamingInvoker<Q, R> = Stream<R> Function(
ServiceCall call, ServiceMethod<Q, R> method, Stream<Q> requests);
typedef ServerStreamingInvoker<Q, R> =
Stream<R> Function(
ServiceCall call,
ServiceMethod<Q, R> method,
Stream<Q> requests,
);
/// A gRPC Interceptor.
///
@ -37,8 +41,12 @@ typedef ServerStreamingInvoker<Q, R> = Stream<R> Function(
/// If the interceptor throws [GrpcError], the error will be returned as a response. [ServiceMethod] wouldn't be called if the error is thrown before calling the invoker.
/// If the interceptor modifies the provided stream, the invocation will continue with the provided stream.
abstract class ServerInterceptor {
Stream<R> intercept<Q, R>(ServiceCall call, ServiceMethod<Q, R> method,
Stream<Q> requests, ServerStreamingInvoker<Q, R> invoker) {
Stream<R> intercept<Q, R>(
ServiceCall call,
ServiceMethod<Q, R> method,
Stream<Q> requests,
ServerStreamingInvoker<Q, R> invoker,
) {
return invoker(call, method, requests);
}
}

View File

@ -57,11 +57,12 @@ class ServerTlsCredentials extends ServerCredentials {
///
/// If the [certificate] or [privateKey] is encrypted, the password must also
/// be provided.
ServerTlsCredentials(
{this.certificate,
this.certificatePassword,
this.privateKey,
this.privateKeyPassword});
ServerTlsCredentials({
this.certificate,
this.certificatePassword,
this.privateKey,
this.privateKeyPassword,
});
@override
SecurityContext get securityContext {
@ -70,8 +71,10 @@ class ServerTlsCredentials extends ServerCredentials {
context.usePrivateKeyBytes(privateKey!, password: privateKeyPassword);
}
if (certificate != null) {
context.useCertificateChainBytes(certificate!,
password: certificatePassword);
context.useCertificateChainBytes(
certificate!,
password: certificatePassword,
);
}
return context;
}
@ -105,10 +108,10 @@ class ConnectionServer {
CodecRegistry? codecRegistry,
GrpcErrorHandler? errorHandler,
this._keepAliveOptions = const ServerKeepAliveOptions(),
]) : _codecRegistry = codecRegistry,
_interceptors = interceptors,
_serverInterceptors = serverInterceptors,
_errorHandler = errorHandler {
]) : _codecRegistry = codecRegistry,
_interceptors = interceptors,
_serverInterceptors = serverInterceptors,
_errorHandler = errorHandler {
for (final service in services) {
_services[service.$name] = service;
}
@ -133,31 +136,35 @@ class ConnectionServer {
pingNotifier: connection.onPingReceived,
dataNotifier: onDataReceivedController.stream,
).handle();
connection.incomingStreams.listen((stream) {
final handler = serveStream_(
stream: stream,
clientCertificate: clientCertificate,
remoteAddress: remoteAddress,
onDataReceived: onDataReceivedController.sink,
);
handler.onCanceled.then((_) => handlers[connection]?.remove(handler));
handlers[connection]!.add(handler);
}, onError: (error, stackTrace) {
if (error is Error) {
Zone.current.handleUncaughtError(error, stackTrace);
}
}, onDone: () async {
// TODO(sigurdm): This is not correct behavior in the presence of
// half-closed tcp streams.
// Half-closed streams seems to not be fully supported by package:http2.
// https://github.com/dart-lang/http2/issues/42
for (var handler in handlers[connection]!) {
handler.cancel();
}
_connections.remove(connection);
handlers.remove(connection);
await onDataReceivedController.close();
});
connection.incomingStreams.listen(
(stream) {
final handler = serveStream_(
stream: stream,
clientCertificate: clientCertificate,
remoteAddress: remoteAddress,
onDataReceived: onDataReceivedController.sink,
);
handler.onCanceled.then((_) => handlers[connection]?.remove(handler));
handlers[connection]!.add(handler);
},
onError: (error, stackTrace) {
if (error is Error) {
Zone.current.handleUncaughtError(error, stackTrace);
}
},
onDone: () async {
// TODO(sigurdm): This is not correct behavior in the presence of
// half-closed tcp streams.
// Half-closed streams seems to not be fully supported by package:http2.
// https://github.com/dart-lang/http2/issues/42
for (var handler in handlers[connection]!) {
handler.cancel();
}
_connections.remove(connection);
handlers.remove(connection);
await onDataReceivedController.close();
},
);
}
@visibleForTesting
@ -168,18 +175,18 @@ class ConnectionServer {
Sink<void>? onDataReceived,
}) {
return ServerHandler(
stream: stream,
serviceLookup: lookupService,
interceptors: _interceptors,
serverInterceptors: _serverInterceptors,
codecRegistry: _codecRegistry,
// ignore: unnecessary_cast
clientCertificate: clientCertificate as io_bits.X509Certificate?,
// ignore: unnecessary_cast
remoteAddress: remoteAddress as io_bits.InternetAddress?,
errorHandler: _errorHandler,
onDataReceived: onDataReceived)
..handle();
stream: stream,
serviceLookup: lookupService,
interceptors: _interceptors,
serverInterceptors: _serverInterceptors,
codecRegistry: _codecRegistry,
// ignore: unnecessary_cast
clientCertificate: clientCertificate as io_bits.X509Certificate?,
// ignore: unnecessary_cast
remoteAddress: remoteAddress as io_bits.InternetAddress?,
errorHandler: _errorHandler,
onDataReceived: onDataReceived,
)..handle();
}
}
@ -209,13 +216,13 @@ class Server extends ConnectionServer {
CodecRegistry? codecRegistry,
GrpcErrorHandler? errorHandler,
}) : super(
services,
interceptors,
serverInterceptors,
codecRegistry,
errorHandler,
keepAliveOptions,
);
services,
interceptors,
serverInterceptors,
codecRegistry,
errorHandler,
keepAliveOptions,
);
/// The port that the server is listening on, or `null` if the server is not
/// active.
@ -273,33 +280,36 @@ class Server extends ConnectionServer {
_insecureServer = _server;
server = _server;
}
server.listen((socket) {
// Don't wait for io buffers to fill up before sending requests.
if (socket.address.type != InternetAddressType.unix) {
socket.setOption(SocketOption.tcpNoDelay, true);
}
server.listen(
(socket) {
// Don't wait for io buffers to fill up before sending requests.
if (socket.address.type != InternetAddressType.unix) {
socket.setOption(SocketOption.tcpNoDelay, true);
}
X509Certificate? clientCertificate;
X509Certificate? clientCertificate;
if (socket is SecureSocket) {
clientCertificate = socket.peerCertificate;
}
if (socket is SecureSocket) {
clientCertificate = socket.peerCertificate;
}
final connection = ServerTransportConnection.viaSocket(
socket,
settings: http2ServerSettings,
);
final connection = ServerTransportConnection.viaSocket(
socket,
settings: http2ServerSettings,
);
serveConnection(
connection: connection,
clientCertificate: clientCertificate,
remoteAddress: socket.remoteAddressOrNull,
);
}, onError: (error, stackTrace) {
if (error is Error) {
Zone.current.handleUncaughtError(error, stackTrace);
}
});
serveConnection(
connection: connection,
clientCertificate: clientCertificate,
remoteAddress: socket.remoteAddressOrNull,
);
},
onError: (error, stackTrace) {
if (error is Error) {
Zone.current.handleUncaughtError(error, stackTrace);
}
},
);
}
@override
@ -326,7 +336,8 @@ class Server extends ConnectionServer {
}
@Deprecated(
'This is internal functionality, and will be removed in next major version.')
'This is internal functionality, and will be removed in next major version.',
)
void serveStream(ServerTransportStream stream) {
serveStream_(stream: stream);
}

View File

@ -32,28 +32,27 @@ class ServiceMethod<Q, R> {
final Function handler;
ServiceMethod(
this.name,
this.handler,
this.streamingRequest,
this.streamingResponse,
this.requestDeserializer,
this.responseSerializer);
this.name,
this.handler,
this.streamingRequest,
this.streamingResponse,
this.requestDeserializer,
this.responseSerializer,
);
StreamController<Q> createRequestStream(StreamSubscription incoming) =>
StreamController<Q>(
onListen: incoming.resume,
onPause: incoming.pause,
onResume: incoming.resume);
onListen: incoming.resume,
onPause: incoming.pause,
onResume: incoming.resume,
);
Q deserialize(List<int> data) => requestDeserializer(data);
List<int> serialize(dynamic response) => responseSerializer(response as R);
ServerStreamingInvoker<Q, R> _createCall() => ((
ServiceCall call,
ServiceMethod<Q, R> method,
Stream<Q> requests,
) {
ServerStreamingInvoker<Q, R> _createCall() =>
((ServiceCall call, ServiceMethod<Q, R> method, Stream<Q> requests) {
if (streamingResponse) {
if (streamingRequest) {
return handler(call, requests);
@ -100,8 +99,9 @@ class ServiceMethod<Q, R> {
return value;
}
final future =
stream.fold<Q?>(null, ensureOnlyOneRequest).then(ensureOneRequest);
final future = stream
.fold<Q?>(null, ensureOnlyOneRequest)
.then(ensureOneRequest);
// Make sure errors on the future aren't unhandled, but return the original
// future so the request handler can also get the error.
_awaitAndCatch(future);

View File

@ -18,17 +18,25 @@ import 'codec.dart';
/// Encloses classes related to the compression and decompression of messages.
class CodecRegistry {
CodecRegistry({List<Codec> codecs = const [IdentityCodec()]})
: _codecs = {for (var codec in codecs) codec.encodingName: codec},
_supportedEncodings = codecs.map((c) {
if (c.encodingName.contains(',')) {
throw ArgumentError.value(c.encodingName, 'codecs',
'contains entries with names containing ","');
}
return c.encodingName;
}).join(',') {
: _codecs = {for (var codec in codecs) codec.encodingName: codec},
_supportedEncodings = codecs
.map((c) {
if (c.encodingName.contains(',')) {
throw ArgumentError.value(
c.encodingName,
'codecs',
'contains entries with names containing ","',
);
}
return c.encodingName;
})
.join(',') {
if (_codecs.length != codecs.length) {
throw ArgumentError.value(
codecs, 'codecs', 'contains multiple entries with the same name');
codecs,
'codecs',
'contains multiple entries with the same name',
);
}
}

View File

@ -63,13 +63,16 @@ class GrpcMessageSink implements Sink<GrpcMessage> {
}
List<int> frame(List<int> rawPayload, [Codec? codec]) {
final compressedPayload =
codec == null ? rawPayload : codec.compress(rawPayload);
final compressedPayload = codec == null
? rawPayload
: codec.compress(rawPayload);
final payloadLength = compressedPayload.length;
final bytes = Uint8List(payloadLength + 5);
final header = bytes.buffer.asByteData(0, 5);
header.setUint8(
0, (codec == null || codec.encodingName == 'identity') ? 0 : 1);
0,
(codec == null || codec.encodingName == 'identity') ? 0 : 1,
);
header.setUint32(1, payloadLength);
bytes.setRange(5, bytes.length, compressedPayload);
return bytes;
@ -80,21 +83,23 @@ StreamTransformer<GrpcMessage, GrpcMessage> grpcDecompressor({
}) {
Codec? codec;
return StreamTransformer<GrpcMessage, GrpcMessage>.fromHandlers(
handleData: (GrpcMessage value, EventSink<GrpcMessage> sink) {
if (value is GrpcData && value.isCompressed) {
if (codec == null) {
sink.addError(
GrpcError.unimplemented('Compression mechanism not supported'),
);
handleData: (GrpcMessage value, EventSink<GrpcMessage> sink) {
if (value is GrpcData && value.isCompressed) {
if (codec == null) {
sink.addError(
GrpcError.unimplemented('Compression mechanism not supported'),
);
return;
}
sink.add(GrpcData(codec!.decompress(value.data), isCompressed: false));
return;
}
sink.add(GrpcData(codec!.decompress(value.data), isCompressed: false));
return;
}
if (value is GrpcMetadata && value.metadata.containsKey('grpc-encoding')) {
codec = codecRegistry?.lookup(value.metadata['grpc-encoding']!);
}
sink.add(value);
});
if (value is GrpcMetadata &&
value.metadata.containsKey('grpc-encoding')) {
codec = codecRegistry?.lookup(value.metadata['grpc-encoding']!);
}
sink.add(value);
},
);
}

View File

@ -153,25 +153,25 @@ class StatusCode {
/// Creates a string from a gRPC status code.
static String? name(int status) => switch (status) {
ok => 'OK',
cancelled => 'CANCELLED',
unknown => 'UNKNOWN',
invalidArgument => 'INVALID_ARGUMENT',
deadlineExceeded => 'DEADLINE_EXCEEDED',
notFound => 'NOT_FOUND',
alreadyExists => 'ALREADY_EXISTS',
permissionDenied => 'PERMISSION_DENIED',
resourceExhausted => 'RESOURCE_EXHAUSTED',
failedPrecondition => 'FAILED_PRECONDITION',
aborted => 'ABORTED',
outOfRange => 'OUT_OF_RANGE',
unimplemented => 'UNIMPLEMENTED',
internal => 'INTERNAL',
unavailable => 'UNAVAILABLE',
dataLoss => 'DATA_LOSS',
unauthenticated => 'UNAUTHENTICATED',
int() => null,
};
ok => 'OK',
cancelled => 'CANCELLED',
unknown => 'UNKNOWN',
invalidArgument => 'INVALID_ARGUMENT',
deadlineExceeded => 'DEADLINE_EXCEEDED',
notFound => 'NOT_FOUND',
alreadyExists => 'ALREADY_EXISTS',
permissionDenied => 'PERMISSION_DENIED',
resourceExhausted => 'RESOURCE_EXHAUSTED',
failedPrecondition => 'FAILED_PRECONDITION',
aborted => 'ABORTED',
outOfRange => 'OUT_OF_RANGE',
unimplemented => 'UNIMPLEMENTED',
internal => 'INTERNAL',
unavailable => 'UNAVAILABLE',
dataLoss => 'DATA_LOSS',
unauthenticated => 'UNAUTHENTICATED',
int() => null,
};
}
class GrpcError implements Exception {
@ -182,73 +182,86 @@ class GrpcError implements Exception {
final List<GeneratedMessage>? details;
/// Custom error code.
const GrpcError.custom(this.code,
[this.message, this.details, this.rawResponse, this.trailers = const {}]);
const GrpcError.custom(
this.code, [
this.message,
this.details,
this.rawResponse,
this.trailers = const {},
]);
/// The operation completed successfully.
const GrpcError.ok([this.message, this.details, this.rawResponse])
: trailers = const {},
code = StatusCode.ok;
: trailers = const {},
code = StatusCode.ok;
/// The operation was cancelled (typically by the caller).
const GrpcError.cancelled([this.message, this.details, this.rawResponse])
: trailers = const {},
code = StatusCode.cancelled;
: trailers = const {},
code = StatusCode.cancelled;
/// Unknown error. An example of where this error may be returned is if a
/// Status value received from another address space belongs to an error-space
/// that is not known in this address space. Also errors raised by APIs that
/// do not return enough error information may be converted to this error.
const GrpcError.unknown([this.message, this.details, this.rawResponse])
: trailers = const {},
code = StatusCode.unknown;
: trailers = const {},
code = StatusCode.unknown;
/// Client specified an invalid argument. Note that this differs from
/// [failedPrecondition]. [invalidArgument] indicates arguments that are
/// problematic regardless of the state of the system (e.g., a malformed file
/// name).
const GrpcError.invalidArgument(
[this.message, this.details, this.rawResponse])
: trailers = const {},
code = StatusCode.invalidArgument;
const GrpcError.invalidArgument([
this.message,
this.details,
this.rawResponse,
]) : trailers = const {},
code = StatusCode.invalidArgument;
/// Deadline expired before operation could complete. For operations that
/// change the state of the system, this error may be returned even if the
/// operation has completed successfully. For example, a successful response
/// from a server could have been delayed long enough for the deadline to
/// expire.
const GrpcError.deadlineExceeded(
[this.message, this.details, this.rawResponse])
: trailers = const {},
code = StatusCode.deadlineExceeded;
const GrpcError.deadlineExceeded([
this.message,
this.details,
this.rawResponse,
]) : trailers = const {},
code = StatusCode.deadlineExceeded;
/// Some requested entity (e.g., file or directory) was not found.
const GrpcError.notFound([this.message, this.details, this.rawResponse])
: trailers = const {},
code = StatusCode.notFound;
: trailers = const {},
code = StatusCode.notFound;
/// Some entity that we attempted to create (e.g., file or directory) already
/// exists.
const GrpcError.alreadyExists([this.message, this.details, this.rawResponse])
: trailers = const {},
code = StatusCode.alreadyExists;
: trailers = const {},
code = StatusCode.alreadyExists;
/// The caller does not have permission to execute the specified operation.
/// [permissionDenied] must not be used for rejections caused by exhausting
/// some resource (use [resourceExhausted] instead for those errors).
/// [permissionDenied] must not be used if the caller cannot be identified
/// (use [unauthenticated] instead for those errors).
const GrpcError.permissionDenied(
[this.message, this.details, this.rawResponse])
: trailers = const {},
code = StatusCode.permissionDenied;
const GrpcError.permissionDenied([
this.message,
this.details,
this.rawResponse,
]) : trailers = const {},
code = StatusCode.permissionDenied;
/// Some resource has been exhausted, perhaps a per-user quota, or perhaps the
/// entire file system is out of space.
const GrpcError.resourceExhausted(
[this.message, this.details, this.rawResponse])
: trailers = const {},
code = StatusCode.resourceExhausted;
const GrpcError.resourceExhausted([
this.message,
this.details,
this.rawResponse,
]) : trailers = const {},
code = StatusCode.resourceExhausted;
/// Operation was rejected because the system is not in a state required for
/// the operation's execution. For example, directory to be deleted may be
@ -264,10 +277,12 @@ class GrpcError implements Exception {
/// because the directory is non-empty, [failedPrecondition] should be
/// returned since the client should not retry unless they have first
/// fixed up the directory by deleting files from it.
const GrpcError.failedPrecondition(
[this.message, this.details, this.rawResponse])
: trailers = const {},
code = StatusCode.failedPrecondition;
const GrpcError.failedPrecondition([
this.message,
this.details,
this.rawResponse,
]) : trailers = const {},
code = StatusCode.failedPrecondition;
/// The operation was aborted, typically due to a concurrency issue like
/// sequencer check failures, transaction aborts, etc.
@ -275,8 +290,8 @@ class GrpcError implements Exception {
/// See litmus test above for deciding between [failedPrecondition],
/// [aborted], and [unavailable].
const GrpcError.aborted([this.message, this.details, this.rawResponse])
: trailers = const {},
code = StatusCode.aborted;
: trailers = const {},
code = StatusCode.aborted;
/// Operation was attempted past the valid range. E.g., seeking or reading
/// past end of file.
@ -292,20 +307,23 @@ class GrpcError implements Exception {
/// when it applies so that callers who are iterating through a space can
/// easily look for an [outOfRange] error to detect when they are done.
const GrpcError.outOfRange([this.message, this.details, this.rawResponse])
: trailers = const {},
code = StatusCode.outOfRange;
: trailers = const {},
code = StatusCode.outOfRange;
/// Operation is not implemented or not supported/enabled in this service.
const GrpcError.unimplemented([this.message, this.details, this.rawResponse])
: trailers = const {},
code = StatusCode.unimplemented;
: trailers = const {},
code = StatusCode.unimplemented;
/// Internal errors. Means some invariants expected by underlying system has
/// been broken. If you see one of these errors, something is very broken.
// TODO(sigurdm): This should probably not be an [Exception].
const GrpcError.internal(
[this.message, this.details, this.rawResponse, this.trailers])
: code = StatusCode.internal;
const GrpcError.internal([
this.message,
this.details,
this.rawResponse,
this.trailers,
]) : code = StatusCode.internal;
/// The service is currently unavailable. This is a most likely a transient
/// condition and may be corrected by retrying with a backoff.
@ -313,20 +331,22 @@ class GrpcError implements Exception {
/// See litmus test above for deciding between [failedPrecondition],
/// [aborted], and [unavailable].
const GrpcError.unavailable([this.message, this.details, this.rawResponse])
: trailers = const {},
code = StatusCode.unavailable;
: trailers = const {},
code = StatusCode.unavailable;
/// Unrecoverable data loss or corruption.
const GrpcError.dataLoss([this.message, this.details, this.rawResponse])
: trailers = const {},
code = StatusCode.dataLoss;
: trailers = const {},
code = StatusCode.dataLoss;
/// The request does not have valid authentication credentials for the
/// operation.
const GrpcError.unauthenticated(
[this.message, this.details, this.rawResponse])
: trailers = const {},
code = StatusCode.unauthenticated;
const GrpcError.unauthenticated([
this.message,
this.details,
this.rawResponse,
]) : trailers = const {},
code = StatusCode.unauthenticated;
/// Given a status code, return the name
String get codeName =>
@ -405,18 +425,24 @@ GeneratedMessage parseErrorDetailsFromAny(Any any) {
/// occurs.
///
void validateHttpStatusAndContentType(
int? httpStatus, Map<String, String> headers,
{Object? rawResponse}) {
int? httpStatus,
Map<String, String> headers, {
Object? rawResponse,
}) {
if (httpStatus == null) {
throw GrpcError.unknown(
'HTTP response status is unknown', null, rawResponse);
'HTTP response status is unknown',
null,
rawResponse,
);
}
if (httpStatus == 0) {
throw GrpcError.unknown(
'HTTP request completed without a status (potential CORS issue)',
null,
rawResponse);
'HTTP request completed without a status (potential CORS issue)',
null,
rawResponse,
);
}
final status = StatusCode.fromHttpStatus(httpStatus);
@ -448,7 +474,10 @@ void validateHttpStatusAndContentType(
// Check if content-type header indicates a supported format.
if (!_validContentTypePrefix.any(contentType.startsWith)) {
throw GrpcError.unknown(
'unsupported content-type ($contentType)', null, rawResponse);
'unsupported content-type ($contentType)',
null,
rawResponse,
);
}
}
@ -489,7 +518,7 @@ const _statusDetailsHeader = 'grpc-status-details-bin';
const _validContentTypePrefix = [
'application/grpc',
'application/json+protobuf',
'application/x-protobuf'
'application/x-protobuf',
];
/// Given a string of base64url data, attempt to parse a Status object from it.
@ -505,7 +534,8 @@ const _validContentTypePrefix = [
List<GeneratedMessage> decodeStatusDetails(String data) {
try {
final parsedStatus = Status.fromBuffer(
base64Url.decode(data.padRight((data.length + 3) & ~3, '=')));
base64Url.decode(data.padRight((data.length + 3) & ~3, '=')),
);
return parsedStatus.details.map(parseErrorDetailsFromAny).toList();
} catch (e) {
return <GeneratedMessage>[];

View File

@ -84,7 +84,11 @@ class _GrpcMessageConversionSink
final chunkRemaining = chunkLength - chunkReadOffset;
final toCopy = min(headerRemaining, chunkRemaining);
_dataHeader.setRange(
_dataOffset, _dataOffset + toCopy, chunkData, chunkReadOffset);
_dataOffset,
_dataOffset + toCopy,
chunkData,
chunkReadOffset,
);
_dataOffset += toCopy;
chunkReadOffset += toCopy;
if (_dataOffset == _dataHeader.lengthInBytes) {
@ -101,13 +105,21 @@ class _GrpcMessageConversionSink
final chunkRemaining = chunkLength - chunkReadOffset;
final toCopy = min(dataRemaining, chunkRemaining);
_data!.setRange(
_dataOffset, _dataOffset + toCopy, chunkData, chunkReadOffset);
_dataOffset,
_dataOffset + toCopy,
chunkData,
chunkReadOffset,
);
_dataOffset += toCopy;
chunkReadOffset += toCopy;
}
if (_dataOffset == _data!.lengthInBytes) {
_out.add(GrpcData(_data!,
isCompressed: _dataHeader.buffer.asByteData().getUint8(0) != 0));
_out.add(
GrpcData(
_data!,
isCompressed: _dataHeader.buffer.asByteData().getUint8(0) != 0,
),
);
_data = null;
_dataOffset = 0;
}

View File

@ -35,7 +35,9 @@ class EchoService extends EchoServiceBase {
@override
Stream<ServerStreamingEchoResponse> serverStreamingEcho(
ServiceCall call, ServerStreamingEchoRequest request) {
ServiceCall call,
ServerStreamingEchoRequest request,
) {
// TODO: implement serverStreamingEcho
throw UnimplementedError();
}
@ -53,13 +55,17 @@ Future<void> main() async {
SecurityContextChannelCredentials.baseSecurityContext();
channelContext.useCertificateChain('test/data/localhost.crt');
channelContext.usePrivateKey('test/data/localhost.key');
final channelCredentials = SecurityContextChannelCredentials(channelContext,
onBadCertificate: (cert, s) {
return true;
});
final channel = ClientChannel(address,
port: server.port ?? 443,
options: ChannelOptions(credentials: channelCredentials));
final channelCredentials = SecurityContextChannelCredentials(
channelContext,
onBadCertificate: (cert, s) {
return true;
},
);
final channel = ClientChannel(
address,
port: server.port ?? 443,
options: ChannelOptions(credentials: channelCredentials),
);
final client = EchoServiceClient(channel);
// Test
@ -79,13 +85,17 @@ Future<void> main() async {
SecurityContextChannelCredentials.baseSecurityContext();
channelContext.useCertificateChain('test/data/localhost.crt');
channelContext.usePrivateKey('test/data/localhost.key');
final channelCredentials = SecurityContextChannelCredentials(channelContext,
onBadCertificate: (cert, s) {
return true;
});
final channel = ClientChannel(address,
port: server.port ?? 443,
options: ChannelOptions(credentials: channelCredentials));
final channelCredentials = SecurityContextChannelCredentials(
channelContext,
onBadCertificate: (cert, s) {
return true;
},
);
final channel = ClientChannel(
address,
port: server.port ?? 443,
options: ChannelOptions(credentials: channelCredentials),
);
final client = EchoServiceClient(channel);
// Test
@ -103,23 +113,27 @@ Future<Server> _setUpServer([bool requireClientCertificate = false]) async {
serverContext.useCertificateChain('test/data/localhost.crt');
serverContext.usePrivateKey('test/data/localhost.key');
serverContext.setTrustedCertificates('test/data/localhost.crt');
final ServerCredentials serverCredentials =
SecurityContextServerCredentials(serverContext);
final ServerCredentials serverCredentials = SecurityContextServerCredentials(
serverContext,
);
await server.serve(
address: address,
port: 0,
security: serverCredentials,
requireClientCertificate: requireClientCertificate);
address: address,
port: 0,
security: serverCredentials,
requireClientCertificate: requireClientCertificate,
);
return server;
}
class SecurityContextChannelCredentials extends ChannelCredentials {
final SecurityContext _securityContext;
SecurityContextChannelCredentials(SecurityContext securityContext,
{super.authority, super.onBadCertificate})
: _securityContext = securityContext,
super.secure();
SecurityContextChannelCredentials(
SecurityContext securityContext, {
super.authority,
super.onBadCertificate,
}) : _securityContext = securityContext,
super.secure();
@override
SecurityContext get securityContext => _securityContext;
@ -133,8 +147,8 @@ class SecurityContextServerCredentials extends ServerTlsCredentials {
final SecurityContext _securityContext;
SecurityContextServerCredentials(SecurityContext securityContext)
: _securityContext = securityContext,
super();
: _securityContext = securityContext,
super();
@override
SecurityContext get securityContext => _securityContext;

View File

@ -29,15 +29,19 @@ import 'common.dart';
class TestClient extends grpc.Client {
static final _$stream = grpc.ClientMethod<int, int>(
'/test.TestService/stream',
(int value) => [value],
(List<int> value) => value[0]);
'/test.TestService/stream',
(int value) => [value],
(List<int> value) => value[0],
);
TestClient(super.channel);
grpc.ResponseStream<int> stream(int request, {grpc.CallOptions? options}) {
return $createStreamingCall(_$stream, Stream.value(request),
options: options);
return $createStreamingCall(
_$stream,
Stream.value(request),
options: options,
);
}
}
@ -46,8 +50,16 @@ class TestService extends grpc.Service {
String get $name => 'test.TestService';
TestService() {
$addMethod(grpc.ServiceMethod<int, int>('stream', stream, false, true,
(List<int> value) => value[0], (int value) => [value]));
$addMethod(
grpc.ServiceMethod<int, int>(
'stream',
stream,
false,
true,
(List<int> value) => value[0],
(int value) => [value],
),
);
}
Stream<int> stream(grpc.ServiceCall call, Future request) async* {
@ -72,29 +84,34 @@ class FixedConnectionClientChannel extends ClientChannelBase {
}
Future<void> main() async {
testTcpAndUds('client reconnects after the connection gets old',
(address) async {
testTcpAndUds('client reconnects after the connection gets old', (
address,
) async {
// client reconnect after a short delay.
final server = grpc.Server.create(services: [TestService()]);
await server.serve(address: address, port: 0);
final channel = FixedConnectionClientChannel(Http2ClientConnection(
address,
server.port!,
grpc.ChannelOptions(
idleTimeout: Duration(minutes: 1),
// Short delay to test that it will time out.
connectionTimeout: Duration(milliseconds: 100),
credentials: grpc.ChannelCredentials.insecure(),
final channel = FixedConnectionClientChannel(
Http2ClientConnection(
address,
server.port!,
grpc.ChannelOptions(
idleTimeout: Duration(minutes: 1),
// Short delay to test that it will time out.
connectionTimeout: Duration(milliseconds: 100),
credentials: grpc.ChannelCredentials.insecure(),
),
),
));
);
final testClient = TestClient(channel);
expect(await testClient.stream(1).toList(), [1, 2, 3]);
await Future.delayed(Duration(milliseconds: 200));
expect(await testClient.stream(1).toList(), [1, 2, 3]);
expect(
channel.states.where((x) => x == grpc.ConnectionState.ready).length, 2);
channel.states.where((x) => x == grpc.ConnectionState.ready).length,
2,
);
server.shutdown();
});
@ -102,14 +119,18 @@ Future<void> main() async {
// client reconnect after setting stream limit.
final server = grpc.Server.create(services: [TestService()]);
await server.serve(
address: address,
port: 0,
http2ServerSettings: ServerSettings(concurrentStreamLimit: 2));
address: address,
port: 0,
http2ServerSettings: ServerSettings(concurrentStreamLimit: 2),
);
final channel = FixedConnectionClientChannel(Http2ClientConnection(
final channel = FixedConnectionClientChannel(
Http2ClientConnection(
address,
server.port!,
grpc.ChannelOptions(credentials: grpc.ChannelCredentials.insecure())));
grpc.ChannelOptions(credentials: grpc.ChannelCredentials.insecure()),
),
);
final states = <grpc.ConnectionState>[];
channel.onConnectionStateChanged.listen((state) {
states.add(state);

View File

@ -34,8 +34,10 @@ void main() {
});
test('WebCallOptions mergeWith CallOptions returns WebCallOptions', () {
final options1 =
WebCallOptions(bypassCorsPreflight: true, withCredentials: true);
final options1 = WebCallOptions(
bypassCorsPreflight: true,
withCredentials: true,
);
final metadata = {'test': '42'};
final options2 = CallOptions(metadata: metadata);
final mergedOptions1 = options1.mergedWith(options2) as WebCallOptions;
@ -50,56 +52,45 @@ void main() {
expect(mergedOptions2.withCredentials, true);
});
test(
'Cancelling a call correctly complete headers future',
() async {
final clientCall = harness.client.unary(dummyValue);
test('Cancelling a call correctly complete headers future', () async {
final clientCall = harness.client.unary(dummyValue);
Future.delayed(
Duration(milliseconds: cancelDurationMillis),
).then((_) => clientCall.cancel());
Future.delayed(
Duration(milliseconds: cancelDurationMillis),
).then((_) => clientCall.cancel());
expect(await clientCall.headers, isEmpty);
expect(await clientCall.headers, isEmpty);
await expectLater(
clientCall,
throwsA(
isA<GrpcError>().having(
(e) => e.codeName,
'Test codename',
contains('CANCELLED'),
),
await expectLater(
clientCall,
throwsA(
isA<GrpcError>().having(
(e) => e.codeName,
'Test codename',
contains('CANCELLED'),
),
);
},
);
),
);
});
test(
'Cancelling a call correctly complete trailers futures',
() async {
final clientCall = harness.client.unary(dummyValue);
test('Cancelling a call correctly complete trailers futures', () async {
final clientCall = harness.client.unary(dummyValue);
Future.delayed(
Duration(milliseconds: cancelDurationMillis),
).then((_) {
clientCall.cancel();
});
Future.delayed(Duration(milliseconds: cancelDurationMillis)).then((_) {
clientCall.cancel();
});
expect(
await clientCall.trailers,
isEmpty,
);
expect(await clientCall.trailers, isEmpty);
await expectLater(
clientCall,
throwsA(
isA<GrpcError>().having(
(e) => e.codeName,
'Test codename',
contains('CANCELLED'),
),
await expectLater(
clientCall,
throwsA(
isA<GrpcError>().having(
(e) => e.codeName,
'Test codename',
contains('CANCELLED'),
),
);
},
);
),
);
});
}

View File

@ -45,8 +45,12 @@ class FakeInterceptor implements ClientInterceptor {
FakeInterceptor(this._id);
@override
ResponseFuture<R> interceptUnary<Q, R>(ClientMethod<Q, R> method, Q request,
CallOptions options, ClientUnaryInvoker<Q, R> invoker) {
ResponseFuture<R> interceptUnary<Q, R>(
ClientMethod<Q, R> method,
Q request,
CallOptions options,
ClientUnaryInvoker<Q, R> invoker,
) {
_invocations.add(InterceptorInvocation(_id, ++_unary, _streaming));
return invoker(method, request, _inject(options));
@ -54,10 +58,11 @@ class FakeInterceptor implements ClientInterceptor {
@override
ResponseStream<R> interceptStreaming<Q, R>(
ClientMethod<Q, R> method,
Stream<Q> requests,
CallOptions options,
ClientStreamingInvoker<Q, R> invoker) {
ClientMethod<Q, R> method,
Stream<Q> requests,
CallOptions options,
ClientStreamingInvoker<Q, R> invoker,
) {
_invocations.add(InterceptorInvocation(_id, _unary, ++_streaming));
final requestStream = _id > 10
@ -68,9 +73,13 @@ class FakeInterceptor implements ClientInterceptor {
}
CallOptions _inject(CallOptions options) {
return options.mergedWith(CallOptions(metadata: {
'x-interceptor': _invocations.map((i) => i.toString()).join(', '),
}));
return options.mergedWith(
CallOptions(
metadata: {
'x-interceptor': _invocations.map((i) => i.toString()).join(', '),
},
),
);
}
static void tearDown() {
@ -102,7 +111,7 @@ void main() {
expectedResult: responseValue,
expectedPath: '/Test/Unary',
expectedCustomHeaders: {
'x-interceptor': '{id: 1, unary: 1, streaming: 0}'
'x-interceptor': '{id: 1, unary: 1, streaming: 0}',
},
serverHandlers: [handleRequest],
);
@ -135,7 +144,7 @@ void main() {
expectedPath: '/Test/Unary',
expectedCustomHeaders: {
'x-interceptor':
'{id: 1, unary: 1, streaming: 0}, {id: 2, unary: 1, streaming: 0}'
'{id: 1, unary: 1, streaming: 0}, {id: 2, unary: 1, streaming: 0}',
},
serverHandlers: [handleRequest],
);
@ -170,12 +179,13 @@ void main() {
}
await harness.runTest(
clientCall:
harness.client.bidirectional(Stream.fromIterable(requests)).toList(),
clientCall: harness.client
.bidirectional(Stream.fromIterable(requests))
.toList(),
expectedResult: responses,
expectedPath: '/Test/Bidirectional',
expectedCustomHeaders: {
'x-interceptor': '{id: 1, unary: 0, streaming: 1}'
'x-interceptor': '{id: 1, unary: 0, streaming: 1}',
},
serverHandlers: [handleRequest, handleRequest, handleRequest],
doneHandler: handleDone,
@ -211,13 +221,14 @@ void main() {
}
await harness.runTest(
clientCall:
harness.client.bidirectional(Stream.fromIterable(requests)).toList(),
clientCall: harness.client
.bidirectional(Stream.fromIterable(requests))
.toList(),
expectedResult: responses,
expectedPath: '/Test/Bidirectional',
expectedCustomHeaders: {
'x-interceptor':
'{id: 1, unary: 0, streaming: 1}, {id: 2, unary: 0, streaming: 1}'
'{id: 1, unary: 0, streaming: 1}, {id: 2, unary: 0, streaming: 1}',
},
serverHandlers: [handleRequest, handleRequest, handleRequest],
doneHandler: handleDone,
@ -254,8 +265,9 @@ void main() {
}
await harness.runTest(
clientCall:
harness.client.bidirectional(Stream.fromIterable(requests)).toList(),
clientCall: harness.client
.bidirectional(Stream.fromIterable(requests))
.toList(),
expectedResult: responses,
expectedPath: '/Test/Bidirectional',
serverHandlers: [handleRequest, handleRequest, handleRequest],

View File

@ -50,7 +50,8 @@ void main() {
void initKeepAliveManager([ClientKeepAliveOptions? opt]) {
reset(pinger);
final options = opt ??
final options =
opt ??
ClientKeepAliveOptions(
pingInterval: pingInterval,
timeout: timeout,
@ -172,11 +173,13 @@ void main() {
test('transportGoesIdle_doesntCauseIdleWhenEnabled', () {
FakeAsync().run((async) {
keepAliveManager.onTransportTermination();
initKeepAliveManager(ClientKeepAliveOptions(
pingInterval: pingInterval,
timeout: timeout,
permitWithoutCalls: true,
));
initKeepAliveManager(
ClientKeepAliveOptions(
pingInterval: pingInterval,
timeout: timeout,
permitWithoutCalls: true,
),
);
keepAliveManager.onTransportStarted();
// Keepalive scheduling should have started immediately.

View File

@ -78,8 +78,10 @@ void main() {
}
await harness.runTest(
clientCall: harness.client.unary(requestValue,
options: CallOptions(metadata: {'grpc-accept-encoding': 'gzip'})),
clientCall: harness.client.unary(
requestValue,
options: CallOptions(metadata: {'grpc-accept-encoding': 'gzip'}),
),
expectedResult: responseValue,
expectedCustomHeaders: {'grpc-accept-encoding': 'gzip'},
expectedPath: '/Test/Unary',
@ -157,8 +159,9 @@ void main() {
}
await harness.runTest(
clientCall:
harness.client.bidirectional(Stream.fromIterable(requests)).toList(),
clientCall: harness.client
.bidirectional(Stream.fromIterable(requests))
.toList(),
expectedResult: responses,
expectedPath: '/Test/Bidirectional',
serverHandlers: [handleRequest, handleRequest, handleRequest],
@ -189,8 +192,9 @@ void main() {
await harness.runFailureTest(
clientCall: harness.client.unary(dummyValue),
expectedException:
GrpcError.unimplemented('More than one response received'),
expectedException: GrpcError.unimplemented(
'More than one response received',
),
serverHandlers: [handleRequest],
);
});
@ -229,8 +233,9 @@ void main() {
await harness.runFailureTest(
clientCall: harness.client.unary(dummyValue),
expectedException:
GrpcError.unimplemented('Received data before headers'),
expectedException: GrpcError.unimplemented(
'Received data before headers',
),
serverHandlers: [handleRequest],
);
});
@ -245,8 +250,9 @@ void main() {
await harness.runFailureTest(
clientCall: harness.client.unary(dummyValue),
expectedException:
GrpcError.unimplemented('Received data after trailers'),
expectedException: GrpcError.unimplemented(
'Received data after trailers',
),
serverHandlers: [handleRequest],
);
});
@ -271,50 +277,60 @@ void main() {
const customStatusMessage = 'Custom message';
void handleRequest(_) {
harness.toClient.add(HeadersStreamMessage([
Header.ascii(':status', '200'),
Header.ascii('content-type', 'application/grpc'),
Header.ascii('grpc-status', '$customStatusCode'),
Header.ascii('grpc-message', customStatusMessage)
], endStream: true));
harness.toClient.add(
HeadersStreamMessage([
Header.ascii(':status', '200'),
Header.ascii('content-type', 'application/grpc'),
Header.ascii('grpc-status', '$customStatusCode'),
Header.ascii('grpc-message', customStatusMessage),
], endStream: true),
);
harness.toClient.close();
}
await harness.runFailureTest(
clientCall: harness.client.unary(dummyValue),
expectedException:
GrpcError.custom(customStatusCode, customStatusMessage),
expectedException: GrpcError.custom(
customStatusCode,
customStatusMessage,
),
serverHandlers: [handleRequest],
);
});
test('Call throws if HTTP status indicates an error', () async {
void handleRequest(_) {
harness.toClient.add(HeadersStreamMessage([
Header.ascii(':status', HttpStatus.serviceUnavailable.toString()),
Header.ascii('content-type', 'application/grpc'),
]));
harness.toClient.add(
HeadersStreamMessage([
Header.ascii(':status', HttpStatus.serviceUnavailable.toString()),
Header.ascii('content-type', 'application/grpc'),
]),
);
// Send a frame that might be misinterpreted as a length-prefixed proto
// message and cause OOM.
harness.toClient
.add(DataStreamMessage([0, 0xFF, 0xFF, 0xFF, 0xFF], endStream: true));
harness.toClient.add(
DataStreamMessage([0, 0xFF, 0xFF, 0xFF, 0xFF], endStream: true),
);
harness.toClient.close();
}
await harness.runFailureTest(
clientCall: harness.client.unary(dummyValue),
expectedException: GrpcError.unavailable(
'HTTP connection completed with 503 instead of 200'),
'HTTP connection completed with 503 instead of 200',
),
serverHandlers: [handleRequest],
);
});
test('Call throws if content-type indicates an error', () async {
void handleRequest(_) {
harness.toClient.add(HeadersStreamMessage([
Header.ascii(':status', '200'),
Header.ascii('content-type', 'text/html'),
]));
harness.toClient.add(
HeadersStreamMessage([
Header.ascii(':status', '200'),
Header.ascii('content-type', 'text/html'),
]),
);
// Send a frame that might be misinterpreted as a length-prefixed proto
// message and cause OOM.
harness.toClient.add(DataStreamMessage([0, 0xFF, 0xFF, 0xFF, 0xFF]));
@ -323,15 +339,16 @@ void main() {
await harness.runFailureTest(
clientCall: harness.client.unary(dummyValue),
expectedException:
GrpcError.unknown('unsupported content-type (text/html)'),
expectedException: GrpcError.unknown(
'unsupported content-type (text/html)',
),
serverHandlers: [handleRequest],
);
});
for (var contentType in [
'application/json+protobuf',
'application/x-protobuf'
'application/x-protobuf',
]) {
test('$contentType content type is accepted', () async {
const requestValue = 17;
@ -342,10 +359,12 @@ void main() {
expect(mockDecode(data.data), requestValue);
harness
..toClient.add(HeadersStreamMessage([
Header.ascii(':status', '200'),
Header.ascii('content-type', contentType),
]))
..toClient.add(
HeadersStreamMessage([
Header.ascii(':status', '200'),
Header.ascii('content-type', contentType),
]),
)
..sendResponseValue(responseValue)
..sendResponseTrailer();
}
@ -365,19 +384,23 @@ void main() {
const encodedCustomStatusMessage = '%E3%82%A8%E3%83%A9%E3%83%BC';
void handleRequest(_) {
harness.toClient.add(HeadersStreamMessage([
Header.ascii(':status', '200'),
Header.ascii('content-type', 'application/grpc'),
Header.ascii('grpc-status', '$customStatusCode'),
Header.ascii('grpc-message', encodedCustomStatusMessage)
], endStream: true));
harness.toClient.add(
HeadersStreamMessage([
Header.ascii(':status', '200'),
Header.ascii('content-type', 'application/grpc'),
Header.ascii('grpc-status', '$customStatusCode'),
Header.ascii('grpc-message', encodedCustomStatusMessage),
], endStream: true),
);
harness.toClient.close();
}
await harness.runFailureTest(
clientCall: harness.client.unary(dummyValue),
expectedException:
GrpcError.custom(customStatusCode, customStatusMessage),
expectedException: GrpcError.custom(
customStatusCode,
customStatusMessage,
),
serverHandlers: [handleRequest],
);
});
@ -404,9 +427,12 @@ void main() {
..sendResponseTrailer();
}
harness.client = TestClient(harness.channel, decode: (bytes) {
throw 'error decoding';
});
harness.client = TestClient(
harness.channel,
decode: (bytes) {
throw 'error decoding';
},
);
await harness.runFailureTest(
clientCall: harness.client.unary(dummyValue),
@ -474,15 +500,18 @@ void main() {
test('Connection states are reported', () async {
final connectionStates = <ConnectionState>[];
harness.channel.onConnectionStateChanged.listen((state) {
connectionStates.add(state);
}, onDone: () {
expect(connectionStates, [
ConnectionState.connecting,
ConnectionState.ready,
ConnectionState.shutdown
]);
});
harness.channel.onConnectionStateChanged.listen(
(state) {
connectionStates.add(state);
},
onDone: () {
expect(connectionStates, [
ConnectionState.connecting,
ConnectionState.ready,
ConnectionState.shutdown,
]);
},
);
await makeUnaryCall();
});
@ -490,36 +519,49 @@ void main() {
test('Connection errors are reported', () async {
final connectionStates = <ConnectionState>[];
harness.connection!.connectionError = 'Connection error';
harness.channel.onConnectionStateChanged.listen((state) {
connectionStates.add(state);
}, onDone: () {
expect(
connectionStates, [ConnectionState.connecting, ConnectionState.idle]);
});
harness.channel.onConnectionStateChanged.listen(
(state) {
connectionStates.add(state);
},
onDone: () {
expect(connectionStates, [
ConnectionState.connecting,
ConnectionState.idle,
]);
},
);
final expectedException =
GrpcError.unavailable('Error connecting: Connection error');
final expectedException = GrpcError.unavailable(
'Error connecting: Connection error',
);
await harness.expectThrows(
harness.client.unary(dummyValue), expectedException);
harness.client.unary(dummyValue),
expectedException,
);
});
test('Connections time out if idle', () async {
final done = Completer();
final connectionStates = <ConnectionState>[];
harness.channel.onConnectionStateChanged.listen((state) {
connectionStates.add(state);
if (state == ConnectionState.idle) done.complete();
}, onDone: () async {
expect(connectionStates,
[ConnectionState.connecting, ConnectionState.ready]);
await done.future;
expect(connectionStates, [
ConnectionState.connecting,
ConnectionState.ready,
ConnectionState.idle
]);
});
harness.channel.onConnectionStateChanged.listen(
(state) {
connectionStates.add(state);
if (state == ConnectionState.idle) done.complete();
},
onDone: () async {
expect(connectionStates, [
ConnectionState.connecting,
ConnectionState.ready,
]);
await done.future;
expect(connectionStates, [
ConnectionState.connecting,
ConnectionState.ready,
ConnectionState.idle,
]);
},
);
harness.channelOptions.idleTimeout = const Duration(microseconds: 10);
@ -545,38 +587,53 @@ void main() {
test('authority is computed correctly', () {
final emptyOptions = ChannelOptions();
expect(Http2ClientConnection('localhost', 8080, emptyOptions).authority,
'localhost:8080');
expect(Http2ClientConnection('localhost', 443, emptyOptions).authority,
'localhost');
expect(
Http2ClientConnection('localhost', 8080, emptyOptions).authority,
'localhost:8080',
);
expect(
Http2ClientConnection('localhost', 443, emptyOptions).authority,
'localhost',
);
final channelOptions = ChannelOptions(
credentials: ChannelCredentials.insecure(authority: 'myauthority.com'));
expect(Http2ClientConnection('localhost', 8080, channelOptions).authority,
'myauthority.com');
expect(Http2ClientConnection('localhost', 443, channelOptions).authority,
'myauthority.com');
credentials: ChannelCredentials.insecure(authority: 'myauthority.com'),
);
expect(
Http2ClientConnection('localhost', 8080, channelOptions).authority,
'myauthority.com',
);
expect(
Http2ClientConnection('localhost', 443, channelOptions).authority,
'myauthority.com',
);
});
test(
'decodeStatusDetails should decode details into a List<GeneratedMessage> if base64 present',
() {
final decodedDetails = decodeStatusDetails(
'CAMSEGFtb3VudCB0b28gc21hbGwafgopdHlwZS5nb29nbGVhcGlzLmNvbS9nb29nbGUucnBjLkJhZFJlcXVlc3QSUQpPCgZhbW91bnQSRVRoZSByZXF1aXJlZCBjdXJyZW5jeSBjb252ZXJzaW9uIHdvdWxkIHJlc3VsdCBpbiBhIHplcm8gdmFsdWUgcGF5bWVudA');
expect(decodedDetails, isA<List<GeneratedMessage>>());
expect(decodedDetails.length, 1);
});
'decodeStatusDetails should decode details into a List<GeneratedMessage> if base64 present',
() {
final decodedDetails = decodeStatusDetails(
'CAMSEGFtb3VudCB0b28gc21hbGwafgopdHlwZS5nb29nbGVhcGlzLmNvbS9nb29nbGUucnBjLkJhZFJlcXVlc3QSUQpPCgZhbW91bnQSRVRoZSByZXF1aXJlZCBjdXJyZW5jeSBjb252ZXJzaW9uIHdvdWxkIHJlc3VsdCBpbiBhIHplcm8gdmFsdWUgcGF5bWVudA',
);
expect(decodedDetails, isA<List<GeneratedMessage>>());
expect(decodedDetails.length, 1);
},
);
test(
'decodeStatusDetails should decode details into an empty list for an invalid base64 string',
() {
final decodedDetails = decodeStatusDetails('xxxxxxxxxxxxxxxxxxxxxx');
expect(decodedDetails, isA<List<GeneratedMessage>>());
expect(decodedDetails.length, 0);
});
'decodeStatusDetails should decode details into an empty list for an invalid base64 string',
() {
final decodedDetails = decodeStatusDetails('xxxxxxxxxxxxxxxxxxxxxx');
expect(decodedDetails, isA<List<GeneratedMessage>>());
expect(decodedDetails.length, 0);
},
);
test('parseGeneratedMessage should parse out a valid Any type', () {
final status = Status.fromBuffer(base64Url.decode(
'CAMSEGFtb3VudCB0b28gc21hbGwafgopdHlwZS5nb29nbGVhcGlzLmNvbS9nb29nbGUucnBjLkJhZFJlcXVlc3QSUQpPCgZhbW91bnQSRVRoZSByZXF1aXJlZCBjdXJyZW5jeSBjb252ZXJzaW9uIHdvdWxkIHJlc3VsdCBpbiBhIHplcm8gdmFsdWUgcGF5bWVudA=='));
final status = Status.fromBuffer(
base64Url.decode(
'CAMSEGFtb3VudCB0b28gc21hbGwafgopdHlwZS5nb29nbGVhcGlzLmNvbS9nb29nbGUucnBjLkJhZFJlcXVlc3QSUQpPCgZhbW91bnQSRVRoZSByZXF1aXJlZCBjdXJyZW5jeSBjb252ZXJzaW9uIHdvdWxkIHJlc3VsdCBpbiBhIHplcm8gdmFsdWUgcGF5bWVudA==',
),
);
expect(status.details, isNotEmpty);
final detailItem = status.details.first;
@ -586,8 +643,10 @@ void main() {
final castedResult = parsedResult as BadRequest;
expect(castedResult.fieldViolations, isNotEmpty);
expect(castedResult.fieldViolations.first.field_1, 'amount');
expect(castedResult.fieldViolations.first.description,
'The required currency conversion would result in a zero value payment');
expect(
castedResult.fieldViolations.first.description,
'The required currency conversion would result in a zero value payment',
);
});
test('Call should throw details embedded in the headers', () async {
@ -597,13 +656,15 @@ void main() {
'CAMSEGFtb3VudCB0b28gc21hbGwafgopdHlwZS5nb29nbGVhcGlzLmNvbS9nb29nbGUucnBjLkJhZFJlcXVlc3QSUQpPCgZhbW91bnQSRVRoZSByZXF1aXJlZCBjdXJyZW5jeSBjb252ZXJzaW9uIHdvdWxkIHJlc3VsdCBpbiBhIHplcm8gdmFsdWUgcGF5bWVudA';
void handleRequest(_) {
harness.toClient.add(HeadersStreamMessage([
Header.ascii(':status', '200'),
Header.ascii('content-type', 'application/grpc'),
Header.ascii('grpc-status', code.toString()),
Header.ascii('grpc-message', message),
Header.ascii('grpc-status-details-bin', details),
], endStream: true));
harness.toClient.add(
HeadersStreamMessage([
Header.ascii(':status', '200'),
Header.ascii('content-type', 'application/grpc'),
Header.ascii('grpc-status', code.toString()),
Header.ascii('grpc-message', message),
Header.ascii('grpc-status-details-bin', details),
], endStream: true),
);
harness.toClient.close();
}
@ -625,24 +686,21 @@ void main() {
final customVal = 'some custom value';
final customTrailers = <String, String>{customKey: customVal};
void handleRequest(_) {
harness.toClient.add(HeadersStreamMessage([
Header.ascii(':status', '200'),
Header.ascii('content-type', 'application/grpc'),
Header.ascii('grpc-status', code.toString()),
Header.ascii('grpc-message', message),
Header.ascii(customKey, customVal),
], endStream: true));
harness.toClient.add(
HeadersStreamMessage([
Header.ascii(':status', '200'),
Header.ascii('content-type', 'application/grpc'),
Header.ascii('grpc-status', code.toString()),
Header.ascii('grpc-message', message),
Header.ascii(customKey, customVal),
], endStream: true),
);
harness.toClient.close();
}
await harness.runFailureTest(
clientCall: harness.client.unary(dummyValue),
expectedException: GrpcError.custom(
code,
message,
[],
customTrailers,
),
expectedException: GrpcError.custom(code, message, [], customTrailers),
expectedCustomTrailers: customTrailers,
serverHandlers: [handleRequest],
);

View File

@ -128,8 +128,9 @@ void main() {
}
await harness.runTest(
clientCall:
harness.client.bidirectional(Stream.fromIterable(requests)).toList(),
clientCall: harness.client
.bidirectional(Stream.fromIterable(requests))
.toList(),
expectedResult: responses,
expectedPath: '/Test/Bidirectional',
serverHandlers: [handleRequest, handleRequest, handleRequest],
@ -160,8 +161,9 @@ void main() {
await harness.runFailureTest(
clientCall: harness.client.unary(dummyValue),
expectedException:
GrpcError.unimplemented('More than one response received'),
expectedException: GrpcError.unimplemented(
'More than one response received',
),
serverHandlers: [handleRequest],
);
});
@ -200,8 +202,9 @@ void main() {
await harness.runFailureTest(
clientCall: harness.client.unary(dummyValue),
expectedException:
GrpcError.unimplemented('Received data before headers'),
expectedException: GrpcError.unimplemented(
'Received data before headers',
),
serverHandlers: [handleRequest],
);
});
@ -216,8 +219,9 @@ void main() {
await harness.runFailureTest(
clientCall: harness.client.unary(dummyValue),
expectedException:
GrpcError.unimplemented('Received data after trailers'),
expectedException: GrpcError.unimplemented(
'Received data after trailers',
),
serverHandlers: [handleRequest],
);
});
@ -242,19 +246,23 @@ void main() {
const customStatusMessage = 'Custom message';
void handleRequest(_) {
harness.toClient.add(HeadersStreamMessage([
Header.ascii(':status', '200'),
Header.ascii('content-type', 'application/grpc'),
Header.ascii('grpc-status', '$customStatusCode'),
Header.ascii('grpc-message', customStatusMessage)
], endStream: true));
harness.toClient.add(
HeadersStreamMessage([
Header.ascii(':status', '200'),
Header.ascii('content-type', 'application/grpc'),
Header.ascii('grpc-status', '$customStatusCode'),
Header.ascii('grpc-message', customStatusMessage),
], endStream: true),
);
harness.toClient.close();
}
await harness.runFailureTest(
clientCall: harness.client.unary(dummyValue),
expectedException:
GrpcError.custom(customStatusCode, customStatusMessage),
expectedException: GrpcError.custom(
customStatusCode,
customStatusMessage,
),
serverHandlers: [handleRequest],
);
});
@ -281,9 +289,12 @@ void main() {
..sendResponseTrailer();
}
harness.client = TestClient(harness.channel, decode: (bytes) {
throw 'error decoding';
});
harness.client = TestClient(
harness.channel,
decode: (bytes) {
throw 'error decoding';
},
);
await harness.runFailureTest(
clientCall: harness.client.unary(dummyValue),
@ -351,36 +362,49 @@ void main() {
test('Connection errors are reported', () async {
final connectionStates = <ConnectionState>[];
final expectedException =
GrpcError.unavailable('Error connecting: Connection error');
final expectedException = GrpcError.unavailable(
'Error connecting: Connection error',
);
harness.connection!.connectionError = 'Connection error';
harness.channel.onConnectionStateChanged.listen((state) {
connectionStates.add(state);
}, onDone: () async {
await harness.expectThrows(
harness.client.unary(dummyValue), expectedException);
harness.channel.onConnectionStateChanged.listen(
(state) {
connectionStates.add(state);
},
onDone: () async {
await harness.expectThrows(
harness.client.unary(dummyValue),
expectedException,
);
expect(
connectionStates, [ConnectionState.connecting, ConnectionState.idle]);
});
expect(connectionStates, [
ConnectionState.connecting,
ConnectionState.idle,
]);
},
);
});
test('Connections time out if idle', () async {
final done = Completer();
final connectionStates = <ConnectionState>[];
harness.channel.onConnectionStateChanged.listen((state) {
connectionStates.add(state);
if (state == ConnectionState.idle) done.complete();
}, onDone: () async {
expect(connectionStates,
[ConnectionState.connecting, ConnectionState.ready]);
await done.future;
expect(connectionStates, [
ConnectionState.connecting,
ConnectionState.ready,
ConnectionState.idle
]);
});
harness.channel.onConnectionStateChanged.listen(
(state) {
connectionStates.add(state);
if (state == ConnectionState.idle) done.complete();
},
onDone: () async {
expect(connectionStates, [
ConnectionState.connecting,
ConnectionState.ready,
]);
await done.future;
expect(connectionStates, [
ConnectionState.connecting,
ConnectionState.ready,
ConnectionState.idle,
]);
},
);
harness.channelOptions.idleTimeout = const Duration(microseconds: 10);
@ -406,15 +430,24 @@ void main() {
test('authority is computed correctly', () {
final emptyOptions = ChannelOptions();
expect(Http2ClientConnection('localhost', 8080, emptyOptions).authority,
'localhost:8080');
expect(Http2ClientConnection('localhost', 443, emptyOptions).authority,
'localhost');
expect(
Http2ClientConnection('localhost', 8080, emptyOptions).authority,
'localhost:8080',
);
expect(
Http2ClientConnection('localhost', 443, emptyOptions).authority,
'localhost',
);
final channelOptions = ChannelOptions(
credentials: ChannelCredentials.insecure(authority: 'myauthority.com'));
expect(Http2ClientConnection('localhost', 8080, channelOptions).authority,
'myauthority.com');
expect(Http2ClientConnection('localhost', 443, channelOptions).authority,
'myauthority.com');
credentials: ChannelCredentials.insecure(authority: 'myauthority.com'),
);
expect(
Http2ClientConnection('localhost', 8080, channelOptions).authority,
'myauthority.com',
);
expect(
Http2ClientConnection('localhost', 443, channelOptions).authority,
'myauthority.com',
);
});
}

View File

@ -29,8 +29,10 @@ import 'package:stream_transform/stream_transform.dart';
import 'package:test/test.dart';
import 'package:web/web.dart';
final readyStateChangeEvent =
Event('readystatechange', EventInit(bubbles: false, cancelable: false));
final readyStateChangeEvent = Event(
'readystatechange',
EventInit(bubbles: false, cancelable: false),
);
final progressEvent = ProgressEvent('onloadstart');
class MockHttpRequest extends Mock implements IXMLHttpRequest {
@ -63,15 +65,16 @@ class MockHttpRequest extends Mock implements IXMLHttpRequest {
super.noSuchMethod(Invocation.getter(#readyState), returnValue: -1);
@override
Map<String, String> get responseHeaders =>
super.noSuchMethod(Invocation.getter(#responseHeaders),
returnValue: <String, String>{});
Map<String, String> get responseHeaders => super.noSuchMethod(
Invocation.getter(#responseHeaders),
returnValue: <String, String>{},
);
}
class MockXhrClientConnection extends XhrClientConnection {
MockXhrClientConnection({int? code})
: _statusCode = code ?? 200,
super(Uri.parse('test:0'));
: _statusCode = code ?? 200,
super(Uri.parse('test:0'));
late MockHttpRequest latestRequest;
final int _statusCode;
@ -88,54 +91,84 @@ void main() {
test('Make request sends correct headers', () async {
final metadata = <String, String>{
'parameter_1': 'value_1',
'parameter_2': 'value_2'
'parameter_2': 'value_2',
};
final connection = MockXhrClientConnection();
connection.makeRequest('path', Duration(seconds: 10), metadata,
(error, _) => fail(error.toString()));
connection.makeRequest(
'path',
Duration(seconds: 10),
metadata,
(error, _) => fail(error.toString()),
);
verify(connection.latestRequest
.setRequestHeader('Content-Type', 'application/grpc-web+proto'));
verify(connection.latestRequest
.setRequestHeader('X-User-Agent', 'grpc-web-dart/0.1'));
verify(
connection.latestRequest.setRequestHeader(
'Content-Type',
'application/grpc-web+proto',
),
);
verify(
connection.latestRequest.setRequestHeader(
'X-User-Agent',
'grpc-web-dart/0.1',
),
);
verify(connection.latestRequest.setRequestHeader('X-Grpc-Web', '1'));
verify(connection.latestRequest
.overrideMimeType('text/plain; charset=x-user-defined'));
verify(
connection.latestRequest.overrideMimeType(
'text/plain; charset=x-user-defined',
),
);
verify(connection.latestRequest.responseType = 'text');
});
test(
'Make request sends correct headers and path if bypassCorsPreflight=true',
() async {
final metadata = {'header_1': 'value_1', 'header_2': 'value_2'};
final connection = MockXhrClientConnection();
'Make request sends correct headers and path if bypassCorsPreflight=true',
() async {
final metadata = {'header_1': 'value_1', 'header_2': 'value_2'};
final connection = MockXhrClientConnection();
connection.makeRequest('path', Duration(seconds: 10), metadata,
connection.makeRequest(
'path',
Duration(seconds: 10),
metadata,
(error, _) => fail(error.toString()),
callOptions: WebCallOptions(bypassCorsPreflight: true));
callOptions: WebCallOptions(bypassCorsPreflight: true),
);
expect(metadata, isEmpty);
verify(connection.latestRequest.open('POST',
'test:path?%24httpHeaders=header_1%3Avalue_1%0D%0Aheader_2%3Avalue_2%0D%0AContent-Type%3Aapplication%2Fgrpc-web%2Bproto%0D%0AX-User-Agent%3Agrpc-web-dart%2F0.1%0D%0AX-Grpc-Web%3A1%0D%0A'));
verify(connection.latestRequest
.overrideMimeType('text/plain; charset=x-user-defined'));
verify(connection.latestRequest.responseType = 'text');
});
expect(metadata, isEmpty);
verify(
connection.latestRequest.open(
'POST',
'test:path?%24httpHeaders=header_1%3Avalue_1%0D%0Aheader_2%3Avalue_2%0D%0AContent-Type%3Aapplication%2Fgrpc-web%2Bproto%0D%0AX-User-Agent%3Agrpc-web-dart%2F0.1%0D%0AX-Grpc-Web%3A1%0D%0A',
),
);
verify(
connection.latestRequest.overrideMimeType(
'text/plain; charset=x-user-defined',
),
);
verify(connection.latestRequest.responseType = 'text');
},
);
test(
'Make request sends correct headers if call options already have '
test('Make request sends correct headers if call options already have '
'Content-Type header', () async {
final metadata = {
'header_1': 'value_1',
'header_2': 'value_2',
'Content-Type': 'application/json+protobuf'
'Content-Type': 'application/json+protobuf',
};
final connection = MockXhrClientConnection();
connection.makeRequest('/path', Duration(seconds: 10), metadata,
(error, _) => fail(error.toString()));
connection.makeRequest(
'/path',
Duration(seconds: 10),
metadata,
(error, _) => fail(error.toString()),
);
expect(metadata, {
'header_1': 'value_1',
@ -147,12 +180,16 @@ void main() {
test('Content-Type header case insensitivity', () async {
final metadata = {
'header_1': 'value_1',
'CONTENT-TYPE': 'application/json+protobuf'
'CONTENT-TYPE': 'application/json+protobuf',
};
final connection = MockXhrClientConnection();
connection.makeRequest('/path', Duration(seconds: 10), metadata,
(error, _) => fail(error.toString()));
connection.makeRequest(
'/path',
Duration(seconds: 10),
metadata,
(error, _) => fail(error.toString()),
);
expect(metadata, {
'header_1': 'value_1',
'CONTENT-TYPE': 'application/json+protobuf',
@ -160,54 +197,79 @@ void main() {
final lowerMetadata = {
'header_1': 'value_1',
'content-type': 'application/json+protobuf'
'content-type': 'application/json+protobuf',
};
connection.makeRequest('/path', Duration(seconds: 10), lowerMetadata,
(error, _) => fail(error.toString()));
connection.makeRequest(
'/path',
Duration(seconds: 10),
lowerMetadata,
(error, _) => fail(error.toString()),
);
expect(lowerMetadata, {
'header_1': 'value_1',
'content-type': 'application/json+protobuf',
});
});
test('Make request sends correct headers path if only withCredentials=true',
() async {
final metadata = {'header_1': 'value_1', 'header_2': 'value_2'};
final connection = MockXhrClientConnection();
test(
'Make request sends correct headers path if only withCredentials=true',
() async {
final metadata = {'header_1': 'value_1', 'header_2': 'value_2'};
final connection = MockXhrClientConnection();
connection.makeRequest('path', Duration(seconds: 10), metadata,
connection.makeRequest(
'path',
Duration(seconds: 10),
metadata,
(error, _) => fail(error.toString()),
callOptions: WebCallOptions(withCredentials: true));
callOptions: WebCallOptions(withCredentials: true),
);
expect(metadata, {
'header_1': 'value_1',
'header_2': 'value_2',
'Content-Type': 'application/grpc-web+proto',
'X-User-Agent': 'grpc-web-dart/0.1',
'X-Grpc-Web': '1'
});
verify(connection.latestRequest
.setRequestHeader('Content-Type', 'application/grpc-web+proto'));
verify(connection.latestRequest
.setRequestHeader('X-User-Agent', 'grpc-web-dart/0.1'));
verify(connection.latestRequest.setRequestHeader('X-Grpc-Web', '1'));
verify(connection.latestRequest.open('POST', 'test:path'));
verify(connection.latestRequest.withCredentials = true);
verify(connection.latestRequest
.overrideMimeType('text/plain; charset=x-user-defined'));
verify(connection.latestRequest.responseType = 'text');
});
expect(metadata, {
'header_1': 'value_1',
'header_2': 'value_2',
'Content-Type': 'application/grpc-web+proto',
'X-User-Agent': 'grpc-web-dart/0.1',
'X-Grpc-Web': '1',
});
verify(
connection.latestRequest.setRequestHeader(
'Content-Type',
'application/grpc-web+proto',
),
);
verify(
connection.latestRequest.setRequestHeader(
'X-User-Agent',
'grpc-web-dart/0.1',
),
);
verify(connection.latestRequest.setRequestHeader('X-Grpc-Web', '1'));
verify(connection.latestRequest.open('POST', 'test:path'));
verify(connection.latestRequest.withCredentials = true);
verify(
connection.latestRequest.overrideMimeType(
'text/plain; charset=x-user-defined',
),
);
verify(connection.latestRequest.responseType = 'text');
},
);
test('Sent data converted to stream properly', () async {
final metadata = <String, String>{
'parameter_1': 'value_1',
'parameter_2': 'value_2'
'parameter_2': 'value_2',
};
final connection = MockXhrClientConnection();
final stream = connection.makeRequest('path', Duration(seconds: 10),
metadata, (error, _) => fail(error.toString()));
final stream = connection.makeRequest(
'path',
Duration(seconds: 10),
metadata,
(error, _) => fail(error.toString()),
);
final data = List.filled(10, 0);
stream.outgoingMessages.add(data);
@ -215,7 +277,8 @@ void main() {
final expectedData = frame(data);
verify(
connection.latestRequest.send(Uint8List.fromList(expectedData).toJS));
connection.latestRequest.send(Uint8List.fromList(expectedData).toJS),
);
});
test('Stream handles headers properly', () async {
@ -227,21 +290,28 @@ void main() {
final transport = MockXhrClientConnection();
final stream = transport.makeRequest('test_path', Duration(seconds: 10), {},
(error, _) => fail(error.toString()));
final stream = transport.makeRequest(
'test_path',
Duration(seconds: 10),
{},
(error, _) => fail(error.toString()),
);
when(transport.latestRequest.responseHeaders).thenReturn(responseHeaders);
when(transport.latestRequest.responseText)
.thenReturn(String.fromCharCodes(frame(<int>[])));
when(
transport.latestRequest.responseText,
).thenReturn(String.fromCharCodes(frame(<int>[])));
// Set expectation for request readyState and generate two readyStateChange
// events, so that incomingMessages stream completes.
final readyStates = [XMLHttpRequest.HEADERS_RECEIVED, XMLHttpRequest.DONE];
when(transport.latestRequest.readyState).thenReturnInOrder(readyStates);
transport.latestRequest.readyStateChangeController
.add(readyStateChangeEvent);
transport.latestRequest.readyStateChangeController
.add(readyStateChangeEvent);
transport.latestRequest.readyStateChangeController.add(
readyStateChangeEvent,
);
transport.latestRequest.readyStateChangeController.add(
readyStateChangeEvent,
);
// Should be only one metadata message with headers augmented with :status
// field.
@ -261,13 +331,19 @@ void main() {
final connection = MockXhrClientConnection();
final stream = connection.makeRequest('test_path', Duration(seconds: 10),
requestHeaders, (error, _) => fail(error.toString()));
final stream = connection.makeRequest(
'test_path',
Duration(seconds: 10),
requestHeaders,
(error, _) => fail(error.toString()),
);
final encodedTrailers = frame(responseTrailers.entries
.map((e) => '${e.key}:${e.value}')
.join('\r\n')
.codeUnits);
final encodedTrailers = frame(
responseTrailers.entries
.map((e) => '${e.key}:${e.value}')
.join('\r\n')
.codeUnits,
);
encodedTrailers[0] = 0x80; // Mark this frame as trailers.
final encodedString = String.fromCharCodes(encodedTrailers);
@ -276,31 +352,37 @@ void main() {
// Set expectation for request readyState and generate events so that
// incomingMessages stream completes.
when(connection.latestRequest.readyState).thenReturnInOrder(
[XMLHttpRequest.HEADERS_RECEIVED, XMLHttpRequest.DONE]);
connection.latestRequest.readyStateChangeController
.add(readyStateChangeEvent);
when(
connection.latestRequest.readyState,
).thenReturnInOrder([XMLHttpRequest.HEADERS_RECEIVED, XMLHttpRequest.DONE]);
connection.latestRequest.readyStateChangeController.add(
readyStateChangeEvent,
);
connection.latestRequest.progressController.add(progressEvent);
connection.latestRequest.readyStateChangeController
.add(readyStateChangeEvent);
connection.latestRequest.readyStateChangeController.add(
readyStateChangeEvent,
);
// Should be two metadata messages: headers and trailers.
final messages =
await stream.incomingMessages.whereType<GrpcMetadata>().toList();
final messages = await stream.incomingMessages
.whereType<GrpcMetadata>()
.toList();
expect(messages.length, 2);
expect(messages.first.metadata, requestHeaders);
expect(messages.last.metadata, responseTrailers);
});
test('Stream handles empty trailers properly', () async {
final requestHeaders = {
'content-type': 'application/grpc+proto',
};
final requestHeaders = {'content-type': 'application/grpc+proto'};
final connection = MockXhrClientConnection();
final stream = connection.makeRequest('test_path', Duration(seconds: 10),
{}, (error, _) => fail(error.toString()));
final stream = connection.makeRequest(
'test_path',
Duration(seconds: 10),
{},
(error, _) => fail(error.toString()),
);
final encoded = frame(''.codeUnits);
encoded[0] = 0x80; // Mark this frame as trailers.
@ -310,17 +392,21 @@ void main() {
when(connection.latestRequest.responseText).thenReturn(encodedString);
// Set expectation for request readyState and generate events so that
// incomingMessages stream completes.
when(connection.latestRequest.readyState).thenReturnInOrder(
[XMLHttpRequest.HEADERS_RECEIVED, XMLHttpRequest.DONE]);
connection.latestRequest.readyStateChangeController
.add(readyStateChangeEvent);
when(
connection.latestRequest.readyState,
).thenReturnInOrder([XMLHttpRequest.HEADERS_RECEIVED, XMLHttpRequest.DONE]);
connection.latestRequest.readyStateChangeController.add(
readyStateChangeEvent,
);
connection.latestRequest.progressController.add(progressEvent);
connection.latestRequest.readyStateChangeController
.add(readyStateChangeEvent);
connection.latestRequest.readyStateChangeController.add(
readyStateChangeEvent,
);
// Should be two metadata messages: headers and trailers.
final messages =
await stream.incomingMessages.whereType<GrpcMetadata>().toList();
final messages = await stream.incomingMessages
.whereType<GrpcMetadata>()
.toList();
expect(messages.length, 2);
expect(messages.first.metadata, requestHeaders);
expect(messages.last.metadata, isEmpty);
@ -335,22 +421,30 @@ void main() {
final connection = MockXhrClientConnection();
final stream = connection.makeRequest('test_path', Duration(seconds: 10),
requestHeaders, (error, _) => fail(error.toString()));
final stream = connection.makeRequest(
'test_path',
Duration(seconds: 10),
requestHeaders,
(error, _) => fail(error.toString()),
);
final data = List<int>.filled(10, 224);
when(connection.latestRequest.responseHeaders).thenReturn(requestHeaders);
when(connection.latestRequest.responseText)
.thenReturn(String.fromCharCodes(frame(data)));
when(
connection.latestRequest.responseText,
).thenReturn(String.fromCharCodes(frame(data)));
// Set expectation for request readyState and generate events, so that
// incomingMessages stream completes.
when(connection.latestRequest.readyState).thenReturnInOrder(
[XMLHttpRequest.HEADERS_RECEIVED, XMLHttpRequest.DONE]);
connection.latestRequest.readyStateChangeController
.add(readyStateChangeEvent);
when(
connection.latestRequest.readyState,
).thenReturnInOrder([XMLHttpRequest.HEADERS_RECEIVED, XMLHttpRequest.DONE]);
connection.latestRequest.readyStateChangeController.add(
readyStateChangeEvent,
);
connection.latestRequest.progressController.add(progressEvent);
connection.latestRequest.readyStateChangeController
.add(readyStateChangeEvent);
connection.latestRequest.readyStateChangeController.add(
readyStateChangeEvent,
);
// Expect a single data message.
final message = await stream.incomingMessages.whereType<GrpcData>().single;
@ -368,12 +462,14 @@ void main() {
errors.add(e as GrpcError);
});
const errorDetails = 'error details';
when(connection.latestRequest.responseHeaders)
.thenReturn({'content-type': 'application/grpc+proto'});
when(
connection.latestRequest.responseHeaders,
).thenReturn({'content-type': 'application/grpc+proto'});
when(connection.latestRequest.readyState).thenReturn(XMLHttpRequest.DONE);
when(connection.latestRequest.responseText).thenReturn(errorDetails);
connection.latestRequest.readyStateChangeController
.add(readyStateChangeEvent);
connection.latestRequest.readyStateChangeController.add(
readyStateChangeEvent,
);
await errorReceived.future;
expect(errors.single.rawResponse, errorDetails);
});
@ -387,19 +483,25 @@ void main() {
final connection = MockXhrClientConnection();
final stream = connection.makeRequest('test_path', Duration(seconds: 10),
metadata, (error, _) => fail(error.toString()));
final stream = connection.makeRequest(
'test_path',
Duration(seconds: 10),
metadata,
(error, _) => fail(error.toString()),
);
final data = <List<int>>[
List<int>.filled(10, 224),
List<int>.filled(5, 124)
List<int>.filled(5, 124),
];
final encodedStrings =
data.map((d) => String.fromCharCodes(frame(d))).toList();
final encodedStrings = data
.map((d) => String.fromCharCodes(frame(d)))
.toList();
when(connection.latestRequest.responseHeaders).thenReturn(metadata);
when(connection.latestRequest.readyState)
.thenReturn(XMLHttpRequest.HEADERS_RECEIVED);
when(
connection.latestRequest.readyState,
).thenReturn(XMLHttpRequest.HEADERS_RECEIVED);
// At first invocation the response should be the the first message, after
// that first + last messages.
@ -413,13 +515,15 @@ void main() {
});
final readyStates = [XMLHttpRequest.HEADERS_RECEIVED, XMLHttpRequest.DONE];
when(connection.latestRequest.readyState)
.thenAnswer((_) => readyStates.removeAt(0));
when(
connection.latestRequest.readyState,
).thenAnswer((_) => readyStates.removeAt(0));
final queue = StreamQueue(stream.incomingMessages);
// Headers.
connection.latestRequest.readyStateChangeController
.add(readyStateChangeEvent);
connection.latestRequest.readyStateChangeController.add(
readyStateChangeEvent,
);
expect(((await queue.next) as GrpcMetadata).metadata, metadata);
// Data 1.
connection.latestRequest.progressController.add(progressEvent);
@ -428,8 +532,9 @@ void main() {
connection.latestRequest.progressController.add(progressEvent);
expect(((await queue.next) as GrpcData).data, data[1]);
// Done.
connection.latestRequest.readyStateChangeController
.add(readyStateChangeEvent);
connection.latestRequest.readyStateChangeController.add(
readyStateChangeEvent,
);
expect(await queue.hasNext, isFalse);
});
}

View File

@ -32,11 +32,15 @@ void main() {
expect(channel is GrpcWebClientChannel, isTrue);
final webChannel = channel as GrpcWebClientChannel;
expect(
webChannel.uri, equals(Uri(host: host, port: port, scheme: 'https')));
webChannel.uri,
equals(Uri(host: host, port: port, scheme: 'https')),
);
});
test('Constructor grpc on web throws UnsupportedError', () {
expect(() => GrpcOrGrpcWebClientChannel.grpc(host, port: port),
throwsUnsupportedError);
expect(
() => GrpcOrGrpcWebClientChannel.grpc(host, port: port),
throwsUnsupportedError,
);
});
}

View File

@ -24,8 +24,10 @@ void testUds(String name, FutureOr<void> Function(InternetAddress) testCase) {
test(name, () async {
final tempDir = await Directory.systemTemp.createTemp();
final address = InternetAddress('${tempDir.path}/socket',
type: InternetAddressType.unix);
final address = InternetAddress(
'${tempDir.path}/socket',
type: InternetAddressType.unix,
);
addTearDown(() => tempDir.delete(recursive: true));
await testCase(address);
});
@ -33,8 +35,10 @@ void testUds(String name, FutureOr<void> Function(InternetAddress) testCase) {
/// Test functionality for both TCP and Unix domain sockets.
void testTcpAndUds(
String name, FutureOr<void> Function(InternetAddress) testCase,
{String host = 'localhost'}) {
String name,
FutureOr<void> Function(InternetAddress) testCase, {
String host = 'localhost',
}) {
test(name, () async {
final address = await InternetAddress.lookup(host);
await testCase(address.first);

View File

@ -112,7 +112,9 @@ void main() {
test('Server returns error on unimplemented path', () async {
harness
..expectErrorResponse(
StatusCode.unimplemented, 'Path /Test/NotFound not found')
StatusCode.unimplemented,
'Path /Test/NotFound not found',
)
..sendRequestHeader('/Test/NotFound');
await harness.fromServer.done;
});
@ -120,7 +122,8 @@ void main() {
/// Returns a service method handler that verifies that awaiting the request
/// throws a specific error.
Future<int> Function(ServiceCall call, Future<int> request) expectError(
expectedError) {
expectedError,
) {
return expectAsync2((ServiceCall call, Future<int> request) async {
try {
final result = await request;
@ -140,7 +143,7 @@ void main() {
/// Returns a service method handler that verifies that awaiting the request
/// stream throws a specific error.
Stream<int> Function(ServiceCall call, Stream<int> request)
expectErrorStreaming(expectedError) {
expectErrorStreaming(expectedError) {
return (ServiceCall call, Stream<int> request) async* {
try {
await for (var entry in request) {
@ -160,30 +163,35 @@ void main() {
test('Server returns error on missing request for unary call', () async {
harness
..service.unaryHandler =
expectError(GrpcError.unimplemented('No request received'))
..service.unaryHandler = expectError(
GrpcError.unimplemented('No request received'),
)
..expectErrorResponse(StatusCode.unimplemented, 'No request received')
..sendRequestHeader('/Test/Unary')
..toServer.close();
await harness.fromServer.done;
});
test('Server returns error if multiple headers are received for unary call',
() async {
harness
..service.unaryHandler =
expectError(GrpcError.unimplemented('Expected request'))
..expectErrorResponse(StatusCode.unimplemented, 'Expected request')
..sendRequestHeader('/Test/Unary')
..toServer.add(HeadersStreamMessage([]))
..toServer.close();
await harness.fromServer.done;
});
test(
'Server returns error if multiple headers are received for unary call',
() async {
harness
..service.unaryHandler = expectError(
GrpcError.unimplemented('Expected request'),
)
..expectErrorResponse(StatusCode.unimplemented, 'Expected request')
..sendRequestHeader('/Test/Unary')
..toServer.add(HeadersStreamMessage([]))
..toServer.close();
await harness.fromServer.done;
},
);
test('Server returns error on too many requests for unary call', () async {
harness
..service.unaryHandler =
expectError(GrpcError.unimplemented('Too many requests'))
..service.unaryHandler = expectError(
GrpcError.unimplemented('Too many requests'),
)
..expectErrorResponse(StatusCode.unimplemented, 'Too many requests')
..sendRequestHeader('/Test/Unary')
..sendData(dummyValue)
@ -195,9 +203,12 @@ void main() {
test('Server returns request deserialization errors', () async {
harness
..service.bidirectionalHandler = expectErrorStreaming(
GrpcError.internal('Error deserializing request: Failed'))
GrpcError.internal('Error deserializing request: Failed'),
)
..expectErrorResponse(
StatusCode.internal, 'Error deserializing request: Failed')
StatusCode.internal,
'Error deserializing request: Failed',
)
..sendRequestHeader('/Test/RequestError')
..sendData(dummyValue)
..toServer.close();
@ -207,9 +218,12 @@ void main() {
test('Server returns response serialization errors', () async {
harness
..service.bidirectionalHandler = expectErrorStreaming(
GrpcError.internal('Error sending response: Failed'))
GrpcError.internal('Error sending response: Failed'),
)
..expectErrorResponse(
StatusCode.internal, 'Error sending response: Failed')
StatusCode.internal,
'Error sending response: Failed',
)
..sendRequestHeader('/Test/ResponseError')
..sendData(dummyValue)
..sendData(dummyValue)
@ -257,11 +271,13 @@ void main() {
harness
..service.unaryHandler = methodHandler
..fromServer.stream.listen(expectAsync1((_) {}, count: 0),
onError: expectAsync1((dynamic error) {
expect(error, 'TERMINATED');
}, count: 1),
onDone: expectAsync0(() {}, count: 1))
..fromServer.stream.listen(
expectAsync1((_) {}, count: 0),
onError: expectAsync1((dynamic error) {
expect(error, 'TERMINATED');
}, count: 1),
onDone: expectAsync0(() {}, count: 1),
)
..sendRequestHeader('/Test/Unary')
..toServer.addError('CANCEL');
@ -271,14 +287,17 @@ void main() {
});
test(
'Server returns error if request stream is closed before sending anything',
() async {
harness
..expectErrorResponse(
StatusCode.unavailable, 'Request stream closed unexpectedly')
..toServer.close();
await harness.fromServer.done;
});
'Server returns error if request stream is closed before sending anything',
() async {
harness
..expectErrorResponse(
StatusCode.unavailable,
'Request stream closed unexpectedly',
)
..toServer.close();
await harness.fromServer.done;
},
);
group('Server with interceptor', () {
group('processes calls if interceptor allows request', () {
@ -306,8 +325,10 @@ void main() {
}
test('with sync interceptor', () => doTest(interceptor));
test('with async interceptor',
() => doTest((call, method) async => interceptor(call, method)));
test(
'with async interceptor',
() => doTest((call, method) async => interceptor(call, method)),
);
});
group('returns error if interceptor blocks request', () {
@ -322,15 +343,19 @@ void main() {
harness
..interceptor.handler = handler
..expectErrorResponse(
StatusCode.unauthenticated, 'Request is unauthenticated')
StatusCode.unauthenticated,
'Request is unauthenticated',
)
..sendRequestHeader('/Test/Unary');
await harness.fromServer.done;
}
test('with sync interceptor', () => doTest(interceptor));
test('with async interceptor',
() => doTest((call, method) async => interceptor(call, method)));
test(
'with async interceptor',
() => doTest((call, method) async => interceptor(call, method)),
);
});
group('returns internal error if interceptor throws exception', () {
@ -342,15 +367,19 @@ void main() {
harness
..interceptor.handler = handler
..expectErrorResponse(
StatusCode.internal, 'Exception: Reason is unknown')
StatusCode.internal,
'Exception: Reason is unknown',
)
..sendRequestHeader('/Test/Unary');
await harness.fromServer.done;
}
test('with sync interceptor', () => doTest(interceptor));
test('with async interceptor',
() => doTest((call, method) async => interceptor(call, method)));
test(
'with async interceptor',
() => doTest((call, method) async => interceptor(call, method)),
);
});
test("don't fail if interceptor await 2 times", () async {
@ -363,7 +392,9 @@ void main() {
harness
..interceptor.handler = interceptor
..expectErrorResponse(
StatusCode.internal, 'Exception: Reason is unknown')
StatusCode.internal,
'Exception: Reason is unknown',
)
..sendRequestHeader('/Test/Unary')
..sendData(1);

View File

@ -22,11 +22,15 @@ import 'package:test/test.dart';
void main() {
test('decoding an empty repeated', () async {
final data = await GrpcWebDecoder()
.bind(Stream.fromIterable([
Uint8List.fromList([0, 0, 0, 0, 0]).buffer
]))
.first as GrpcData;
final data =
await GrpcWebDecoder()
.bind(
Stream.fromIterable([
Uint8List.fromList([0, 0, 0, 0, 0]).buffer,
]),
)
.first
as GrpcData;
expect(data.data, []);
});
}

View File

@ -36,7 +36,9 @@ class EchoService extends EchoServiceBase {
@override
Stream<ServerStreamingEchoResponse> serverStreamingEcho(
ServiceCall call, ServerStreamingEchoRequest request) async* {
ServiceCall call,
ServerStreamingEchoRequest request,
) async* {
for (var i = 0; i < request.messageCount; i++) {
yield ServerStreamingEchoResponse()..message = request.message;
if (i < request.messageCount - 1) {
@ -132,7 +134,8 @@ Future<void> hybridMain(StreamChannel channel) async {
final tempDir = await Directory.systemTemp.createTemp();
final config = p.join(tempDir.path, 'config.yaml');
await File(config).writeAsString(
envoyConfig.replaceAll('%TARGET_PORT%', server.port.toString()));
envoyConfig.replaceAll('%TARGET_PORT%', server.port.toString()),
);
// Spawn a proxy that would translate gRPC-web protocol into gRPC protocol
// for us. We use envoy proxy. See CONTRIBUTING.md for setup.
@ -152,24 +155,25 @@ if you are running tests locally.
// Parse output of the proxy process looking for a port it selected.
final portRe = RegExp(
r'Set listener listener_0 socket factory local address to 0.0.0.0:(\d+)');
r'Set listener listener_0 socket factory local address to 0.0.0.0:(\d+)',
);
proxy.stderr
.transform(utf8.decoder)
.transform(const LineSplitter())
.listen((line) {
proxy.stderr.transform(utf8.decoder).transform(const LineSplitter()).listen((
line,
) {
_info('envoy|stderr] $line');
final m = portRe.firstMatch(line);
if (m != null) {
channel.sink
.add({'grpcPort': int.parse(m[1]!), 'httpPort': httpServer.port});
channel.sink.add({
'grpcPort': int.parse(m[1]!),
'httpPort': httpServer.port,
});
}
});
proxy.stdout
.transform(utf8.decoder)
.transform(const LineSplitter())
.listen((line) {
proxy.stdout.transform(utf8.decoder).transform(const LineSplitter()).listen((
line,
) {
_info('envoy|stdout] $line');
});
@ -228,9 +232,11 @@ Future<HttpServer> startHttpServer() async {
server.defaultResponseHeaders.add('Access-Control-Allow-Origin', '*');
server.listen((request) async {
_info('${request.method} ${request.requestedUri} ${request.headers}');
final message = await GrpcHttpDecoder()
.bind(request.map((list) => DataStreamMessage(list)))
.first as GrpcData;
final message =
await GrpcHttpDecoder()
.bind(request.map((list) => DataStreamMessage(list)))
.first
as GrpcData;
final echoRequest = EchoRequest.fromBuffer(message.data);
(testCases[echoRequest.message] ?? defaultHandler)(request.response);
});

View File

@ -55,16 +55,19 @@ void main() {
// in one go).
final sw = Stopwatch()..start();
final timings = await service
.serverStreamingEcho(ServerStreamingEchoRequest()
..message = testMessage
..messageCount = 20
..messageInterval = 100)
.serverStreamingEcho(
ServerStreamingEchoRequest()
..message = testMessage
..messageCount = 20
..messageInterval = 100,
)
.map((response) {
expect(response.message, equals(testMessage));
final timing = sw.elapsedMilliseconds;
sw.reset();
return timing;
}).toList();
expect(response.message, equals(testMessage));
final timing = sw.elapsedMilliseconds;
sw.reset();
return timing;
})
.toList();
final maxDelay = timings.reduce(math.max);
expect(maxDelay, lessThan(500));
});
@ -84,26 +87,36 @@ void main() {
var terminated = false;
service
.serverStreamingEcho(ServerStreamingEchoRequest()
..message = testMessage
..messageCount = 20
..messageInterval = 100)
.listen((response) {
expect(response.message, equals(testMessage));
}, onError: (e) {
expect(terminated, isTrue);
});
.serverStreamingEcho(
ServerStreamingEchoRequest()
..message = testMessage
..messageCount = 20
..messageInterval = 100,
)
.listen(
(response) {
expect(response.message, equals(testMessage));
},
onError: (e) {
expect(terminated, isTrue);
},
);
service
.serverStreamingEcho(ServerStreamingEchoRequest()
..message = testMessage
..messageCount = 20
..messageInterval = 100)
.listen((response) {
expect(response.message, equals(testMessage));
}, onError: (e) {
expect(terminated, isTrue);
});
.serverStreamingEcho(
ServerStreamingEchoRequest()
..message = testMessage
..messageCount = 20
..messageInterval = 100,
)
.listen(
(response) {
expect(response.message, equals(testMessage));
},
onError: (e) {
expect(terminated, isTrue);
},
);
await Future.delayed(Duration(milliseconds: 500));
terminated = true;
@ -118,13 +131,15 @@ void main() {
const testMessage = 'hello from gRPC-web';
final stream = service
.serverStreamingEcho(ServerStreamingEchoRequest()
..message = testMessage
..messageCount = 20
..messageInterval = 100)
.serverStreamingEcho(
ServerStreamingEchoRequest()
..message = testMessage
..messageCount = 20
..messageInterval = 100,
)
.listen((response) {
expect(response.message, equals(testMessage));
});
expect(response.message, equals(testMessage));
});
await Future.delayed(Duration(milliseconds: 500));
await stream.cancel();
@ -134,11 +149,14 @@ void main() {
final invalidResponseTests = {
'cors': GrpcError.unknown(
'HTTP request completed without a status (potential CORS issue)'),
'HTTP request completed without a status (potential CORS issue)',
),
'status-503': GrpcError.unavailable(
'HTTP connection completed with 503 instead of 200'),
'bad-content-type':
GrpcError.unknown('unsupported content-type (text/html)'),
'HTTP connection completed with 503 instead of 200',
),
'bad-content-type': GrpcError.unknown(
'unsupported content-type (text/html)',
),
};
for (var entry in invalidResponseTests.entries) {
@ -150,10 +168,14 @@ void main() {
// See [startHttpServer] in [grpc_web_server.dart] for the server part.
test('invalid response: ${entry.key}', () async {
final channel = GrpcWebClientChannel.xhr(server.httpUri);
final service = EchoServiceClient(channel,
options: WebCallOptions(bypassCorsPreflight: true));
expect(() => service.echo(EchoRequest()..message = 'test:${entry.key}'),
throwsA(entry.value));
final service = EchoServiceClient(
channel,
options: WebCallOptions(bypassCorsPreflight: true),
);
expect(
() => service.echo(EchoRequest()..message = 'test:${entry.key}'),
throwsA(entry.value),
);
});
}
}
@ -181,23 +203,28 @@ class GrpcWebServer {
static Future<GrpcWebServer> start() async {
// Spawn the server code on the server side, it will send us back port
// number we should be talking to.
final serverChannel =
spawnHybridUri('grpc_web_server.dart', stayAlive: true);
final serverChannel = spawnHybridUri(
'grpc_web_server.dart',
stayAlive: true,
);
final portCompleter = Completer<Map>();
final exitCompleter = Completer<void>();
serverChannel.stream.listen((event) {
if (!portCompleter.isCompleted) {
portCompleter.complete(event);
} else if (event == 'EXITED') {
exitCompleter.complete();
}
}, onError: (e) {
if (!portCompleter.isCompleted) {
portCompleter.completeError(e);
} else if (!exitCompleter.isCompleted) {
exitCompleter.completeError(e);
}
});
serverChannel.stream.listen(
(event) {
if (!portCompleter.isCompleted) {
portCompleter.complete(event);
} else if (event == 'EXITED') {
exitCompleter.complete();
}
},
onError: (e) {
if (!portCompleter.isCompleted) {
portCompleter.completeError(e);
} else if (!exitCompleter.isCompleted) {
exitCompleter.completeError(e);
}
},
);
final ports = await portCompleter.future;
@ -208,9 +235,10 @@ class GrpcWebServer {
// because browsers like chrome don't trust self-signed certificates by
// default.
return GrpcWebServer(
serverChannel,
exitCompleter.future,
Uri.parse('http://localhost:$grpcPort'),
Uri.parse('http://localhost:$httpPort'));
serverChannel,
exitCompleter.future,
Uri.parse('http://localhost:$grpcPort'),
Uri.parse('http://localhost:$httpPort'),
);
}
}

View File

@ -80,40 +80,46 @@ void main() {
await server.shutdown();
});
test('Server terminates connection after too many pings without data',
() async {
await fakeClient.echo(EchoRequest());
await Future.delayed(timeout * maxBadPings * 2);
await fakeClient.echo(EchoRequest());
// Check that the server closed the connection, the next request then has
// to build a new one.
expect(fakeChannel.newConnectionCounter, 2);
});
test('Server doesnt terminate connection after pings, as data is sent',
() async {
for (var i = 0; i < 10; i++) {
test(
'Server terminates connection after too many pings without data',
() async {
await fakeClient.echo(EchoRequest());
await Future.delayed(timeout * 0.2);
}
await Future.delayed(timeout * maxBadPings * 2);
await fakeClient.echo(EchoRequest());
// Check that the server closed the connection, the next request then has
// to build a new one.
expect(fakeChannel.newConnectionCounter, 2);
},
);
// Check that the server never closed the connection
expect(fakeChannel.newConnectionCounter, 1);
});
test(
'Server doesnt terminate connection after pings, as data is sent',
() async {
for (var i = 0; i < 10; i++) {
await fakeClient.echo(EchoRequest());
await Future.delayed(timeout * 0.2);
}
test('Server doesnt ack the ping, making the client shutdown the transport',
() async {
//Send a first request, get a connection
await unresponsiveClient.echo(EchoRequest());
expect(unresponsiveChannel.newConnectionCounter, 1);
// Check that the server never closed the connection
expect(fakeChannel.newConnectionCounter, 1);
},
);
//Ping is not being acked on time
await Future.delayed(timeout * 2);
test(
'Server doesnt ack the ping, making the client shutdown the transport',
() async {
//Send a first request, get a connection
await unresponsiveClient.echo(EchoRequest());
expect(unresponsiveChannel.newConnectionCounter, 1);
//A second request gets a new connection
await unresponsiveClient.echo(EchoRequest());
expect(unresponsiveChannel.newConnectionCounter, 2);
});
//Ping is not being acked on time
await Future.delayed(timeout * 2);
//A second request gets a new connection
await unresponsiveClient.echo(EchoRequest());
expect(unresponsiveChannel.newConnectionCounter, 2);
},
);
}
/// A wrapper around a [FakeHttp2ClientConnection]
@ -160,8 +166,11 @@ class UnresponsiveClientChannel extends FakeClientChannel {
@override
ClientConnection createConnection() {
fakeHttp2ClientConnection =
UnresponsiveHttp2ClientConnection(host, port, options);
fakeHttp2ClientConnection = UnresponsiveHttp2ClientConnection(
host,
port,
options,
);
return fakeHttp2ClientConnection!;
}
}
@ -182,10 +191,11 @@ class UnresponsiveHttp2ClientConnection extends FakeHttp2ClientConnection {
}
class FakeClientKeepAlive extends ClientKeepAlive {
FakeClientKeepAlive(
{required super.options,
required super.ping,
required super.onPingTimeout});
FakeClientKeepAlive({
required super.options,
required super.ping,
required super.onPingTimeout,
});
@override
void onFrameReceived() {
@ -200,6 +210,7 @@ class FakeEchoService extends EchoServiceBase {
@override
Stream<ServerStreamingEchoResponse> serverStreamingEcho(
ServiceCall call, ServerStreamingEchoRequest request) =>
throw UnsupportedError('Not used in this test');
ServiceCall call,
ServerStreamingEchoRequest request,
) => throw UnsupportedError('Not used in this test');
}

View File

@ -27,16 +27,21 @@ void main() {
test('report password errors correctly', () async {
final certificates = await File('test/data/certstore.p12').readAsBytes();
final missingPassword =
ChannelCredentials.secure(certificates: certificates);
final missingPassword = ChannelCredentials.secure(
certificates: certificates,
);
expect(() => missingPassword.securityContext, throwsA(isTlsException));
final wrongPassword = ChannelCredentials.secure(
certificates: certificates, password: 'wrong');
certificates: certificates,
password: 'wrong',
);
expect(() => wrongPassword.securityContext, throwsA(isTlsException));
final correctPassword = ChannelCredentials.secure(
certificates: certificates, password: 'correct');
certificates: certificates,
password: 'correct',
);
expect(correctPassword.securityContext, isNotNull);
});
});

View File

@ -86,6 +86,7 @@ class FakeEchoService extends EchoServiceBase {
@override
Stream<ServerStreamingEchoResponse> serverStreamingEcho(
ServiceCall call, ServerStreamingEchoRequest request) =>
throw UnimplementedError();
ServiceCall call,
ServerStreamingEchoRequest request,
) => throw UnimplementedError();
}

View File

@ -68,6 +68,7 @@ class FakeEchoService extends EchoServiceBase {
@override
Stream<ServerStreamingEchoResponse> serverStreamingEcho(
ServiceCall call, ServerStreamingEchoRequest request) =>
throw UnimplementedError();
ServiceCall call,
ServerStreamingEchoRequest request,
) => throw UnimplementedError();
}

View File

@ -28,14 +28,20 @@ import 'package:test/test.dart';
import 'common.dart';
class TestClient extends Client {
static final _$stream = ClientMethod<int, int>('/test.TestService/stream',
(int value) => [value], (List<int> value) => value[0]);
static final _$stream = ClientMethod<int, int>(
'/test.TestService/stream',
(int value) => [value],
(List<int> value) => value[0],
);
TestClient(super.channel);
ResponseStream<int> stream(int request, {CallOptions? options}) {
return $createStreamingCall(_$stream, Stream.value(request),
options: options);
return $createStreamingCall(
_$stream,
Stream.value(request),
options: options,
);
}
}
@ -46,8 +52,16 @@ class TestService extends Service {
String get $name => 'test.TestService';
TestService({this.expectedAuthority}) {
$addMethod(ServiceMethod<int, int>('stream', stream, false, true,
(List<int> value) => value[0], (int value) => [value]));
$addMethod(
ServiceMethod<int, int>(
'stream',
stream,
false,
true,
(List<int> value) => value[0],
(int value) => [value],
),
);
}
static const requestFiniteStream = 1;
@ -85,10 +99,7 @@ class TestServiceWithGrpcError extends TestService {
'This error should contain trailers',
null,
null,
{
'key1': 'value1',
'key2': 'value2',
},
{'key1': 'value1', 'key2': 'value2'},
);
}
}
@ -111,57 +122,74 @@ Future<void> main() async {
final server = Server.create(services: [TestService()]);
await server.serve(address: address, port: 0);
final channel = FixedConnectionClientChannel(Http2ClientConnection(
address,
server.port!,
ChannelOptions(credentials: ChannelCredentials.insecure()),
));
final channel = FixedConnectionClientChannel(
Http2ClientConnection(
address,
server.port!,
ChannelOptions(credentials: ChannelCredentials.insecure()),
),
);
final testClient = TestClient(channel);
expect(await testClient.stream(TestService.requestFiniteStream).toList(),
[1, 2, 3]);
expect(await testClient.stream(TestService.requestFiniteStream).toList(), [
1,
2,
3,
]);
server.shutdown();
});
testUds('UDS provides valid default authority', (address) async {
// round trip test of insecure connection.
final server =
Server.create(services: [TestService(expectedAuthority: 'localhost')]);
final server = Server.create(
services: [TestService(expectedAuthority: 'localhost')],
);
await server.serve(address: address, port: 0);
final channel = FixedConnectionClientChannel(Http2ClientConnection(
address,
server.port!,
ChannelOptions(credentials: ChannelCredentials.insecure()),
));
final channel = FixedConnectionClientChannel(
Http2ClientConnection(
address,
server.port!,
ChannelOptions(credentials: ChannelCredentials.insecure()),
),
);
final testClient = TestClient(channel);
expect(await testClient.stream(TestService.requestFiniteStream).toList(),
[1, 2, 3]);
expect(await testClient.stream(TestService.requestFiniteStream).toList(), [
1,
2,
3,
]);
server.shutdown();
});
testTcpAndUds('round trip with outgoing and incoming compression',
(address) async {
testTcpAndUds('round trip with outgoing and incoming compression', (
address,
) async {
final server = Server.create(
services: [TestService()],
codecRegistry: CodecRegistry(codecs: const [GzipCodec()]),
);
await server.serve(address: address, port: 0);
final channel = FixedConnectionClientChannel(Http2ClientConnection(
address,
server.port!,
ChannelOptions(
credentials: ChannelCredentials.insecure(),
codecRegistry: CodecRegistry(codecs: const [GzipCodec()]),
final channel = FixedConnectionClientChannel(
Http2ClientConnection(
address,
server.port!,
ChannelOptions(
credentials: ChannelCredentials.insecure(),
codecRegistry: CodecRegistry(codecs: const [GzipCodec()]),
),
),
));
);
final testClient = TestClient(channel);
expect(
await testClient
.stream(TestService.requestFiniteStream,
options: CallOptions(compression: const GzipCodec()))
.toList(),
[1, 2, 3]);
await testClient
.stream(
TestService.requestFiniteStream,
options: CallOptions(compression: const GzipCodec()),
)
.toList(),
[1, 2, 3],
);
await server.shutdown();
});
@ -169,40 +197,53 @@ Future<void> main() async {
// round trip test of secure connection.
final server = Server.create(services: [TestService()]);
await server.serve(
address: address,
port: 0,
security: ServerTlsCredentials(
certificate: File('test/data/localhost.crt').readAsBytesSync(),
privateKey: File('test/data/localhost.key').readAsBytesSync()));
address: address,
port: 0,
security: ServerTlsCredentials(
certificate: File('test/data/localhost.crt').readAsBytesSync(),
privateKey: File('test/data/localhost.key').readAsBytesSync(),
),
);
final channel = FixedConnectionClientChannel(Http2ClientConnection(
address,
server.port!,
ChannelOptions(
final channel = FixedConnectionClientChannel(
Http2ClientConnection(
address,
server.port!,
ChannelOptions(
credentials: ChannelCredentials.secure(
certificates: File('test/data/localhost.crt').readAsBytesSync(),
authority: 'localhost')),
));
certificates: File('test/data/localhost.crt').readAsBytesSync(),
authority: 'localhost',
),
),
),
);
final testClient = TestClient(channel);
expect(await testClient.stream(TestService.requestFiniteStream).toList(),
[1, 2, 3]);
expect(await testClient.stream(TestService.requestFiniteStream).toList(), [
1,
2,
3,
]);
server.shutdown();
});
test('exception in onMetadataException', () async {
final server =
Server.create(services: [TestServiceWithOnMetadataException()]);
final server = Server.create(
services: [TestServiceWithOnMetadataException()],
);
await server.serve(address: 'localhost', port: 0);
final channel = FixedConnectionClientChannel(Http2ClientConnection(
'localhost',
server.port!,
ChannelOptions(credentials: ChannelCredentials.insecure()),
));
final channel = FixedConnectionClientChannel(
Http2ClientConnection(
'localhost',
server.port!,
ChannelOptions(credentials: ChannelCredentials.insecure()),
),
);
final testClient = TestClient(channel);
await expectLater(
testClient.stream(TestService.requestFiniteStream).toList(),
throwsA(isA<GrpcError>()));
testClient.stream(TestService.requestFiniteStream).toList(),
throwsA(isA<GrpcError>()),
);
await server.shutdown();
});
@ -210,11 +251,13 @@ Future<void> main() async {
final server = Server.create(services: [TestService()]);
await server.serve(address: 'localhost', port: 0);
final channel = FixedConnectionClientChannel(Http2ClientConnection(
'localhost',
server.port!,
ChannelOptions(credentials: ChannelCredentials.insecure()),
));
final channel = FixedConnectionClientChannel(
Http2ClientConnection(
'localhost',
server.port!,
ChannelOptions(credentials: ChannelCredentials.insecure()),
),
);
final testClient = TestClient(channel);
expect(await testClient.stream(TestService.requestInfiniteStream).first, 1);
await channel.shutdown();
@ -225,24 +268,29 @@ Future<void> main() async {
final server = Server.create(services: [TestServiceWithGrpcError()]);
await server.serve(address: 'localhost', port: 0);
final channel = FixedConnectionClientChannel(Http2ClientConnection(
'localhost',
server.port!,
ChannelOptions(credentials: ChannelCredentials.insecure()),
));
final channel = FixedConnectionClientChannel(
Http2ClientConnection(
'localhost',
server.port!,
ChannelOptions(credentials: ChannelCredentials.insecure()),
),
);
final testClient = TestClient(channel);
await expectLater(
testClient.stream(TestService.requestFiniteStream).toList(),
throwsA(predicate<GrpcError>((e) {
final trailers = e.trailers;
if (trailers == null || trailers.length != 2) return false;
final entries = trailers.entries.toList();
final isOk = entries[0].key == 'key1' &&
entries[0].value == 'value1' &&
entries[1].key == 'key2' &&
entries[1].value == 'value2';
return isOk;
})),
throwsA(
predicate<GrpcError>((e) {
final trailers = e.trailers;
if (trailers == null || trailers.length != 2) return false;
final entries = trailers.entries.toList();
final isOk =
entries[0].key == 'key1' &&
entries[0].value == 'value1' &&
entries[1].key == 'key2' &&
entries[1].value == 'value2';
return isOk;
}),
),
);
await server.shutdown();
});

View File

@ -28,7 +28,9 @@ class EchoService extends EchoServiceBase {
@override
Stream<ServerStreamingEchoResponse> serverStreamingEcho(
ServiceCall call, ServerStreamingEchoRequest request) async* {
ServiceCall call,
ServerStreamingEchoRequest request,
) async* {
for (var i = 0; i < request.messageCount; i++) {
yield ServerStreamingEchoResponse(message: '$i');
await Future.delayed(Duration(milliseconds: request.messageInterval));
@ -44,9 +46,7 @@ void main() {
server.handlers.entries.firstOrNull?.value.length ?? 0;
setUp(() async {
server = Server.create(
services: [EchoService()],
);
server = Server.create(services: [EchoService()]);
await server.serve(address: 'localhost', port: 0);
channel = ClientChannel(
'localhost',
@ -65,12 +65,12 @@ void main() {
messageCount: 5,
messageInterval: 5,
);
final stream1 = EchoServiceClient(channel)
.serverStreamingEcho(request)
.asBroadcastStream();
final stream2 = EchoServiceClient(channel)
.serverStreamingEcho(request)
.asBroadcastStream();
final stream1 = EchoServiceClient(
channel,
).serverStreamingEcho(request).asBroadcastStream();
final stream2 = EchoServiceClient(
channel,
).serverStreamingEcho(request).asBroadcastStream();
expect(numberHandlers(), 0);

View File

@ -27,16 +27,22 @@ import 'common.dart';
class TestClient extends grpc.Client {
static final _$infiniteStream = grpc.ClientMethod<int, int>(
'/test.TestService/infiniteStream',
(int value) => [value],
(List<int> value) => value[0]);
'/test.TestService/infiniteStream',
(int value) => [value],
(List<int> value) => value[0],
);
TestClient(grpc.ClientChannel super.channel);
grpc.ResponseStream<int> infiniteStream(int request,
{grpc.CallOptions? options}) {
return $createStreamingCall(_$infiniteStream, Stream.value(request),
options: options);
grpc.ResponseStream<int> infiniteStream(
int request, {
grpc.CallOptions? options,
}) {
return $createStreamingCall(
_$infiniteStream,
Stream.value(request),
options: options,
);
}
}
@ -46,12 +52,22 @@ class TestService extends grpc.Service {
final void Function() finallyCallback;
TestService({required this.finallyCallback}) {
$addMethod(grpc.ServiceMethod<int, int>('infiniteStream', infiniteStream,
false, true, (List<int> value) => value[0], (int value) => [value]));
$addMethod(
grpc.ServiceMethod<int, int>(
'infiniteStream',
infiniteStream,
false,
true,
(List<int> value) => value[0],
(int value) => [value],
),
);
}
Stream<int> infiniteStream(
grpc.ServiceCall call, Future<int> request) async* {
grpc.ServiceCall call,
Future<int> request,
) async* {
var count = await request;
try {
while (true) {
@ -73,8 +89,11 @@ class ClientData {
final int port;
final SendPort sendPort;
ClientData(
{required this.address, required this.port, required this.sendPort});
ClientData({
required this.address,
required this.port,
required this.sendPort,
});
}
void client(ClientData clientData) async {
@ -85,37 +104,52 @@ void client(ClientData clientData) async {
credentials: grpc.ChannelCredentials.insecure(),
),
);
TestClient(channel).infiniteStream(1).listen((count) async {
await channel.terminate();
}, onError: (e) {
clientData.sendPort.send(e);
});
TestClient(channel)
.infiniteStream(1)
.listen(
(count) async {
await channel.terminate();
},
onError: (e) {
clientData.sendPort.send(e);
},
);
}
Future<void> main() async {
testTcpAndUds(
'the client interrupting the connection does not crash the server',
(address) async {
// interrrupt the connect of client, the server does not crash.
late grpc.Server server;
server = grpc.Server.create(services: [
TestService(
finallyCallback: expectAsync0(() {
expect(server.shutdown(), completes);
}, reason: 'the producer should get cancelled'),
)
]);
await server.serve(address: address, port: 0);
final receivePort = ReceivePort();
Isolate.spawn<ClientData>(
'the client interrupting the connection does not crash the server',
(address) async {
// interrrupt the connect of client, the server does not crash.
late grpc.Server server;
server = grpc.Server.create(
services: [
TestService(
finallyCallback: expectAsync0(() {
expect(server.shutdown(), completes);
}, reason: 'the producer should get cancelled'),
),
],
);
await server.serve(address: address, port: 0);
final receivePort = ReceivePort();
Isolate.spawn<ClientData>(
client,
ClientData(
address: address,
port: server.port!,
sendPort: receivePort.sendPort));
receivePort.listen(expectAsync1((e) {
expect(e, isA<grpc.GrpcError>());
receivePort.close();
}, reason: 'the client should send an error from the destroyed channel'));
});
address: address,
port: server.port!,
sendPort: receivePort.sendPort,
),
);
receivePort.listen(
expectAsync1(
(e) {
expect(e, isA<grpc.GrpcError>());
receivePort.close();
},
reason: 'the client should send an error from the destroyed channel',
),
);
},
);
}

View File

@ -27,15 +27,16 @@ void main() {
var goAway = false;
void initServer([ServerKeepAliveOptions? options]) => ServerKeepAlive(
options: options ??
ServerKeepAliveOptions(
maxBadPings: maxBadPings,
minIntervalBetweenPingsWithoutData: Duration(milliseconds: 5),
),
pingNotifier: pingStream.stream,
dataNotifier: dataStream.stream,
tooManyBadPings: () async => goAway = true,
).handle();
options:
options ??
ServerKeepAliveOptions(
maxBadPings: maxBadPings,
minIntervalBetweenPingsWithoutData: Duration(milliseconds: 5),
),
pingNotifier: pingStream.stream,
dataNotifier: dataStream.stream,
tooManyBadPings: () async => goAway = true,
).handle();
setUp(() {
pingStream = StreamController();
@ -72,25 +73,28 @@ void main() {
});
});
test(
'Sending too many pings without data doesn`t kill connection if the server doesn`t care',
() async {
FakeAsync().run((async) {
initServer(ServerKeepAliveOptions(
maxBadPings: null,
minIntervalBetweenPingsWithoutData: Duration(milliseconds: 5),
));
// Send good ping
pingStream.sink.add(null);
async.elapse(timeAfterPing);
// Send a lot of bad pings, that's still ok.
for (var i = 0; i < 50; i++) {
'Sending too many pings without data doesn`t kill connection if the server doesn`t care',
() async {
FakeAsync().run((async) {
initServer(
ServerKeepAliveOptions(
maxBadPings: null,
minIntervalBetweenPingsWithoutData: Duration(milliseconds: 5),
),
);
// Send good ping
pingStream.sink.add(null);
}
async.elapse(timeAfterPing);
expect(goAway, false);
});
});
async.elapse(timeAfterPing);
// Send a lot of bad pings, that's still ok.
for (var i = 0; i < 50; i++) {
pingStream.sink.add(null);
}
async.elapse(timeAfterPing);
expect(goAway, false);
});
},
);
test('Sending many pings with data doesn`t kill connection', () async {
FakeAsync().run((async) {

View File

@ -112,7 +112,9 @@ void main() {
test('Server returns error on unimplemented path', () async {
harness
..expectErrorResponse(
StatusCode.unimplemented, 'Path /Test/NotFound not found')
StatusCode.unimplemented,
'Path /Test/NotFound not found',
)
..sendRequestHeader('/Test/NotFound');
await harness.fromServer.done;
});
@ -120,7 +122,8 @@ void main() {
/// Returns a service method handler that verifies that awaiting the request
/// throws a specific error.
Future<int> Function(ServiceCall call, Future<int> request) expectError(
expectedError) {
expectedError,
) {
return expectAsync2((ServiceCall call, Future<int> request) async {
try {
final result = await request;
@ -140,7 +143,7 @@ void main() {
/// Returns a service method handler that verifies that awaiting the request
/// stream throws a specific error.
Stream<int> Function(ServiceCall call, Stream<int> request)
expectErrorStreaming(expectedError) {
expectErrorStreaming(expectedError) {
return (ServiceCall call, Stream<int> request) async* {
try {
await for (var entry in request) {
@ -160,8 +163,9 @@ void main() {
test('Server returns error on missing request for unary call', () async {
harness
..service.unaryHandler =
expectError(GrpcError.unimplemented('No request received'))
..service.unaryHandler = expectError(
GrpcError.unimplemented('No request received'),
)
..expectErrorResponse(StatusCode.unimplemented, 'No request received')
..sendRequestHeader('/Test/Unary')
..toServer.close();
@ -182,22 +186,26 @@ void main() {
await harness.fromServer.done;
});
test('Server returns error if multiple headers are received for unary call',
() async {
harness
..service.unaryHandler =
expectError(GrpcError.unimplemented('Expected request'))
..expectErrorResponse(StatusCode.unimplemented, 'Expected request')
..sendRequestHeader('/Test/Unary')
..toServer.add(HeadersStreamMessage([]))
..toServer.close();
await harness.fromServer.done;
});
test(
'Server returns error if multiple headers are received for unary call',
() async {
harness
..service.unaryHandler = expectError(
GrpcError.unimplemented('Expected request'),
)
..expectErrorResponse(StatusCode.unimplemented, 'Expected request')
..sendRequestHeader('/Test/Unary')
..toServer.add(HeadersStreamMessage([]))
..toServer.close();
await harness.fromServer.done;
},
);
test('Server returns error on too many requests for unary call', () async {
harness
..service.unaryHandler =
expectError(GrpcError.unimplemented('Too many requests'))
..service.unaryHandler = expectError(
GrpcError.unimplemented('Too many requests'),
)
..expectErrorResponse(StatusCode.unimplemented, 'Too many requests')
..sendRequestHeader('/Test/Unary')
..sendData(dummyValue)
@ -209,9 +217,12 @@ void main() {
test('Server returns request deserialization errors', () async {
harness
..service.bidirectionalHandler = expectErrorStreaming(
GrpcError.internal('Error deserializing request: Failed'))
GrpcError.internal('Error deserializing request: Failed'),
)
..expectErrorResponse(
StatusCode.internal, 'Error deserializing request: Failed')
StatusCode.internal,
'Error deserializing request: Failed',
)
..sendRequestHeader('/Test/RequestError')
..sendData(dummyValue)
..toServer.close();
@ -221,9 +232,12 @@ void main() {
test('Server returns response serialization errors', () async {
harness
..service.bidirectionalHandler = expectErrorStreaming(
GrpcError.internal('Error sending response: Failed'))
GrpcError.internal('Error sending response: Failed'),
)
..expectErrorResponse(
StatusCode.internal, 'Error sending response: Failed')
StatusCode.internal,
'Error sending response: Failed',
)
..sendRequestHeader('/Test/ResponseError')
..sendData(dummyValue)
..sendData(dummyValue)
@ -271,11 +285,13 @@ void main() {
harness
..service.unaryHandler = methodHandler
..fromServer.stream.listen(expectAsync1((_) {}, count: 0),
onError: expectAsync1((dynamic error) {
expect(error, 'TERMINATED');
}, count: 1),
onDone: expectAsync0(() {}, count: 1))
..fromServer.stream.listen(
expectAsync1((_) {}, count: 0),
onError: expectAsync1((dynamic error) {
expect(error, 'TERMINATED');
}, count: 1),
onDone: expectAsync0(() {}, count: 1),
)
..sendRequestHeader('/Test/Unary')
..toServer.addError('CANCEL');
@ -285,14 +301,17 @@ void main() {
});
test(
'Server returns error if request stream is closed before sending anything',
() async {
harness
..expectErrorResponse(
StatusCode.unavailable, 'Request stream closed unexpectedly')
..toServer.close();
await harness.fromServer.done;
});
'Server returns error if request stream is closed before sending anything',
() async {
harness
..expectErrorResponse(
StatusCode.unavailable,
'Request stream closed unexpectedly',
)
..toServer.close();
await harness.fromServer.done;
},
);
group('Server with interceptor', () {
group('processes calls if interceptor allows request', () {
@ -320,8 +339,10 @@ void main() {
}
test('with sync interceptor', () => doTest(interceptor));
test('with async interceptor',
() => doTest((call, method) async => interceptor(call, method)));
test(
'with async interceptor',
() => doTest((call, method) async => interceptor(call, method)),
);
});
group('returns error if interceptor blocks request', () {
@ -336,15 +357,19 @@ void main() {
harness
..interceptor.handler = handler
..expectErrorResponse(
StatusCode.unauthenticated, 'Request is unauthenticated')
StatusCode.unauthenticated,
'Request is unauthenticated',
)
..sendRequestHeader('/Test/Unary');
await harness.fromServer.done;
}
test('with sync interceptor', () => doTest(interceptor));
test('with async interceptor',
() => doTest((call, method) async => interceptor(call, method)));
test(
'with async interceptor',
() => doTest((call, method) async => interceptor(call, method)),
);
});
group('returns internal error if interceptor throws exception', () {
@ -356,15 +381,19 @@ void main() {
harness
..interceptor.handler = handler
..expectErrorResponse(
StatusCode.internal, 'Exception: Reason is unknown')
StatusCode.internal,
'Exception: Reason is unknown',
)
..sendRequestHeader('/Test/Unary');
await harness.fromServer.done;
}
test('with sync interceptor', () => doTest(interceptor));
test('with async interceptor',
() => doTest((call, method) async => interceptor(call, method)));
test(
'with async interceptor',
() => doTest((call, method) async => interceptor(call, method)),
);
});
test("don't fail if interceptor await 2 times", () async {
@ -377,7 +406,9 @@ void main() {
harness
..interceptor.handler = interceptor
..expectErrorResponse(
StatusCode.internal, 'Exception: Reason is unknown')
StatusCode.internal,
'Exception: Reason is unknown',
)
..sendRequestHeader('/Test/Unary')
..sendData(1);
@ -412,9 +443,11 @@ void main() {
test('with sync interceptor', () => doTest(interceptor));
test(
'with async interceptor',
() => doTest((call, method, requests) async =>
interceptor(call, method, requests)));
'with async interceptor',
() => doTest(
(call, method, requests) async => interceptor(call, method, requests),
),
);
});
group('returns error if interceptor blocks request', () {
@ -429,7 +462,9 @@ void main() {
harness
..serverInterceptor.onStart = handler
..expectErrorResponse(
StatusCode.unauthenticated, 'Request is unauthenticated')
StatusCode.unauthenticated,
'Request is unauthenticated',
)
..sendRequestHeader('/Test/Unary');
await harness.fromServer.done;
@ -437,9 +472,11 @@ void main() {
test('with sync interceptor', () => doTest(interceptor));
test(
'with async interceptor',
() => doTest((call, method, request) async =>
interceptor(call, method, request)));
'with async interceptor',
() => doTest(
(call, method, request) async => interceptor(call, method, request),
),
);
});
test("don't fail if interceptor await 2 times", () async {
@ -490,7 +527,7 @@ void main() {
onFinish: (call, method, requests) {
invocationsOrderRecords.add('Done');
},
)
),
]);
expect(invocationsOrderRecords, equals(['Start', 'Data [7]', 'Done']));
@ -521,19 +558,20 @@ void main() {
onFinish: (call, method, requests) {
invocationsOrderRecords.add('Done 2');
},
)
),
]);
expect(
invocationsOrderRecords,
equals([
'Start 1',
'Start 2',
'Data 2 [7]',
'Data 1 [7]',
'Done 2',
'Done 1',
]));
invocationsOrderRecords,
equals([
'Start 1',
'Start 2',
'Data 2 [7]',
'Data 1 [7]',
'Done 2',
'Done 1',
]),
);
});
});
@ -556,13 +594,15 @@ void main() {
invocationsOrderRecords.add('Done 1');
},
),
TestServerInterruptingInterceptor(transform: <R>(value) {
if (value is int) {
return value * 2 as R;
}
TestServerInterruptingInterceptor(
transform: <R>(value) {
if (value is int) {
return value * 2 as R;
}
return value;
}),
return value;
},
),
TestServerInterceptor(
onStart: (call, method, requests) {
invocationsOrderRecords.add('Start 2');
@ -573,7 +613,7 @@ void main() {
onFinish: (call, method, requests) {
invocationsOrderRecords.add('Done 2');
},
)
),
];
Future<int> methodHandler(ServiceCall call, Future<int> request) async {
@ -590,15 +630,16 @@ void main() {
await harness.fromServer.done;
expect(
invocationsOrderRecords,
equals([
'Start 1',
'Start 2',
'Data 2 [7]',
'Data 1 [14]',
'Done 2',
'Done 1',
]));
invocationsOrderRecords,
equals([
'Start 1',
'Start 2',
'Data 2 [7]',
'Data 1 [14]',
'Done 2',
'Done 1',
]),
);
});
});
}

View File

@ -35,7 +35,7 @@ class FakeConnection extends Http2ClientConnection {
Object? connectionError;
FakeConnection(String host, this.transport, ChannelOptions options)
: super(host, 443, options);
: super(host, 443, options);
@override
Future<ClientTransportConnection> connectTransport() async {
@ -50,7 +50,7 @@ class FakeClientTransportConnection extends Http2ClientConnection {
Object? connectionError;
FakeClientTransportConnection(this.connector, ChannelOptions options)
: super.fromClientTransportConnector(connector, options);
: super.fromClientTransportConnector(connector, options);
@override
Future<ClientTransportConnection> connectTransport() async {
@ -91,7 +91,7 @@ class FakeChannel extends ClientChannel {
FakeChannelOptions get options => super.options as FakeChannelOptions;
FakeChannel(String super.host, this.connection, FakeChannelOptions options)
: super(options: options);
: super(options: options);
@override
Future<Http2ClientConnection> getConnection() async => connection;
@ -104,8 +104,10 @@ class FakeClientConnectorChannel extends ClientTransportConnectorChannel {
FakeChannelOptions get options => super.options as FakeChannelOptions;
FakeClientConnectorChannel(
super.connector, this.connection, FakeChannelOptions options)
: super(options: options);
super.connector,
this.connection,
FakeChannelOptions options,
) : super(options: options);
@override
Future<Http2ClientConnection> getConnection() async => connection;
@ -121,34 +123,57 @@ class TestClient extends Client {
final int Function(List<int> value) decode;
TestClient(super.channel,
{super.options, super.interceptors, this.decode = mockDecode}) {
TestClient(
super.channel, {
super.options,
super.interceptors,
this.decode = mockDecode,
}) {
_$unary = ClientMethod<int, int>('/Test/Unary', mockEncode, decode);
_$clientStreaming =
ClientMethod<int, int>('/Test/ClientStreaming', mockEncode, decode);
_$serverStreaming =
ClientMethod<int, int>('/Test/ServerStreaming', mockEncode, decode);
_$bidirectional =
ClientMethod<int, int>('/Test/Bidirectional', mockEncode, decode);
_$clientStreaming = ClientMethod<int, int>(
'/Test/ClientStreaming',
mockEncode,
decode,
);
_$serverStreaming = ClientMethod<int, int>(
'/Test/ServerStreaming',
mockEncode,
decode,
);
_$bidirectional = ClientMethod<int, int>(
'/Test/Bidirectional',
mockEncode,
decode,
);
}
ResponseFuture<int> unary(int request, {CallOptions? options}) {
return $createUnaryCall(_$unary, request, options: options);
}
ResponseFuture<int> clientStreaming(Stream<int> request,
{CallOptions? options}) {
return $createStreamingCall(_$clientStreaming, request, options: options)
.single;
ResponseFuture<int> clientStreaming(
Stream<int> request, {
CallOptions? options,
}) {
return $createStreamingCall(
_$clientStreaming,
request,
options: options,
).single;
}
ResponseStream<int> serverStreaming(int request, {CallOptions? options}) {
return $createStreamingCall(_$serverStreaming, Stream.value(request),
options: options);
return $createStreamingCall(
_$serverStreaming,
Stream.value(request),
options: options,
);
}
ResponseStream<int> bidirectional(Stream<int> request,
{CallOptions? options}) {
ResponseStream<int> bidirectional(
Stream<int> request, {
CallOptions? options,
}) {
return $createStreamingCall(_$bidirectional, request, options: options);
}
}
@ -226,8 +251,9 @@ abstract class _Harness {
stream = MockClientTransportStream();
fromClient = StreamController();
toClient = StreamController();
when(transport.makeRequest(any, endStream: anyNamed('endStream')))
.thenReturn(stream);
when(
transport.makeRequest(any, endStream: anyNamed('endStream')),
).thenReturn(stream);
when(transport.onActiveStateChanged = captureAny).thenReturn(null);
when(transport.isOpen).thenReturn(true);
when(stream.outgoingMessages).thenReturn(fromClient.sink);
@ -247,9 +273,7 @@ abstract class _Harness {
Header.ascii('content-type', 'application/grpc'),
];
static final _defaultTrailers = [
Header.ascii('grpc-status', '0'),
];
static final _defaultTrailers = [Header.ascii('grpc-status', '0')];
void sendResponseHeader() {
assert(!headersWereSent);
@ -262,54 +286,66 @@ abstract class _Harness {
}
void sendResponseTrailer({bool closeStream = true}) {
toClient.add(HeadersStreamMessage([
if (!headersWereSent) ..._defaultHeaders,
..._defaultTrailers,
], endStream: true));
toClient.add(
HeadersStreamMessage([
if (!headersWereSent) ..._defaultHeaders,
..._defaultTrailers,
], endStream: true),
);
if (closeStream) toClient.close();
}
void signalIdle() {
final ActiveStateHandler handler =
verify(transport.onActiveStateChanged = captureAny).captured.single;
final ActiveStateHandler handler = verify(
transport.onActiveStateChanged = captureAny,
).captured.single;
expect(handler, isNotNull);
handler(false);
}
Future<void> runTest(
{Future? clientCall,
dynamic expectedResult,
String? expectedPath,
Duration? expectedTimeout,
Map<String, String>? expectedCustomHeaders,
List<MessageHandler> serverHandlers = const [],
void Function()? doneHandler,
bool expectDone = true}) async {
Future<void> runTest({
Future? clientCall,
dynamic expectedResult,
String? expectedPath,
Duration? expectedTimeout,
Map<String, String>? expectedCustomHeaders,
List<MessageHandler> serverHandlers = const [],
void Function()? doneHandler,
bool expectDone = true,
}) async {
var serverHandlerIndex = 0;
void handleServerMessage(StreamMessage message) {
serverHandlers[serverHandlerIndex++](message);
}
final clientSubscription = fromClient.stream.listen(
expectAsync1(handleServerMessage, count: serverHandlers.length),
onError: expectAsync1((dynamic _) {}, count: 0),
onDone: expectAsync0(doneHandler ?? () {}, count: expectDone ? 1 : 0));
expectAsync1(handleServerMessage, count: serverHandlers.length),
onError: expectAsync1((dynamic _) {}, count: 0),
onDone: expectAsync0(doneHandler ?? () {}, count: expectDone ? 1 : 0),
);
final result = await clientCall;
if (expectedResult != null) {
expect(result, expectedResult);
}
final List<Header> capturedHeaders =
verify(transport.makeRequest(captureAny)).captured.single;
final List<Header> capturedHeaders = verify(
transport.makeRequest(captureAny),
).captured.single;
validateRequestHeaders(
Map.fromEntries(capturedHeaders.map((header) =>
MapEntry(utf8.decode(header.name), utf8.decode(header.value)))),
path: expectedPath,
authority: expectedAuthority,
timeout:
expectedTimeout == null ? null : toTimeoutString(expectedTimeout),
customHeaders: expectedCustomHeaders);
Map.fromEntries(
capturedHeaders.map(
(header) =>
MapEntry(utf8.decode(header.name), utf8.decode(header.value)),
),
),
path: expectedPath,
authority: expectedAuthority,
timeout: expectedTimeout == null
? null
: toTimeoutString(expectedTimeout),
customHeaders: expectedCustomHeaders,
);
await clientSubscription.cancel();
}
@ -335,15 +371,16 @@ abstract class _Harness {
}
}
Future<void> runFailureTest(
{Future? clientCall,
dynamic expectedException,
String? expectedPath,
Duration? expectedTimeout,
Map<String, String>? expectedCustomHeaders,
Map<String, String>? expectedCustomTrailers,
List<MessageHandler> serverHandlers = const [],
bool expectDone = true}) async {
Future<void> runFailureTest({
Future? clientCall,
dynamic expectedException,
String? expectedPath,
Duration? expectedTimeout,
Map<String, String>? expectedCustomHeaders,
Map<String, String>? expectedCustomTrailers,
List<MessageHandler> serverHandlers = const [],
bool expectDone = true,
}) async {
return runTest(
clientCall: expectThrows(
clientCall,

View File

@ -29,24 +29,53 @@ class TestService extends Service {
Future<int> Function(ServiceCall call, Future<int> request)? unaryHandler;
Future<int> Function(ServiceCall call, Stream<int> request)?
clientStreamingHandler;
clientStreamingHandler;
Stream<int> Function(ServiceCall call, Future<int> request)?
serverStreamingHandler;
serverStreamingHandler;
Stream<int> Function(ServiceCall call, Stream<int> request)?
bidirectionalHandler;
bidirectionalHandler;
TestService() {
$addMethod(ServerHarness.createMethod('Unary', _unary, false, false));
$addMethod(ServerHarness.createMethod(
'ClientStreaming', _clientStreaming, true, false));
$addMethod(ServerHarness.createMethod(
'ServerStreaming', _serverStreaming, false, true));
$addMethod(ServerHarness.createMethod(
'Bidirectional', _bidirectional, true, true));
$addMethod(ServiceMethod<int, int>('RequestError', _bidirectional, true,
true, (List<int> value) => throw 'Failed', mockEncode));
$addMethod(ServiceMethod<int, int>('ResponseError', _bidirectional, true,
true, mockDecode, (int value) => throw 'Failed'));
$addMethod(
ServerHarness.createMethod(
'ClientStreaming',
_clientStreaming,
true,
false,
),
);
$addMethod(
ServerHarness.createMethod(
'ServerStreaming',
_serverStreaming,
false,
true,
),
);
$addMethod(
ServerHarness.createMethod('Bidirectional', _bidirectional, true, true),
);
$addMethod(
ServiceMethod<int, int>(
'RequestError',
_bidirectional,
true,
true,
(List<int> value) => throw 'Failed',
mockEncode,
),
);
$addMethod(
ServiceMethod<int, int>(
'ResponseError',
_bidirectional,
true,
true,
mockDecode,
(int value) => throw 'Failed',
),
);
}
Future<int> _unary(ServiceCall call, Future<int> request) {
@ -90,12 +119,17 @@ class TestInterceptor {
}
}
typedef TestServerInterceptorOnStart = Function(
ServiceCall call, ServiceMethod method, Stream requests);
typedef TestServerInterceptorOnData = Function(
ServiceCall call, ServiceMethod method, Stream requests, dynamic data);
typedef TestServerInterceptorOnFinish = Function(
ServiceCall call, ServiceMethod method, Stream requests);
typedef TestServerInterceptorOnStart =
Function(ServiceCall call, ServiceMethod method, Stream requests);
typedef TestServerInterceptorOnData =
Function(
ServiceCall call,
ServiceMethod method,
Stream requests,
dynamic data,
);
typedef TestServerInterceptorOnFinish =
Function(ServiceCall call, ServiceMethod method, Stream requests);
class TestServerInterceptor extends ServerInterceptor {
TestServerInterceptorOnStart? onStart;
@ -105,12 +139,20 @@ class TestServerInterceptor extends ServerInterceptor {
TestServerInterceptor({this.onStart, this.onData, this.onFinish});
@override
Stream<R> intercept<Q, R>(ServiceCall call, ServiceMethod<Q, R> method,
Stream<Q> requests, ServerStreamingInvoker<Q, R> invoker) async* {
Stream<R> intercept<Q, R>(
ServiceCall call,
ServiceMethod<Q, R> method,
Stream<Q> requests,
ServerStreamingInvoker<Q, R> invoker,
) async* {
await onStart?.call(call, method, requests);
await for (final chunk
in super.intercept(call, method, requests, invoker)) {
await for (final chunk in super.intercept(
call,
method,
requests,
invoker,
)) {
await onData?.call(call, method, requests, chunk);
yield chunk;
}
@ -125,8 +167,12 @@ class TestServerInterruptingInterceptor extends ServerInterceptor {
TestServerInterruptingInterceptor({required this.transform});
@override
Stream<R> intercept<Q, R>(ServiceCall call, ServiceMethod<Q, R> method,
Stream<Q> requests, ServerStreamingInvoker<Q, R> invoker) async* {
Stream<R> intercept<Q, R>(
ServiceCall call,
ServiceMethod<Q, R> method,
Stream<Q> requests,
ServerStreamingInvoker<Q, R> invoker,
) async* {
yield* super.intercept(call, method, requests, invoker).map(transform);
}
}
@ -162,24 +208,32 @@ class TestServerStream extends ServerTransportStream {
class ServerHarness extends _Harness {
@override
ConnectionServer createServer() => Server.create(
services: <Service>[service],
interceptors: <Interceptor>[interceptor.call],
serverInterceptors: serverInterceptors..insert(0, serverInterceptor),
);
services: <Service>[service],
interceptors: <Interceptor>[interceptor.call],
serverInterceptors: serverInterceptors..insert(0, serverInterceptor),
);
static ServiceMethod<int, int> createMethod(String name,
Function methodHandler, bool clientStreaming, bool serverStreaming) {
return ServiceMethod<int, int>(name, methodHandler, clientStreaming,
serverStreaming, mockDecode, mockEncode);
static ServiceMethod<int, int> createMethod(
String name,
Function methodHandler,
bool clientStreaming,
bool serverStreaming,
) {
return ServiceMethod<int, int>(
name,
methodHandler,
clientStreaming,
serverStreaming,
mockDecode,
mockEncode,
);
}
}
class ConnectionServerHarness extends _Harness {
@override
ConnectionServer createServer() => ConnectionServer(
<Service>[service],
<Interceptor>[interceptor.call],
);
ConnectionServer createServer() =>
ConnectionServer(<Service>[service], <Interceptor>[interceptor.call]);
static ServiceMethod<int, int> createMethod(
String name,
@ -230,9 +284,10 @@ abstract class _Harness {
}
fromServer.stream.listen(
expectAsync1(handleMessages, count: handlers.length),
onError: expectAsync1((dynamic _) {}, count: 0),
onDone: expectAsync0(() {}, count: 1));
expectAsync1(handleMessages, count: handlers.length),
onError: expectAsync1((dynamic _) {}, count: 0),
onDone: expectAsync0(() {}, count: 1),
);
}
void expectErrorResponse(int status, String message) {
@ -242,17 +297,25 @@ abstract class _Harness {
void expectTrailingErrorResponse(int status, String message) {
setupTest([
headerValidator(),
errorTrailerValidator(status, message, validateHeader: false)
errorTrailerValidator(status, message, validateHeader: false),
]);
}
void sendRequestHeader(String path,
{String authority = 'test',
Map<String, String>? metadata,
Duration? timeout}) {
void sendRequestHeader(
String path, {
String authority = 'test',
Map<String, String>? metadata,
Duration? timeout,
}) {
final headers = Http2ClientConnection.createCallHeaders(
true, authority, path, timeout, metadata, null,
userAgent: 'dart-grpc/1.0.0 test');
true,
authority,
path,
timeout,
metadata,
null,
userAgent: 'dart-grpc/1.0.0 test',
);
toServer.add(HeadersStreamMessage(headers));
}

View File

@ -26,14 +26,17 @@ List<int> mockEncode(int value) => List.filled(value, 0);
int mockDecode(List<int> value) => value.length;
Map<String, String> headersToMap(List<Header> headers) =>
{for (var h in headers) ascii.decode(h.name): ascii.decode(h.value)};
Map<String, String> headersToMap(List<Header> headers) => {
for (var h in headers) ascii.decode(h.name): ascii.decode(h.value),
};
void validateRequestHeaders(Map<String, String> headers,
{String? path,
String authority = 'test',
String? timeout,
Map<String, String>? customHeaders}) {
void validateRequestHeaders(
Map<String, String> headers, {
String? path,
String authority = 'test',
String? timeout,
Map<String, String>? customHeaders,
}) {
expect(headers[':method'], 'POST');
expect(headers[':scheme'], 'https');
if (path != null) {
@ -50,10 +53,12 @@ void validateRequestHeaders(Map<String, String> headers,
});
}
void validateResponseHeaders(Map<String, String> headers,
{int status = 200,
bool allowTrailers = false,
Map<String, String>? customHeaders}) {
void validateResponseHeaders(
Map<String, String> headers, {
int status = 200,
bool allowTrailers = false,
Map<String, String>? customHeaders,
}) {
expect(headers[':status'], '200');
expect(headers['content-type'], startsWith('application/grpc'));
if (!allowTrailers) {
@ -65,8 +70,12 @@ void validateResponseHeaders(Map<String, String> headers,
});
}
void validateResponseTrailers(Map<String, String> trailers,
{int status = 0, String? message, Map<String, String>? customTrailers}) {
void validateResponseTrailers(
Map<String, String> trailers, {
int status = 0,
String? message,
Map<String, String>? customTrailers,
}) {
expect(trailers['grpc-status'], '$status');
if (message != null) {
expect(trailers['grpc-message'], message);
@ -76,8 +85,10 @@ void validateResponseTrailers(Map<String, String> trailers,
});
}
GrpcMetadata validateMetadataMessage(StreamMessage message,
{bool endStream = false}) {
GrpcMetadata validateMetadataMessage(
StreamMessage message, {
bool endStream = false,
}) {
expect(message, TypeMatcher<HeadersStreamMessage>());
expect(message.endStream, endStream);
@ -103,14 +114,19 @@ void Function(StreamMessage message) headerValidator() {
}
void Function(StreamMessage message) errorTrailerValidator(
int status, String statusMessage,
{bool validateHeader = false}) {
int status,
String statusMessage, {
bool validateHeader = false,
}) {
return (StreamMessage message) {
final trailer = validateMetadataMessage(message, endStream: true);
if (validateHeader) {
validateResponseHeaders(trailer.metadata, allowTrailers: true);
}
validateResponseTrailers(trailer.metadata,
status: status, message: statusMessage);
validateResponseTrailers(
trailer.metadata,
status: status,
message: statusMessage,
);
};
}

View File

@ -37,8 +37,26 @@ void main() {
..add(DataStreamMessage([0, 0, 10, 48, 49]))
..add(DataStreamMessage([50, 51, 52, 53]))
..add(DataStreamMessage([54, 55, 56, 57, 0, 0, 0]))
..add(DataStreamMessage(
[0, 4, 97, 98, 99, 100, 0, 0, 0, 0, 1, 65, 0, 0, 0, 0]))
..add(
DataStreamMessage([
0,
4,
97,
98,
99,
100,
0,
0,
0,
0,
1,
65,
0,
0,
0,
0,
]),
)
..add(DataStreamMessage([4, 48, 49, 50, 51, 1, 0, 0, 1, 0]))
..add(DataStreamMessage(List.filled(256, 90)));
input.close();
@ -50,29 +68,41 @@ void main() {
}
expect(converted[0], TypeMatcher<GrpcMetadata>());
verify(
converted[1] as GrpcData, [48, 49, 50, 51, 52, 53, 54, 55, 56, 57]);
verify(converted[1] as GrpcData, [
48,
49,
50,
51,
52,
53,
54,
55,
56,
57,
]);
verify(converted[2] as GrpcData, [97, 98, 99, 100]);
verify(converted[3] as GrpcData, [65]);
verify(converted[4] as GrpcData, [48, 49, 50, 51]);
verify(converted[5] as GrpcData, List.filled(256, 90));
});
test('throws error if input is closed while receiving data header',
() async {
final result = output.toList();
input
..add(HeadersStreamMessage([]))
..add(DataStreamMessage([0, 0, 0]))
..close();
try {
await result;
fail('Did not throw');
} on GrpcError catch (e) {
expect(e.code, StatusCode.unavailable);
expect(e.message, 'Closed in non-idle state');
}
});
test(
'throws error if input is closed while receiving data header',
() async {
final result = output.toList();
input
..add(HeadersStreamMessage([]))
..add(DataStreamMessage([0, 0, 0]))
..close();
try {
await result;
fail('Did not throw');
} on GrpcError catch (e) {
expect(e.code, StatusCode.unavailable);
expect(e.message, 'Closed in non-idle state');
}
},
);
test('throws error if input is closed while receiving data', () async {
final result = output.toList();
@ -89,22 +119,24 @@ void main() {
}
});
test('throws error if receiving metadata while reading data header',
() async {
final result = output.toList();
input
..add(HeadersStreamMessage([]))
..add(DataStreamMessage([0, 0, 0, 0]))
..add(HeadersStreamMessage([]))
..close();
try {
await result;
fail('Did not throw');
} on GrpcError catch (e) {
expect(e.code, StatusCode.unimplemented);
expect(e.message, 'Received header while reading data');
}
});
test(
'throws error if receiving metadata while reading data header',
() async {
final result = output.toList();
input
..add(HeadersStreamMessage([]))
..add(DataStreamMessage([0, 0, 0, 0]))
..add(HeadersStreamMessage([]))
..close();
try {
await result;
fail('Did not throw');
} on GrpcError catch (e) {
expect(e.code, StatusCode.unimplemented);
expect(e.message, 'Received header while reading data');
}
},
);
test('throws error if receiving metadata while reading data', () async {
final result = output.toList();

View File

@ -15,7 +15,8 @@
@TestOn('vm')
@Skip(
'Run only as `dart run --enable-vm-service --timeline-streams=Dart test/timeline_test.dart`')
'Run only as `dart run --enable-vm-service --timeline-streams=Dart test/timeline_test.dart`',
)
library;
import 'dart:async';
@ -33,12 +34,18 @@ const String path = '/test.TestService/stream';
class TestClient extends Client {
static final _$stream = ClientMethod<int, int>(
path, (int value) => [value], (List<int> value) => value[0]);
path,
(int value) => [value],
(List<int> value) => value[0],
);
TestClient(super.channel);
ResponseStream<int> stream(int request, {CallOptions? options}) {
return $createStreamingCall(_$stream, Stream.fromIterable([request]),
options: options);
return $createStreamingCall(
_$stream,
Stream.fromIterable([request]),
options: options,
);
}
}
@ -47,8 +54,16 @@ class TestService extends Service {
String get $name => 'test.TestService';
TestService() {
$addMethod(ServiceMethod<int, int>('stream', stream, false, true,
(List<int> value) => value[0], (int value) => [value]));
$addMethod(
ServiceMethod<int, int>(
'stream',
stream,
false,
true,
(List<int> value) => value[0],
(int value) => [value],
),
);
}
Stream<int> stream(ServiceCall call, Future request) async* {
@ -75,11 +90,13 @@ Future<VmService> testee() async {
final vmService = await vmServiceConnectUri(uri.toString());
final server = Server.create(services: [TestService()]);
await server.serve(address: 'localhost', port: 0);
final channel = FixedConnectionClientChannel(Http2ClientConnection(
'localhost',
server.port!,
ChannelOptions(credentials: ChannelCredentials.insecure()),
));
final channel = FixedConnectionClientChannel(
Http2ClientConnection(
'localhost',
server.port!,
ChannelOptions(credentials: ChannelCredentials.insecure()),
),
);
final testClient = TestClient(channel);
await testClient.stream(1).toList();
await server.shutdown();

View File

@ -89,8 +89,10 @@ void main() {
final timeout = Duration(microseconds: 1);
await harness.runFailureTest(
clientCall: harness.client
.unary(dummyValue, options: CallOptions(timeout: timeout)),
clientCall: harness.client.unary(
dummyValue,
options: CallOptions(timeout: timeout),
),
expectedException: GrpcError.deadlineExceeded('Deadline exceeded'),
expectedPath: '/Test/Unary',
expectedTimeout: timeout,

View File

@ -23,8 +23,9 @@ Future<void> main(List<String> args) async {
final serverPort = 0;
final proxyPort = int.tryParse(args.first);
final proxy =
proxyPort != null ? Proxy(host: 'localhost', port: proxyPort) : null;
final proxy = proxyPort != null
? Proxy(host: 'localhost', port: proxyPort)
: null;
final port = proxyPort ?? serverPort;
@ -34,24 +35,24 @@ Future<void> main(List<String> args) async {
ChannelOptions(proxy: proxy),
);
await connector.initSocket('localhost', port);
final incoming =
proxy == null ? connector.socket : await connector.connectToProxy(proxy);
final incoming = proxy == null
? connector.socket
: await connector.connectToProxy(proxy);
final uri = Uri.parse('http://localhost:0');
final transport =
ClientTransportConnection.viaStreams(incoming, connector.socket);
final request = transport.makeRequest(
[
Header.ascii(':method', 'GET'),
Header.ascii(':path', uri.path),
Header.ascii(':scheme', uri.scheme),
Header.ascii(':authority', uri.host),
],
endStream: true,
final transport = ClientTransportConnection.viaStreams(
incoming,
connector.socket,
);
final request = transport.makeRequest([
Header.ascii(':method', 'GET'),
Header.ascii(':path', uri.path),
Header.ascii(':scheme', uri.scheme),
Header.ascii(':authority', uri.host),
], endStream: true);
await for (var message in request.incomingMessages) {
if (message is HeadersStreamMessage) {
for (var header in message.headers) {