From ed5e8fb43e67ae92f52e3602b0c0c1519fb2ba99 Mon Sep 17 00:00:00 2001 From: Jakob Andersen Date: Mon, 10 Jul 2017 12:53:08 +0200 Subject: [PATCH] Use correct gRPC error codes. (#23) Copied error code definitions from source grpc/grpc repo. Fixes #7. --- lib/src/client.dart | 29 +++--- lib/src/server.dart | 49 ++++----- lib/src/status.dart | 224 ++++++++++++++++++++++++++++++++++++++++++ lib/src/streams.dart | 26 ++--- test/stream_test.dart | 18 ++-- 5 files changed, 286 insertions(+), 60 deletions(-) create mode 100644 lib/src/status.dart diff --git a/lib/src/client.dart b/lib/src/client.dart index 56ebd55..4a1bed6 100644 --- a/lib/src/client.dart +++ b/lib/src/client.dart @@ -10,6 +10,7 @@ import 'dart:io'; import 'package:http2/transport.dart'; import 'shared.dart'; +import 'status.dart'; import 'streams.dart'; const _reservedHeaders = const [ @@ -112,7 +113,7 @@ class ClientCall implements Response { _responses = new StreamController(onListen: _onResponseListen); _callSetup = _initiateCall().catchError((error) { _responses.addError( - new GrpcError(1703, 'Error connecting: ${error.toString()}')); + new GrpcError.unavailable('Error connecting: ${error.toString()}')); }); } @@ -213,11 +214,13 @@ class ClientCall implements Response { void _onResponseData(GrpcMessage data) { if (data is GrpcData) { if (!_headers.isCompleted) { - _responseError(new GrpcError(1217, 'Received data before headers')); + _responseError( + new GrpcError.unimplemented('Received data before headers')); return; } if (_trailers.isCompleted) { - _responseError(new GrpcError(1218, 'Received data after trailers')); + _responseError( + new GrpcError.unimplemented('Received data after trailers')); return; } _responses.add(_method.responseDeserializer(data.data)); @@ -230,7 +233,8 @@ class ClientCall implements Response { return; } if (_trailers.isCompleted) { - _responseError(new GrpcError(1219, 'Received multiple trailers')); + _responseError( + new GrpcError.unimplemented('Received multiple trailers')); return; } final metadata = data.metadata; @@ -240,11 +244,11 @@ class ClientCall implements Response { final status = int.parse(metadata['grpc-status']); final message = metadata['grpc-message']; if (status != 0) { - _responseError(new GrpcError(status, message, metadata)); + _responseError(new GrpcError.custom(status, message)); } } } else { - _responseError(new GrpcError(1220, 'Unexpected frame received')); + _responseError(new GrpcError.unimplemented('Unexpected frame received')); } } @@ -255,32 +259,33 @@ class ClientCall implements Response { _responseError(error); return; } - _responseError(new GrpcError(1221, error.toString())); + _responseError(new GrpcError.unknown(error.toString())); } /// Handles closure of the response stream. Verifies that server has sent /// response messages and header/trailer metadata, as necessary. void _onResponseDone() { if (!_headers.isCompleted) { - _responseError(new GrpcError(1223, 'Did not receive anything')); + _responseError(new GrpcError.unavailable('Did not receive anything')); return; } if (!_trailers.isCompleted) { if (_hasReceivedResponses) { // Trailers are required after receiving data. - _responseError(new GrpcError(1222, 'Missing trailers')); + _responseError(new GrpcError.unavailable('Missing trailers')); return; } // Only received a header frame and no data frames, so the header // should contain "trailers" as well (Trailers-Only). + _trailers.complete(_headerMetadata); final status = _headerMetadata['grpc-status']; + // If status code is missing, we must treat it as '0'. As in 'success'. final statusCode = status != null ? int.parse(status) : 0; if (statusCode != 0) { final message = _headerMetadata['grpc-message']; - _responseError(new GrpcError(statusCode, message, _headerMetadata)); + _responseError(new GrpcError.custom(statusCode, message)); } - // If status code is missing, we must treat it as '0'. As in 'success'. } _responses.close(); _responseSubscription.cancel(); @@ -291,7 +296,7 @@ class ClientCall implements Response { /// error to the user code on the [_responses] stream. void _onRequestError(error) { if (error is! GrpcError) { - error = new GrpcError(1217, error.toString()); + error = new GrpcError.unknown(error.toString()); } _responses.addError(error); diff --git a/lib/src/server.dart b/lib/src/server.dart index 8fcfdb6..c6a3ed8 100644 --- a/lib/src/server.dart +++ b/lib/src/server.dart @@ -7,6 +7,7 @@ import 'dart:io'; import 'package:http2/transport.dart'; +import 'status.dart'; import 'streams.dart'; /// Definition of a gRPC service method. @@ -182,7 +183,7 @@ class ServerHandler { void _onDataIdle(GrpcMessage message) { if (message is! GrpcMetadata) { - _sendError(401, 'Expected header frame'); + _sendError(new GrpcError.unimplemented('Expected header frame')); return; } final headerMessage = message @@ -190,14 +191,15 @@ class ServerHandler { _clientMetadata = headerMessage.metadata; final path = _clientMetadata[':path'].split('/'); if (path.length < 3) { - _sendError(404, 'Invalid path'); + _sendError(new GrpcError.unimplemented('Invalid path')); return; } final service = path[1]; final method = path[2]; _descriptor = _methodLookup(service, method); if (_descriptor == null) { - _sendError(404, 'Path /$service/$method not found'); + _sendError( + new GrpcError.unimplemented('Path /$service/$method not found')); return; } _startStreamingRequest(); @@ -239,29 +241,32 @@ class ServerHandler { void _onDataActive(GrpcMessage message) { if (message is! GrpcData) { - _sendError(711, 'Expected data frame'); + _sendError(new GrpcError.unimplemented('Expected data frame')); _requests - ..addError(new GrpcError(712, 'No request received')) + ..addError(new GrpcError.unimplemented('No request received')) ..close(); return; } if (_hasReceivedRequest && !_descriptor.streamingRequest) { - _sendError(712, 'Too many requests'); + final error = new GrpcError.unimplemented('Too many requests'); + _sendError(error); _requests - ..addError(new GrpcError(712, 'Too many requests')) + ..addError(error) ..close(); } - final data = - message as GrpcData; // TODO(jakobr): Cast should not be necessary here. + // TODO(jakobr): Cast should not be necessary here. + final data = message as GrpcData; var request; try { request = _descriptor.requestDeserializer(data.data); } catch (error) { - _sendError(730, 'Error deserializing request: $error'); + final grpcError = + new GrpcError.internal('Error deserializing request: $error'); + _sendError(grpcError); _requests - ..addError(new GrpcError(730, 'Error deserializing request: $error')) + ..addError(grpcError) ..close(); return; } @@ -283,7 +288,7 @@ class ServerHandler { if (!_requests.isClosed) { // If we can, alert the handler that things are going wrong. _requests - .addError(new GrpcError(1001, 'Error sending response: $error')); + .addError(new GrpcError.internal('Error sending response: $error')); _requests.close(); } _incomingSubscription.cancel(); @@ -297,15 +302,14 @@ class ServerHandler { void _onResponseError(error) { if (error is GrpcError) { - // TODO(jakobr): error.metadata... - _sendError(error.code, error.message); + _sendError(error); } else { - _sendError(107, error.toString()); + _sendError(new GrpcError.unknown(error.toString())); } } void _sendHeaders() { - if (_headersSent) throw new GrpcError(1514, 'Headers already sent'); + if (_headersSent) throw new GrpcError.internal('Headers already sent'); final headersMap = {}; headersMap.addAll(_customHeaders); _customHeaders = null; @@ -354,24 +358,25 @@ class ServerHandler { // Exception from the incoming stream. Most likely a cancel request from the // client, so we treat it as such. _isCanceled = true; - _requests.addError(new GrpcError(1001, 'Canceled')); + _requests.addError(new GrpcError.cancelled('Cancelled')); _responseSubscription?.cancel(); } void _onDoneError() { - _sendError(710, 'Request stream closed unexpectedly'); + _sendError(new GrpcError.unavailable('Request stream closed unexpectedly')); } void _onDoneExpected() { if (!(_hasReceivedRequest || _descriptor.streamingRequest)) { - _sendError(730, 'Expected request message'); - _requests.addError(new GrpcError(730, 'No request message received')); + final error = new GrpcError.unimplemented('Expected request message'); + _sendError(error); + _requests.addError(error); } _requests.close(); _incomingSubscription.cancel(); } - void _sendError(int status, String message) { - _sendTrailers(status: status, message: message); + void _sendError(GrpcError error) { + _sendTrailers(status: error.code, message: error.message); } } diff --git a/lib/src/status.dart b/lib/src/status.dart new file mode 100644 index 0000000..4d28431 --- /dev/null +++ b/lib/src/status.dart @@ -0,0 +1,224 @@ +// Copyright (c) 2017, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +class StatusCode { + /// The operation completed successfully. + static final ok = 0; + + /// The operation was cancelled (typically by the caller). + static final cancelled = 1; + + /// 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. + static final unknown = 2; + + /// 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). + static final invalidArgument = 3; + + /// 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. + static final deadlineExceeded = 4; + + /// Some requested entity (e.g., file or directory) was not found. + static final notFound = 5; + + /// Some entity that we attempted to create (e.g., file or directory) already + /// exists. + static final alreadyExists = 6; + + /// 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). + static final permissionDenied = 7; + + /// Some resource has been exhausted, perhaps a per-user quota, or perhaps the + /// entire file system is out of space. + static final resourceExhausted = 8; + + /// 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 + /// non-empty, an rmdir operation is applied to a non-directory, etc. + /// + /// A litmus test that may help a service implementor in deciding between + /// [failedPrecondition], [aborted], and [unavailable]: + /// (a) Use [unavailable] if the client can retry just the failing call. + /// (b) Use [aborted] if the client should retry at a higher-level (e.g., + /// restarting a read-modify-write sequence). + /// (c) Use [failedPrecondition] if the client should not retry until the + /// system state has been explicitly fixed. E.g., if an "rmdir" fails + /// 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. + static final failedPrecondition = 9; + + /// The operation was aborted, typically due to a concurrency issue like + /// sequencer check failures, transaction aborts, etc. + /// + /// See litmus test above for deciding between [failedPrecondition], + /// [aborted], and [unavailable]. + static final aborted = 10; + + /// Operation was attempted past the valid range. E.g., seeking or reading + /// past end of file. + /// + /// Unlike invalidArgument, this error indicates a problem that may be fixed + /// if the system state changes. For example, a 32-bit file system will + /// generate invalidArgument if asked to read at an offset that is not in the + /// range [0,2^32-1], but it will generate [outOfRange] if asked to read from + /// an offset past the current file size. + /// + /// There is a fair bit of overlap between [failedPrecondition] and + /// [outOfRange]. We recommend using [outOfRange] (the more specific error) + /// 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. + static final outOfRange = 11; + + /// Operation is not implemented or not supported/enabled in this service. + static final unimplemented = 12; + + /// Internal errors. Means some invariants expected by underlying system has + /// been broken. If you see one of these errors, something is very broken. + static final internal = 13; + + /// The service is currently unavailable. This is a most likely a transient + /// condition and may be corrected by retrying with a backoff. + /// + /// See litmus test above for deciding between [failedPrecondition], + /// [aborted], and [unavailable]. + static final unavailable = 14; + + /// Unrecoverable data loss or corruption. + static final dataLoss = 15; + + /// The request does not have valid authentication credentials for the + /// operation. + static final unauthenticated = 16; +} + +class GrpcError { + final int code; + final String message; + + /// Custom error code. + GrpcError.custom(this.code, [this.message]); + + /// The operation completed successfully. + GrpcError.ok([this.message]) : code = StatusCode.ok; + + /// The operation was cancelled (typically by the caller). + GrpcError.cancelled([this.message]) : 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]) : 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]) : 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. + GrpcError.deadlineExceeded([this.message]) + : code = StatusCode.deadlineExceeded; + + /// Some requested entity (e.g., file or directory) was not found. + GrpcError.notFound([this.message]) : code = StatusCode.notFound; + + /// Some entity that we attempted to create (e.g., file or directory) already + /// exists. + GrpcError.alreadyExists([this.message]) : 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). + GrpcError.permissionDenied([this.message]) + : 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]) + : 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 + /// non-empty, an rmdir operation is applied to a non-directory, etc. + /// + /// A litmus test that may help a service implementor in deciding between + /// [failedPrecondition], [aborted], and [unavailable]: + /// (a) Use [unavailable] if the client can retry just the failing call. + /// (b) Use [aborted] if the client should retry at a higher-level (e.g., + /// restarting a read-modify-write sequence). + /// (c) Use [failedPrecondition] if the client should not retry until the + /// system state has been explicitly fixed. E.g., if an "rmdir" fails + /// 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. + GrpcError.failedPrecondition([this.message]) + : code = StatusCode.failedPrecondition; + + /// The operation was aborted, typically due to a concurrency issue like + /// sequencer check failures, transaction aborts, etc. + /// + /// See litmus test above for deciding between [failedPrecondition], + /// [aborted], and [unavailable]. + GrpcError.aborted([this.message]) : code = StatusCode.aborted; + + /// Operation was attempted past the valid range. E.g., seeking or reading + /// past end of file. + /// + /// Unlike invalidArgument, this error indicates a problem that may be fixed + /// if the system state changes. For example, a 32-bit file system will + /// generate invalidArgument if asked to read at an offset that is not in the + /// range [0,2^32-1], but it will generate [outOfRange] if asked to read from + /// an offset past the current file size. + /// + /// There is a fair bit of overlap between [failedPrecondition] and + /// [outOfRange]. We recommend using [outOfRange] (the more specific error) + /// 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]) : code = StatusCode.outOfRange; + + /// Operation is not implemented or not supported/enabled in this service. + GrpcError.unimplemented([this.message]) : 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. + GrpcError.internal([this.message]) : 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. + /// + /// See litmus test above for deciding between [failedPrecondition], + /// [aborted], and [unavailable]. + GrpcError.unavailable([this.message]) : code = StatusCode.unavailable; + + /// Unrecoverable data loss or corruption. + GrpcError.dataLoss([this.message]) : code = StatusCode.dataLoss; + + /// The request does not have valid authentication credentials for the + /// operation. + GrpcError.unauthenticated([this.message]) : code = StatusCode.unauthenticated; + + @override + String toString() => 'gRPC Error ($code, $message)'; +} diff --git a/lib/src/streams.dart b/lib/src/streams.dart index 5da65a1..3d48e83 100644 --- a/lib/src/streams.dart +++ b/lib/src/streams.dart @@ -9,6 +9,8 @@ import 'dart:typed_data'; import 'package:http2/transport.dart'; +import 'status.dart'; + abstract class GrpcMessage {} class GrpcMetadata extends GrpcMessage { @@ -28,16 +30,6 @@ class GrpcData extends GrpcMessage { String toString() => 'gRPC Data (${data.length} bytes)'; } -class GrpcError { - final int code; - final String message; - final Map metadata; - GrpcError(this.code, this.message, [this.metadata = const {}]); - - @override - String toString() => 'gRPC Error ($code, $message, $metadata)'; -} - StreamTransformer grpcDecompressor() => new StreamTransformer.fromHandlers( handleData: (GrpcMessage value, EventSink sink) { @@ -63,7 +55,7 @@ class GrpcHttpEncoder extends Converter { } else if (input is GrpcData) { return new DataStreamMessage(frame(input.data)); } - throw new GrpcError(99, 'Unexpected message type'); + throw new GrpcError.internal('Unexpected message type'); } static List frame(List payload) { @@ -152,8 +144,7 @@ class _GrpcMessageConversionSink extends ChunkedConversionSink { if (_data != null || _dataOffset != 0) { // We were in the middle of receiving data, so receiving a header frame // is a violation of the gRPC protocol. - throw new GrpcError(101, - 'Received header while reading ${_data == null ? 'header' : 'data (${_data.length} bytes)'} at offset $_dataOffset'); + throw new GrpcError.unimplemented('Received header while reading data'); } final headers = {}; for (var header in chunk.headers) { @@ -171,25 +162,26 @@ class _GrpcMessageConversionSink extends ChunkedConversionSink { @override void add(StreamMessage chunk) { if (_trailerReceived) { - throw new GrpcError(102, 'Received data after trailer metadata'); + throw new GrpcError.unimplemented('Received data after trailer metadata'); } if (chunk is DataStreamMessage) { if (!_headerReceived) { - throw new GrpcError(103, 'Received data before header metadata'); + throw new GrpcError.unimplemented( + 'Received data before header metadata'); } _addData(chunk); } else if (chunk is HeadersStreamMessage) { _addHeaders(chunk); } else { // No clue what this is. - throw new GrpcError(104, 'Received unknown HTTP/2 frame type'); + throw new GrpcError.unimplemented('Received unknown HTTP/2 frame type'); } } @override void close() { if (_data != null || _dataOffset != 0) { - throw new GrpcError(105, 'Closed in non-idle state'); + throw new GrpcError.unavailable('Closed in non-idle state'); } _out.close(); } diff --git a/test/stream_test.dart b/test/stream_test.dart index 11a792b..991a9b3 100644 --- a/test/stream_test.dart +++ b/test/stream_test.dart @@ -8,6 +8,7 @@ import 'package:http2/transport.dart'; import 'package:test/test.dart'; import 'package:grpc/src/streams.dart'; +import 'package:grpc/src/status.dart'; void main() { group('GrpcHttpDecoder', () { @@ -26,7 +27,7 @@ void main() { await output.toList(); fail('Did not throw exception'); } on GrpcError catch (e) { - expect(e.code, 103); + expect(e.code, StatusCode.unimplemented); expect(e.message, 'Received data before header metadata'); } }); @@ -69,7 +70,7 @@ void main() { await result; fail('Did not throw'); } on GrpcError catch (e) { - expect(e.code, 102); + expect(e.code, StatusCode.unimplemented); expect(e.message, 'Received data after trailer metadata'); } }); @@ -85,7 +86,7 @@ void main() { await result; fail('Did not throw'); } on GrpcError catch (e) { - expect(e.code, 105); + expect(e.code, StatusCode.unavailable); expect(e.message, 'Closed in non-idle state'); } }); @@ -100,7 +101,7 @@ void main() { await result; fail('Did not throw'); } on GrpcError catch (e) { - expect(e.code, 105); + expect(e.code, StatusCode.unavailable); expect(e.message, 'Closed in non-idle state'); } }); @@ -117,8 +118,8 @@ void main() { await result; fail('Did not throw'); } on GrpcError catch (e) { - expect(e.code, 101); - expect(e.message, 'Received header while reading header at offset 4'); + expect(e.code, StatusCode.unimplemented); + expect(e.message, 'Received header while reading data'); } }); @@ -133,9 +134,8 @@ void main() { await result; fail('Did not throw'); } on GrpcError catch (e) { - expect(e.code, 101); - expect(e.message, - 'Received header while reading data (2 bytes) at offset 1'); + expect(e.code, StatusCode.unimplemented); + expect(e.message, 'Received header while reading data'); } });