mirror of https://github.com/grpc/grpc-dart.git
dartfmt
This commit is contained in:
parent
86f2dac45a
commit
d75a3ef7c4
|
@ -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: |
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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...');
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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}',
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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));
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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}...');
|
||||
|
|
|
@ -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.';
|
||||
|
|
|
@ -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, {
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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));
|
||||
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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.
|
||||
///
|
||||
|
|
|
@ -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, {
|
||||
|
|
|
@ -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');
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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',
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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>[];
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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'),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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],
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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],
|
||||
);
|
||||
|
|
|
@ -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',
|
||||
);
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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, []);
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
|
|
|
@ -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'),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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');
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -86,6 +86,7 @@ class FakeEchoService extends EchoServiceBase {
|
|||
|
||||
@override
|
||||
Stream<ServerStreamingEchoResponse> serverStreamingEcho(
|
||||
ServiceCall call, ServerStreamingEchoRequest request) =>
|
||||
throw UnimplementedError();
|
||||
ServiceCall call,
|
||||
ServerStreamingEchoRequest request,
|
||||
) => throw UnimplementedError();
|
||||
}
|
||||
|
|
|
@ -68,6 +68,7 @@ class FakeEchoService extends EchoServiceBase {
|
|||
|
||||
@override
|
||||
Stream<ServerStreamingEchoResponse> serverStreamingEcho(
|
||||
ServiceCall call, ServerStreamingEchoRequest request) =>
|
||||
throw UnimplementedError();
|
||||
ServiceCall call,
|
||||
ServerStreamingEchoRequest request,
|
||||
) => throw UnimplementedError();
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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',
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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',
|
||||
]),
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
|
||||
|
|
|
@ -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,
|
||||
);
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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) {
|
||||
|
|
Loading…
Reference in New Issue