Add custom trailers to GrpcError (#493)

This commit is contained in:
Kenneth Gulbrandsøy 2021-08-10 11:53:09 +02:00 committed by GitHub
parent 4775078b8c
commit acd2e93a25
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 113 additions and 34 deletions

View File

@ -339,7 +339,7 @@ class ClientCall<Q, R> implements Response {
/// If there's an error status then process it as a response error.
void _checkForErrorStatus(Map<String, String> trailers) {
final error = grpcErrorFromTrailers(trailers);
final error = grpcErrorDetailsFromTrailers(trailers);
if (error != null) {
_responseError(error);
}

View File

@ -155,32 +155,38 @@ class GrpcError implements Exception {
final int code;
final String? message;
final Object? rawResponse;
final Map<String, String>? trailers;
final List<GeneratedMessage>? details;
/// Custom error code.
GrpcError.custom(this.code, [this.message, this.details, this.rawResponse]);
GrpcError.custom(this.code,
[this.message, this.details, this.rawResponse, this.trailers = const {}]);
/// The operation completed successfully.
GrpcError.ok([this.message, this.details, this.rawResponse])
: code = StatusCode.ok;
: trailers = const {},
code = StatusCode.ok;
/// The operation was cancelled (typically by the caller).
GrpcError.cancelled([this.message, this.details, this.rawResponse])
: 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.
GrpcError.unknown([this.message, this.details, this.rawResponse])
: 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).
GrpcError.invalidArgument([this.message, this.details, this.rawResponse])
: code = StatusCode.invalidArgument;
: 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
@ -188,16 +194,19 @@ class GrpcError implements Exception {
/// from a server could have been delayed long enough for the deadline to
/// expire.
GrpcError.deadlineExceeded([this.message, this.details, this.rawResponse])
: code = StatusCode.deadlineExceeded;
: trailers = const {},
code = StatusCode.deadlineExceeded;
/// Some requested entity (e.g., file or directory) was not found.
GrpcError.notFound([this.message, this.details, this.rawResponse])
: code = StatusCode.notFound;
: trailers = const {},
code = StatusCode.notFound;
/// Some entity that we attempted to create (e.g., file or directory) already
/// exists.
GrpcError.alreadyExists([this.message, this.details, this.rawResponse])
: 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
@ -205,12 +214,14 @@ class GrpcError implements Exception {
/// [permissionDenied] must not be used if the caller cannot be identified
/// (use [unauthenticated] instead for those errors).
GrpcError.permissionDenied([this.message, this.details, this.rawResponse])
: code = StatusCode.permissionDenied;
: 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.
GrpcError.resourceExhausted([this.message, this.details, this.rawResponse])
: code = StatusCode.resourceExhausted;
: 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
@ -227,7 +238,8 @@ class GrpcError implements Exception {
/// returned since the client should not retry unless they have first
/// fixed up the directory by deleting files from it.
GrpcError.failedPrecondition([this.message, this.details, this.rawResponse])
: code = StatusCode.failedPrecondition;
: trailers = const {},
code = StatusCode.failedPrecondition;
/// The operation was aborted, typically due to a concurrency issue like
/// sequencer check failures, transaction aborts, etc.
@ -235,7 +247,8 @@ class GrpcError implements Exception {
/// See litmus test above for deciding between [failedPrecondition],
/// [aborted], and [unavailable].
GrpcError.aborted([this.message, this.details, this.rawResponse])
: code = StatusCode.aborted;
: trailers = const {},
code = StatusCode.aborted;
/// Operation was attempted past the valid range. E.g., seeking or reading
/// past end of file.
@ -251,16 +264,19 @@ 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.
GrpcError.outOfRange([this.message, this.details, this.rawResponse])
: code = StatusCode.outOfRange;
: trailers = const {},
code = StatusCode.outOfRange;
/// Operation is not implemented or not supported/enabled in this service.
GrpcError.unimplemented([this.message, this.details, this.rawResponse])
: 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].
GrpcError.internal([this.message, this.details, this.rawResponse])
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
@ -269,16 +285,19 @@ class GrpcError implements Exception {
/// See litmus test above for deciding between [failedPrecondition],
/// [aborted], and [unavailable].
GrpcError.unavailable([this.message, this.details, this.rawResponse])
: code = StatusCode.unavailable;
: trailers = const {},
code = StatusCode.unavailable;
/// Unrecoverable data loss or corruption.
GrpcError.dataLoss([this.message, this.details, this.rawResponse])
: code = StatusCode.dataLoss;
: trailers = const {},
code = StatusCode.dataLoss;
/// The request does not have valid authentication credentials for the
/// operation.
GrpcError.unauthenticated([this.message, this.details, this.rawResponse])
: code = StatusCode.unauthenticated;
: trailers = const {},
code = StatusCode.unauthenticated;
/// Given a status code, return the name
String get codeName => (Code.valueOf(code) ?? Code.UNKNOWN).name;
@ -294,7 +313,8 @@ class GrpcError implements Exception {
@override
String toString() =>
'gRPC Error (code: $code, codeName: $codeName, message: $message, details: $details, rawResponse: $rawResponse)';
'gRPC Error (code: $code, codeName: $codeName, message: $message, '
'details: $details, rawResponse: $rawResponse, trailers: $trailers)';
}
/// Parse error details `Any` object into the right kind of `GeneratedMessage`.
@ -375,14 +395,16 @@ void validateHttpStatusAndContentType(
// and use this information to report a better error to the application
// layer. However prefer to use status code derived from HTTP status
// if grpc-status itself does not provide an informative error.
final error = grpcErrorFromTrailers(headers);
final error = grpcErrorDetailsFromTrailers(headers);
if (error == null || error.code == StatusCode.unknown) {
throw GrpcError.custom(
status,
error?.message ??
'HTTP connection completed with ${httpStatus} instead of 200',
error?.details,
rawResponse);
rawResponse,
error?.trailers ?? toCustomTrailers(headers),
);
}
throw error;
}
@ -399,7 +421,7 @@ void validateHttpStatusAndContentType(
}
}
GrpcError? grpcErrorFromTrailers(Map<String, String> trailers) {
GrpcError? grpcErrorDetailsFromTrailers(Map<String, String> trailers) {
final status = trailers['grpc-status'];
final statusCode = status != null ? int.parse(status) : StatusCode.unknown;
@ -411,12 +433,23 @@ GrpcError? grpcErrorFromTrailers(Map<String, String> trailers) {
message,
statusDetails == null
? const <GeneratedMessage>[]
: decodeStatusDetails(statusDetails));
: decodeStatusDetails(statusDetails),
null,
toCustomTrailers(trailers),
);
}
return null;
}
Map<String, String> toCustomTrailers(Map<String, String> trailers) {
return Map.from(trailers)
..remove(':status')
..remove('content-type')
..remove('grpc-status')
..remove('grpc-message');
}
const _statusDetailsHeader = 'grpc-status-details-bin';
/// All accepted content-type header's prefix. We are being more permissive

View File

@ -604,4 +604,34 @@ void main() {
serverHandlers: [handleRequest],
);
});
test('Call should throw with custom trailers', () async {
final code = StatusCode.invalidArgument;
final message = 'some custom message';
final customKey = 'some-custom-key';
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.close();
}
await harness.runFailureTest(
clientCall: harness.client.unary(dummyValue),
expectedException: GrpcError.custom(
code,
message,
[],
customTrailers,
),
expectedCustomTrailers: customTrailers,
serverHandlers: [handleRequest],
);
});
}

View File

@ -307,13 +307,24 @@ abstract class _Harness {
await clientSubscription.cancel();
}
Future<void> expectThrows(Future? future, dynamic exception) async {
Future<void> expectThrows(
Future? future,
dynamic exception, {
Map<String, String>? expectedCustomTrailers,
}) async {
try {
await future;
fail('Did not throw');
} catch (e, st) {
expect(e, exception);
expect(st, isNot(equals(StackTrace.current)));
if (expectedCustomTrailers != null) {
if (e is GrpcError) {
expect(e.trailers, expectedCustomTrailers);
} else {
fail('$e is not a GrpcError');
}
}
}
}
@ -323,10 +334,15 @@ abstract class _Harness {
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, expectedException),
clientCall: expectThrows(
clientCall,
expectedException,
expectedCustomTrailers: expectedCustomTrailers,
),
expectedPath: expectedPath,
expectedTimeout: expectedTimeout,
expectedCustomHeaders: expectedCustomHeaders,