Use correct gRPC error codes. (#23)

Copied error code definitions from source grpc/grpc repo.

Fixes #7.
This commit is contained in:
Jakob Andersen 2017-07-10 12:53:08 +02:00 committed by GitHub
parent a5e740c41a
commit ed5e8fb43e
5 changed files with 286 additions and 60 deletions

View File

@ -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<Q, R> 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<Q, R> 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<Q, R> 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<Q, R> 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<Q, R> 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<Q, R> 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);

View File

@ -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 = <String, String>{};
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);
}
}

224
lib/src/status.dart Normal file
View File

@ -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)';
}

View File

@ -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<String, String> metadata;
GrpcError(this.code, this.message, [this.metadata = const {}]);
@override
String toString() => 'gRPC Error ($code, $message, $metadata)';
}
StreamTransformer<GrpcMessage, GrpcMessage> grpcDecompressor() =>
new StreamTransformer<GrpcMessage, GrpcMessage>.fromHandlers(
handleData: (GrpcMessage value, EventSink<GrpcMessage> sink) {
@ -63,7 +55,7 @@ class GrpcHttpEncoder extends Converter<GrpcMessage, StreamMessage> {
} 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<int> frame(List<int> payload) {
@ -152,8 +144,7 @@ class _GrpcMessageConversionSink extends ChunkedConversionSink<StreamMessage> {
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 = <String, String>{};
for (var header in chunk.headers) {
@ -171,25 +162,26 @@ class _GrpcMessageConversionSink extends ChunkedConversionSink<StreamMessage> {
@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();
}

View File

@ -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');
}
});