Propagate grpc backend status to frontend

This commit is contained in:
Stanley Cheung 2017-02-13 11:11:14 -08:00
parent 55cfeac15f
commit cea6f9ae8a
5 changed files with 233 additions and 35 deletions

View File

@ -16,8 +16,6 @@ goog.require('goog.net.XhrIo');
goog.require('goog.net.streams.NodeReadableStream'); goog.require('goog.net.streams.NodeReadableStream');
goog.require('goog.net.streams.createXhrNodeReadableStream'); goog.require('goog.net.streams.createXhrNodeReadableStream');
goog.require('jspb.Message'); goog.require('jspb.Message');
goog.require('proto.google.rpc.Status');
goog.require('proto.grpc.gateway.Pair');
/** /**
@ -28,10 +26,11 @@ goog.require('proto.grpc.gateway.Pair');
* @final * @final
* @param {!goog.net.XhrIo} xhr The XhrIo object * @param {!goog.net.XhrIo} xhr The XhrIo object
* @param {function(?):!jspb.Message} deserializeFunc * @param {function(?):!jspb.Message} deserializeFunc
* @param {function(!string):!Object} parseRpcStatusFunc
* The deserialize function for the proto * The deserialize function for the proto
*/ */
grpc.web.ClientReadableStream = function( grpc.web.ClientReadableStream = function(
xhr, deserializeFunc) { xhr, deserializeFunc, parseRpcStatusFunc) {
/** /**
* @private * @private
* @type {?goog.net.streams.NodeReadableStream} The XHR Node Readable * @type {?goog.net.streams.NodeReadableStream} The XHR Node Readable
@ -57,6 +56,12 @@ grpc.web.ClientReadableStream = function(
* @type {function(!Object)|null} The trailing metadata callback * @type {function(!Object)|null} The trailing metadata callback
*/ */
this.onStatusCallback_ = null; this.onStatusCallback_ = null;
/**
* @private
* @type {function(!string):!Object} A function to parse the Rpc Status response
*/
this.parseRpcStatusFunc_ = parseRpcStatusFunc;
}; };
@ -74,33 +79,13 @@ grpc.web.ClientReadableStream.prototype.on = function(
if (eventType == 'data') { if (eventType == 'data') {
this.xhrNodeReadableStream_.on('data', function(data) { this.xhrNodeReadableStream_.on('data', function(data) {
if ('1' in data) { if ('1' in data) {
var base64_message = data['1']; var response = self.deserializeFunc_(data['1']);
var response = self.deserializeFunc_(base64_message);
callback(response); callback(response);
} }
if ('2' in data) { if ('2' in data && self.onStatusCallback_) {
var base64_message = data['2']; var status = self.parseRpcStatusFunc_(data['2']);
var _status =
proto.google.rpc.Status.deserializeBinary(base64_message);
var status = {};
var metadata = {};
status['code'] = _status.getCode();
status['details'] = _status.getMessage();
var details = _status.getDetailsList();
for (var i = 0; i < details.length; i++) {
var pair = proto.grpc.gateway.Pair.deserializeBinary(
details[i].getValue());
var first = new TextDecoder("utf-8").decode(
/** @type {!ArrayBufferView|undefined} */ (pair.getFirst_asU8()));
var second = new TextDecoder("utf-8").decode(
/** @type {!ArrayBufferView|undefined} */ (pair.getSecond_asU8()));
metadata[first] = second;
}
status['metadata'] = metadata;
if (self.onStatusCallback_) {
self.onStatusCallback_(status); self.onStatusCallback_(status);
} }
}
}); });
} else if (eventType == 'status') { } else if (eventType == 'status') {
this.onStatusCallback_ = callback; this.onStatusCallback_ = callback;
@ -108,6 +93,7 @@ grpc.web.ClientReadableStream.prototype.on = function(
return this; return this;
}; };
/** /**
* Close the stream. * Close the stream.
*/ */

View File

@ -8,9 +8,13 @@
goog.provide('grpc.web.GatewayClientBase'); goog.provide('grpc.web.GatewayClientBase');
goog.require('goog.crypt');
goog.require('goog.crypt.base64'); goog.require('goog.crypt.base64');
goog.require('goog.net.XhrIo'); goog.require('goog.net.XhrIo');
goog.require('grpc.web.ClientReadableStream'); goog.require('grpc.web.ClientReadableStream');
goog.require('grpc.web.Status');
goog.require('proto.google.rpc.Status');
goog.require('proto.grpc.gateway.Pair');
@ -40,7 +44,7 @@ grpc.web.GatewayClientBase.prototype.serialize_ = function(request) {
* @param {!Object<string, string>} metadata User defined call metadata * @param {!Object<string, string>} metadata User defined call metadata
* @param {function(?):!jspb.Message} deserializeFunc * @param {function(?):!jspb.Message} deserializeFunc
* The deserialize function for the proto * The deserialize function for the proto
* @param {function(?string, ?Object=)} callback A callback * @param {function(?Object=, ?Object=)} callback A callback
* function which takes (error, response) * function which takes (error, response)
* @return {!grpc.web.ClientReadableStream|undefined} The Client Readable * @return {!grpc.web.ClientReadableStream|undefined} The Client Readable
* Stream * Stream
@ -55,8 +59,16 @@ grpc.web.GatewayClientBase.prototype.rpcCall = function(
var stream = this.getClientReadableStream_(xhr, deserializeFunc); var stream = this.getClientReadableStream_(xhr, deserializeFunc);
stream.on('data', function(response) { stream.on('data', function(response) {
var err = null; // TODO: propagate error callback(null, response);
callback(err, response); });
stream.on('status', function(status) {
if (status.code != grpc.web.Status.StatusCode.OK) {
callback({
'code': status.code,
'message': status.details
}, null);
}
}); });
xhr.headers.set('Content-Type', 'application/x-protobuf'); xhr.headers.set('Content-Type', 'application/x-protobuf');
@ -110,5 +122,34 @@ grpc.web.GatewayClientBase.prototype.newXhr_ = function() {
*/ */
grpc.web.GatewayClientBase.prototype.getClientReadableStream_ = function( grpc.web.GatewayClientBase.prototype.getClientReadableStream_ = function(
xhr, deserializeFunc) { xhr, deserializeFunc) {
return new grpc.web.ClientReadableStream(xhr, deserializeFunc); return new grpc.web.ClientReadableStream(xhr, deserializeFunc,
grpc.web.GatewayClientBase.parseRpcStatusFunc_);
};
/**
* @private
* @static
* @param {!string} data Data returned from underlying stream
* @return {!Object} status The Rpc Status details
*/
grpc.web.GatewayClientBase.parseRpcStatusFunc_ = function(data) {
var rpcStatus =
proto.google.rpc.Status.deserializeBinary(data);
var status = {};
var metadata = {};
status['code'] = rpcStatus.getCode();
status['details'] = rpcStatus.getMessage();
var details = rpcStatus.getDetailsList();
for (var i = 0; i < details.length; i++) {
var pair = proto.grpc.gateway.Pair.deserializeBinary(
details[i].getValue());
var first = goog.crypt.utf8ByteArrayToString(
pair.getFirst_asU8());
var second = goog.crypt.utf8ByteArrayToString(
pair.getSecond_asU8());
metadata[first] = second;
}
status['metadata'] = metadata;
return status;
}; };

View File

@ -168,7 +168,7 @@ void PrintUnaryCall(Printer* printer, std::map<string, string> vars) {
" * request proto\n" " * request proto\n"
" * @param {!Object<string, string>} metadata User defined\n" " * @param {!Object<string, string>} metadata User defined\n"
" * call metadata\n" " * call metadata\n"
" * @param {function(?string, ?Object=)} callback " " * @param {function(?Object=, ?Object=)} callback "
"The callback\n" "The callback\n"
" * function(error, response)\n" " * function(error, response)\n"
" * @return {!grpc.web.ClientReadableStream|undefined} The XHR Node\n" " * @return {!grpc.web.ClientReadableStream|undefined} The XHR Node\n"

View File

@ -0,0 +1,162 @@
/**
* @fileoverview gRPC Web Status codes and mapping.
*
* gRPC Web Status codes and mapping.
*
* @author stanleycheung@google.com (Stanley Cheung)
*/
goog.provide('grpc.web.Status');
/**
* gRPC Status Codes
* See: https://github.com/grpc/grpc/blob/master/include/grpc%2B%2B/impl/codegen/status_code_enum.h
* @enum {int}
*/
grpc.web.Status.StatusCode = {
// Not an error; returned on success.
OK: 0,
// The operation was cancelled (typically by the caller).
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.
UNKNOWN: 2,
// Client specified an invalid argument. Note that this differs from
// FAILED_PRECONDITION. INVALID_ARGUMENT indicates arguments that are
// problematic regardless of the state of the system (e.g., a malformed file
// name).
INVALID_ARGUMENT: 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.
DEADLINE_EXCEEDED: 4,
// Some requested entity (e.g., file or directory) was not found.
NOT_FOUND: 5,
// Some entity that we attempted to create (e.g., file or directory) already
// exists.
ALREADY_EXISTS: 6,
// The caller does not have permission to execute the specified operation.
// PERMISSION_DENIED must not be used for rejections caused by exhausting
// some resource (use RESOURCE_EXHAUSTED instead for those errors).
// PERMISSION_DENIED must not be used if the caller can not be identified
// (use UNAUTHENTICATED instead for those errors).
PERMISSION_DENIED: 7,
// The request does not have valid authentication credentials for the
// operation.
UNAUTHENTICATED: 16,
// Some resource has been exhausted, perhaps a per-user quota, or perhaps the
// entire file system is out of space.
RESOURCE_EXHAUSTED: 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 FAILED_PRECONDITION, 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 FAILED_PRECONDITION 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, FAILED_PRECONDITION
// should be returned since the client should not retry unless
// they have first fixed up the directory by deleting files from it.
// (d) Use FAILED_PRECONDITION if the client performs conditional
// REST Get/Update/Delete on a resource and the resource on the
// server does not match the condition. E.g., conflicting
// read-modify-write on the same resource.
FAILED_PRECONDITION: 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 FAILED_PRECONDITION, ABORTED,
// and UNAVAILABLE.
ABORTED: 10,
// Operation was attempted past the valid range. E.g., seeking or reading
// past end of file.
//
// Unlike INVALID_ARGUMENT, this error indicates a problem that may be fixed
// if the system state changes. For example, a 32-bit file system will
// generate INVALID_ARGUMENT if asked to read at an offset that is not in the
// range [0,2^32-1], but it will generate OUT_OF_RANGE if asked to read from
// an offset past the current file size.
//
// There is a fair bit of overlap between FAILED_PRECONDITION and
// OUT_OF_RANGE. We recommend using OUT_OF_RANGE (the more specific error)
// when it applies so that callers who are iterating through a space can
// easily look for an OUT_OF_RANGE error to detect when they are done.
OUT_OF_RANGE: 11,
// Operation is not implemented or not supported/enabled in this service.
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.
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 FAILED_PRECONDITION, ABORTED,
// and UNAVAILABLE.
UNAVAILABLE: 14,
// Unrecoverable data loss or corruption.
DATA_LOSS: 15,
};
/**
* Convert HTTP Status code to gRPC Status code
* @param {int} http_status HTTP Status Code
* @return {int} gRPC Status Code
*/
grpc.web.Status.HttpStatusToGrpcStatus = function(http_status) {
switch (http_status) {
case 200:
return grpc.web.Status.StatusCode.OK;
case 400:
return grpc.web.Status.StatusCode.INVALID_ARGUMENT;
case 401:
return grpc.web.Status.StatusCode.UNAUTHENTICATED;
case 403:
return grpc.web.Status.StatusCode.PERMISSION_DENIED;
case 404:
return grpc.web.Status.StatusCode.NOT_FOUND;
case 409:
return grpc.web.Status.StatusCode.ABORTED;
case 412:
return grpc.web.Status.StatusCode.FAILED_PRECONDITION;
case 429:
return grpc.web.Status.StatusCode.RESOURCE_EXHAUSTED;
case 499:
return grpc.web.Status.StatusCode.CANCELLED;
case 500:
return grpc.web.Status.StatusCode.UNKNOWN;
case 501:
return grpc.web.Status.StatusCode.UNIMPLEMENTED;
case 503:
return grpc.web.Status.StatusCode.UNAVAILABLE;
case 504:
return grpc.web.Status.StatusCode.DEADLINE_EXCEEDED;
/* everything else is unknown */
default:
return grpc.web.Status.StatusCode.UNKNOWN;
}
};

View File

@ -32,9 +32,13 @@
var unaryRequest = new proto.grpc.gateway.testing.EchoRequest(); var unaryRequest = new proto.grpc.gateway.testing.EchoRequest();
unaryRequest.setMessage(msg); unaryRequest.setMessage(msg);
echoService.Echo(unaryRequest, {}, function(err, response) { echoService.Echo(unaryRequest, {}, function(err, response) {
if (err) {
addRightMessage('Error code: '+err.code+' "'+err.message+'"');
} else {
setTimeout(function () { setTimeout(function () {
addRightMessage(response.getMessage()); addRightMessage(response.getMessage());
}, INTERVAL); }, INTERVAL);
}
}); });
}; };
@ -51,6 +55,11 @@
stream.on('data', function(response) { stream.on('data', function(response) {
addRightMessage(response.getMessage()); addRightMessage(response.getMessage());
}); });
stream.on('status', function(status) {
if (status.code != grpc.web.Status.StatusCode.OK) {
addRightMessage('Error code: '+status.code+' "'+status.details+'"');
}
});
}; };
var send = function(e) { var send = function(e) {