diff --git a/javascript/net/grpc/web/clientreadablestream.js b/javascript/net/grpc/web/clientreadablestream.js index 9b49c768..9475b236 100644 --- a/javascript/net/grpc/web/clientreadablestream.js +++ b/javascript/net/grpc/web/clientreadablestream.js @@ -12,94 +12,15 @@ goog.provide('grpc.web.ClientReadableStream'); -goog.require('goog.net.XhrIo'); -goog.require('goog.net.streams.NodeReadableStream'); -goog.require('goog.net.streams.createXhrNodeReadableStream'); -goog.require('grpc.web.Status'); - - /** * A stream that the client can read from. Used for calls that are streaming * from the server side. * * @template RESPONSE - * @constructor - * @final - * @param {!goog.net.XhrIo} xhr The XhrIo object - * @param {function(?):!RESPONSE} responseDeserializeFn - * The deserialize function for the proto - * @param {function(?):!grpc.web.Status} - * rpcStatusParseFn A function to parse the Rpc status response + * @interface */ -grpc.web.ClientReadableStream = function( - xhr, responseDeserializeFn, rpcStatusParseFn) { - /** - * @private - * @type {?goog.net.streams.NodeReadableStream} The XHR Node Readable - * Stream - */ - this.xhrNodeReadableStream_ = - goog.net.streams.createXhrNodeReadableStream(xhr); - - /** - * @private - * @type {function(?):!RESPONSE} The deserialize function for the proto - */ - this.responseDeserializeFn_ = responseDeserializeFn; - - /** - * @private - * @type {!goog.net.XhrIo} The XhrIo object - */ - this.xhr_ = xhr; - - /** - * @private - * @type {function(!RESPONSE)|null} The data callback - */ - this.onDataCallback_ = null; - - /** - * @private - * @type {function(!grpc.web.Status)|null} - * The status callback - */ - this.onStatusCallback_ = null; - - /** - * @private - * @type {function(...):?|null} - * The stream end callback - */ - this.onEndCallback_ = null; - - /** - * @private - * @type {function(?):!grpc.web.Status} - * A function to parse the Rpc Status response - */ - this.rpcStatusParseFn_ = rpcStatusParseFn; - - - // Add the callback to the underlying stream - var self = this; - this.xhrNodeReadableStream_.on('data', function(data) { - if ('1' in data && self.onDataCallback_) { - var response = self.responseDeserializeFn_(data['1']); - self.onDataCallback_(response); - } - if ('2' in data && self.onStatusCallback_) { - var status = self.rpcStatusParseFn_(data['2']); - self.onStatusCallback_(status); - } - }); - this.xhrNodeReadableStream_.on('end', function() { - if (self.onEndCallback_) { - self.onEndCallback_(); - } - }); -}; +grpc.web.ClientReadableStream = function() {}; /** @@ -110,23 +31,11 @@ grpc.web.ClientReadableStream = function( * an optional input object * @return {!grpc.web.ClientReadableStream} this object */ -grpc.web.ClientReadableStream.prototype.on = function( - eventType, callback) { - // TODO(stanleycheung): change eventType to @enum type - if (eventType == 'data') { - this.onDataCallback_ = callback; - } else if (eventType == 'status') { - this.onStatusCallback_ = callback; - } else if (eventType == 'end') { - this.onEndCallback_ = callback; - } - return this; -}; +grpc.web.ClientReadableStream.prototype.on = goog.abstractMethod; + /** * Close the stream. */ -grpc.web.ClientReadableStream.prototype.cancel = function() { - this.xhr_.abort(); -}; +grpc.web.ClientReadableStream.prototype.cancel = goog.abstractMethod; diff --git a/javascript/net/grpc/web/gatewayclientbase.js b/javascript/net/grpc/web/gatewayclientbase.js index f3b8fc3b..9cb9a1c9 100644 --- a/javascript/net/grpc/web/gatewayclientbase.js +++ b/javascript/net/grpc/web/gatewayclientbase.js @@ -14,6 +14,7 @@ goog.require('grpc.web.AbstractClientBase'); goog.require('grpc.web.ClientReadableStream'); goog.require('grpc.web.Status'); goog.require('grpc.web.StatusCode'); +goog.require('grpc.web.StreamBodyClientReadableStream'); goog.require('proto.google.rpc.Status'); goog.require('proto.grpc.gateway.Pair'); @@ -109,8 +110,10 @@ grpc.web.GatewayClientBase.prototype.newXhr_ = function() { */ grpc.web.GatewayClientBase.prototype.createClientReadableStream_ = function( xhr, responseDeserializeFn) { - return new grpc.web.ClientReadableStream(xhr, responseDeserializeFn, - grpc.web.GatewayClientBase.parseRpcStatus_); + var stream = new grpc.web.StreamBodyClientReadableStream(xhr); + stream.setResponseDeserializeFn(responseDeserializeFn); + stream.setRpcStatusParseFn(grpc.web.GatewayClientBase.parseRpcStatus_); + return stream; }; diff --git a/javascript/net/grpc/web/grpcwebclientbase.js b/javascript/net/grpc/web/grpcwebclientbase.js new file mode 100644 index 00000000..081047f2 --- /dev/null +++ b/javascript/net/grpc/web/grpcwebclientbase.js @@ -0,0 +1,117 @@ +/** + * @fileoverview gRPC browser client library. + * + * Base class for gRPC Web JS clients using the application/grpc-web wire + * format + * + * @author stanleycheung@google.com (Stanley Cheung) + */ +goog.provide('grpc.web.GrpcWebClientBase'); + + +goog.require('goog.crypt.base64'); +goog.require('goog.net.XhrIo'); +goog.require('grpc.web.AbstractClientBase'); +goog.require('grpc.web.GrpcWebClientReadableStream'); +goog.require('grpc.web.StatusCode'); + + +/** + * Base class for gRPC web client using the application/grpc-web wire format + * @param {?Object=} opt_options + * @constructor + * @implements {grpc.web.AbstractClientBase} + */ +grpc.web.GrpcWebClientBase = function(opt_options) { +}; + + +/** + * @override + */ +grpc.web.GrpcWebClientBase.prototype.rpcCall = function( + method, request, metadata, methodInfo, callback) { + var xhr = this.newXhr_(); + var serialized = methodInfo.requestSerializeFn(request); + xhr.headers.addAll(metadata); + + var stream = new grpc.web.GrpcWebClientReadableStream(xhr); + stream.setResponseDeserializeFn(methodInfo.responseDeserializeFn); + + stream.on('data', function(response) { + callback(null, response); + }); + + stream.on('status', function(status) { + if (status.code != grpc.web.StatusCode.OK) { + callback({ + 'code': status.code, + 'message': status.details + }, null); + } + }); + + xhr.headers.set('Content-Type', 'application/grpc-web-text'); + xhr.headers.set('Accept', 'application/grpc-web-text'); + + var payload = this.encodeRequest_(serialized); + payload = goog.crypt.base64.encodeByteArray(payload); + xhr.send(method, 'POST', payload); + return; +}; + + +/** + * @override + */ +grpc.web.GrpcWebClientBase.prototype.serverStreaming = function( + method, request, metadata, methodInfo) { + var xhr = this.newXhr_(); + var serialized = methodInfo.requestSerializeFn(request); + xhr.headers.addAll(metadata); + + var stream = new grpc.web.GrpcWebClientReadableStream(xhr); + stream.setResponseDeserializeFn(methodInfo.responseDeserializeFn); + + xhr.headers.set('Content-Type', 'application/grpc-web-text'); + xhr.headers.set('Accept', 'application/grpc-web-text'); + + var payload = this.encodeRequest_(serialized); + payload = goog.crypt.base64.encodeByteArray(payload); + xhr.send(method, 'POST', payload); + + return stream; +}; + + +/** + * Create a new XhrIo object + * + * @private + * @return {!goog.net.XhrIo} The created XhrIo object + */ +grpc.web.GrpcWebClientBase.prototype.newXhr_ = function() { + return new goog.net.XhrIo(); +}; + + + +/** + * Encode the grpc-web request + * + * @private + * @param {!Uint8Array} serialized The serialized proto payload + * @return {!Uint8Array} The application/grpc-web padded request + */ +grpc.web.GrpcWebClientBase.prototype.encodeRequest_ = function(serialized) { + var len = serialized.length; + var bytesArray = [0, 0, 0, 0]; + var payload = new Uint8Array(5 + len); + for (var i = 3; i >= 0; i--) { + bytesArray[i] = (len % 256); + len = len >>> 8; + } + payload.set(new Uint8Array(bytesArray), 1); + payload.set(serialized, 5); + return payload; +}; diff --git a/javascript/net/grpc/web/grpcwebclientreadablestream.js b/javascript/net/grpc/web/grpcwebclientreadablestream.js new file mode 100644 index 00000000..1d22bebb --- /dev/null +++ b/javascript/net/grpc/web/grpcwebclientreadablestream.js @@ -0,0 +1,201 @@ +/** + * @fileoverview gRPC web client Readable Stream + * + * This class is being returned after a gRPC streaming call has been + * started. This class provides functionality for user to operates on + * the stream, e.g. set onData callback, etc. + * + * This wraps the underlying goog.net.streams.NodeReadableStream + * + * @author stanleycheung@google.com (Stanley Cheung) + */ +goog.provide('grpc.web.GrpcWebClientReadableStream'); + + +goog.require('goog.crypt.base64'); +goog.require('goog.events'); +goog.require('goog.net.EventType'); +goog.require('goog.net.XhrIo'); +goog.require('goog.net.XmlHttp'); +goog.require('grpc.web.ClientReadableStream'); +goog.require('grpc.web.GrpcWebStreamParser'); +goog.require('grpc.web.StatusCode'); + + + +const GRPC_STATUS = "grpc-status"; +const GRPC_STATUS_MESSAGE = "grpc-message"; + + +/** + * A stream that the client can read from. Used for calls that are streaming + * from the server side. + * + * @template RESPONSE + * @constructor + * @implements {grpc.web.ClientReadableStream} + * @final + * @param {!goog.net.XhrIo} xhr The XhrIo object + */ +grpc.web.GrpcWebClientReadableStream = function(xhr) { + /** + * @private + * @type {!goog.net.XhrIo} The XhrIo object + */ + this.xhr_ = xhr; + + /** + * @private + * @type {function(?):!RESPONSE|null} The deserialize function for the proto + */ + this.responseDeserializeFn_ = null; + + /** + * @private + * @type {function(!RESPONSE)|null} The data callback + */ + this.onDataCallback_ = null; + + /** + * @private + * @type {function(!grpc.web.Status)|null} The status callback + */ + this.onStatusCallback_ = null; + + /** + * @private + * @type {function(...):?|null} The stream end callback + */ + this.onEndCallback_ = null; + + /** + * @private + * @type {number} The stream parser position + */ + this.pos_ = 0; + + /** + * @private + * @type {!grpc.web.GrpcWebStreamParser} The grpc-web stream parser + */ + this.parser_ = new grpc.web.GrpcWebStreamParser(); + + var self = this; + goog.events.listen(this.xhr_, goog.net.EventType.READY_STATE_CHANGE, + function(e) { + var FrameType = grpc.web.GrpcWebStreamParser.FrameType; + + var responseText = self.xhr_.getResponseText(); + var newPos = responseText.length - responseText.length % 4; + var newData = responseText.substr(self.pos_, newPos - self.pos_); + if (newData.length == 0) return; + self.pos_ = newPos; + + var byteSource = goog.crypt.base64.decodeStringToUint8Array(newData); + var messages = self.parser_.parse([].slice.call(byteSource)); + if (!messages) return; + + for (var i = 0; i < messages.length; i++) { + if (FrameType.DATA in messages[i]) { + var data = messages[i][FrameType.DATA]; + if (data) { + var response = self.responseDeserializeFn_(data); + if (response) { + self.onDataCallback_(response); + } + } + } + if (FrameType.TRAILER in messages[i]) { + if (messages[i][FrameType.TRAILER].length > 0) { + var trailerString = ""; + for (var pos = 0; pos < messages[i][FrameType.TRAILER].length; + pos++) { + trailerString += String.fromCharCode( + messages[i][FrameType.TRAILER][pos]); + } + var trailers = self.parseHttp1Headers_(trailerString); + var grpcStatusCode = grpc.web.StatusCode.OK; + var grpcStatusMessage = ""; + if (GRPC_STATUS in trailers) { + grpcStatusCode = trailers[GRPC_STATUS]; + } + if (GRPC_STATUS_MESSAGE in trailers) { + grpcStatusMessage = trailers[GRPC_STATUS_MESSAGE]; + } + if (self.onStatusCallback_) { + self.onStatusCallback_({ + code: Number(grpcStatusCode), + details: grpcStatusMessage, + metadata: trailers, + }); + } + } + } + } + + var readyState = self.xhr_.getReadyState(); + if (readyState == goog.net.XmlHttp.ReadyState.COMPLETE) { + if (self.onEndCallback_) { + self.onEndCallback_(); + } + return; + } + }); +}; + + +/** + * @override + */ +grpc.web.GrpcWebClientReadableStream.prototype.on = function( + eventType, callback) { + // TODO(stanleycheung): change eventType to @enum type + if (eventType == 'data') { + this.onDataCallback_ = callback; + } else if (eventType == 'status') { + this.onStatusCallback_ = callback; + } else if (eventType == 'end') { + this.onEndCallback_ = callback; + } + return this; +}; + + +/** + * Register a callbackl to parse the response + * + * @param {function(?):!RESPONSE} responseDeserializeFn The deserialize + * function for the proto + */ +grpc.web.GrpcWebClientReadableStream.prototype.setResponseDeserializeFn = + function(responseDeserializeFn) { + this.responseDeserializeFn_ = responseDeserializeFn; +}; + + +/** + * @override + */ +grpc.web.GrpcWebClientReadableStream.prototype.cancel = function() { + this.xhr_.abort(); +}; + + +/** + * Parse HTTP headers + * + * @private + * @param {!string} str The raw http header string + * @return {!Object} The header:value pairs + */ +grpc.web.GrpcWebClientReadableStream.prototype.parseHttp1Headers_ = + function(str) { + var chunks = str.trim().split("\r\n"); + var headers = {}; + for (var i = 0; i < chunks.length; i++) { + var pos = chunks[i].indexOf(":"); + headers[chunks[i].substring(0, pos).trim()] = + chunks[i].substring(pos+1).trim(); + } + return headers; +}; diff --git a/javascript/net/grpc/web/grpcwebstreamparser.js b/javascript/net/grpc/web/grpcwebstreamparser.js new file mode 100644 index 00000000..381765bc --- /dev/null +++ b/javascript/net/grpc/web/grpcwebstreamparser.js @@ -0,0 +1,260 @@ +/** + * @fileoverview The default grpc-web stream parser + * + * The default grpc-web parser decodes the input stream (binary) under the + * following rules: + * + * 1. The wire format looks like: + * + * 0x00 0x80 + * + * For details of grpc-web wire format see + * https://github.com/grpc/grpc/blob/master/doc/PROTOCOL-WEB.md + * + * 2. Messages will be delivered once each frame is completed. Partial stream + * segments are accepted. + * + * 3. Example: + * + * Incoming data: 0x00 0x00 0x80 + * + * Result: [ { 0x00 : }, { 0x80 : trailers } ] + */ + +goog.provide('grpc.web.GrpcWebStreamParser'); + +goog.require('goog.asserts'); +goog.require('goog.net.streams.StreamParser'); + +goog.scope(function() { + + +/** + * The default grpc-web stream parser. + * + * @constructor + * @struct + * @implements {goog.net.streams.StreamParser} + * @final + */ +grpc.web.GrpcWebStreamParser = function() { + /** + * The current error message, if any. + * @private {?string} + */ + this.errorMessage_ = null; + + /** + * The currently buffered result (parsed messages). + * @private {!Array} + */ + this.result_ = []; + + /** + * The current position in the streamed data. + * @private {number} + */ + this.streamPos_ = 0; + + /** + * The current parser state. + * @private {grpc.web.GrpcWebStreamParser.State_} + */ + this.state_ = Parser.State_.INIT; + + /** + * The current frame byte being parsed + * @private {number} + */ + this.frame_ = 0; + + /** + * The length of the proto message being parsed. + * @private {number} + */ + this.length_ = 0; + + /** + * Count of processed length bytes. + * @private {number} + */ + this.countLengthBytes_ = 0; + + /** + * Raw bytes of the current message. Uses Uint8Array by default. Falls back to + * native array when Uint8Array is unsupported. + * @private {?Uint8Array|?Array} + */ + this.messageBuffer_ = null; + + /** + * Count of processed message bytes. + * @private {number} + */ + this.countMessageBytes_ = 0; +}; + + +var Parser = grpc.web.GrpcWebStreamParser; + + +/** + * The parser state. + * @private @enum {number} + */ +Parser.State_ = { + INIT: 0, // expecting the next frame byte + LENGTH: 1, // expecting 4 bytes of length + MESSAGE: 2, // expecting more message bytes + INVALID: 3 +}; + + +/** + * Possible frame byte + * @enum {number} + */ +grpc.web.GrpcWebStreamParser.FrameType = { + DATA: 0x00, // expecting a data frame + TRAILER: 0x80, // expecting a trailer frame +}; + + +var FrameType = grpc.web.GrpcWebStreamParser.FrameType; + + +/** + * @override + */ +grpc.web.GrpcWebStreamParser.prototype.isInputValid = function() { + return this.state_ != Parser.State_.INVALID; +}; + + +/** + * @override + */ +grpc.web.GrpcWebStreamParser.prototype.getErrorMessage = function() { + return this.errorMessage_; +}; + + +/** + * @param {!Uint8Array|!Array} inputBytes The current input buffer + * @param {number} pos The position in the current input that triggers the error + * @param {string} errorMsg Additional error message + * @throws {!Error} Throws an error indicating where the stream is broken + * @private + */ +Parser.prototype.error_ = function(inputBytes, pos, errorMsg) { + this.state_ = Parser.State_.INVALID; + this.errorMessage_ = 'The stream is broken @' + this.streamPos_ + '/' + pos + + '. ' + + 'Error: ' + errorMsg + '. ' + + 'With input:\n' + inputBytes; + throw new Error(this.errorMessage_); +}; + + +/** + * @throws {!Error} Throws an error message if the input is invalid. + * @override + */ +grpc.web.GrpcWebStreamParser.prototype.parse = function(input) { + goog.asserts.assert(input instanceof Array || input instanceof ArrayBuffer); + + var parser = this; + var inputBytes = (input instanceof Array) ? input : new Uint8Array(input); + var pos = 0; + + while (pos < inputBytes.length) { + switch (parser.state_) { + case Parser.State_.INVALID: { + parser.error_(inputBytes, pos, 'stream already broken'); + break; + } + case Parser.State_.INIT: { + processFrameByte(inputBytes[pos]); + break; + } + case Parser.State_.LENGTH: { + processLengthByte(inputBytes[pos]); + break; + } + case Parser.State_.MESSAGE: { + processMessageByte(inputBytes[pos]); + break; + } + default: { throw new Error('unexpected parser state: ' + parser.state_); } + } + + parser.streamPos_++; + pos++; + } + + var msgs = parser.result_; + parser.result_ = []; + return msgs.length > 0 ? msgs : null; + + /** + * @param {number} b A frame byte to process + */ + function processFrameByte(b) { + if (b == FrameType.DATA) { + parser.frame_ = b; + } else if (b == FrameType.TRAILER) { + parser.frame_ = b; + } else { + parser.error_(inputBytes, pos, 'invalid frame byte'); + } + + parser.state_ = Parser.State_.LENGTH; + parser.length_ = 0; + parser.countLengthBytes_ = 0; + } + + /** + * @param {number} b A length byte to process + */ + function processLengthByte(b) { + parser.countLengthBytes_++; + parser.length_ = (parser.length_ << 8) + b; + + if (parser.countLengthBytes_ == 4) { // no more length byte + parser.state_ = Parser.State_.MESSAGE; + parser.countMessageBytes_ = 0; + if (typeof Uint8Array !== 'undefined') { + parser.messageBuffer_ = new Uint8Array(parser.length_); + } else { + parser.messageBuffer_ = new Array(parser.length_); + } + + if (parser.length_ == 0) { // empty message + finishMessage(); + } + } + } + + /** + * @param {number} b A message byte to process + */ + function processMessageByte(b) { + parser.messageBuffer_[parser.countMessageBytes_++] = b; + if (parser.countMessageBytes_ == parser.length_) { + finishMessage(); + } + } + + /** + * Finishes up building the current message and resets parser state + */ + function finishMessage() { + var message = {}; + message[parser.frame_] = parser.messageBuffer_; + parser.result_.push(message); + parser.state_ = Parser.State_.INIT; + } +}; + + +}); // goog.scope diff --git a/javascript/net/grpc/web/streambodyclientreadablestream.js b/javascript/net/grpc/web/streambodyclientreadablestream.js new file mode 100644 index 00000000..98ba29fd --- /dev/null +++ b/javascript/net/grpc/web/streambodyclientreadablestream.js @@ -0,0 +1,149 @@ +/** + * @fileoverview gRPC web client Readable Stream + * + * This class is being returned after a gRPC streaming call has been + * started. This class provides functionality for user to operates on + * the stream, e.g. set onData callback, etc. + * + * This wraps the underlying goog.net.streams.NodeReadableStream + * + * @author stanleycheung@google.com (Stanley Cheung) + */ +goog.provide('grpc.web.StreamBodyClientReadableStream'); + + +goog.require('goog.net.XhrIo'); +goog.require('goog.net.streams.NodeReadableStream'); +goog.require('goog.net.streams.createXhrNodeReadableStream'); +goog.require('grpc.web.ClientReadableStream'); +goog.require('grpc.web.Status'); + + + +/** + * A stream that the client can read from. Used for calls that are streaming + * from the server side. + * + * @template RESPONSE + * @constructor + * @implements {grpc.web.ClientReadableStream} + * @final + * @param {!goog.net.XhrIo} xhr The XhrIo object + */ +grpc.web.StreamBodyClientReadableStream = function( + xhr) { + /** + * @private + * @type {?goog.net.streams.NodeReadableStream} The XHR Node Readable + * Stream + */ + this.xhrNodeReadableStream_ = + goog.net.streams.createXhrNodeReadableStream(xhr); + + /** + * @private + * @type {function(?):!RESPONSE|null} The deserialize function for the proto + */ + this.responseDeserializeFn_ = null; + + /** + * @private + * @type {!goog.net.XhrIo} The XhrIo object + */ + this.xhr_ = xhr; + + /** + * @private + * @type {function(!RESPONSE)|null} The data callback + */ + this.onDataCallback_ = null; + + /** + * @private + * @type {function(!grpc.web.Status)|null} + * The status callback + */ + this.onStatusCallback_ = null; + + /** + * @private + * @type {function(...):?|null} + * The stream end callback + */ + this.onEndCallback_ = null; + + /** + * @private + * @type {function(?):!grpc.web.Status|null} + * A function to parse the Rpc Status response + */ + this.rpcStatusParseFn_ = null; + + + // Add the callback to the underlying stream + var self = this; + this.xhrNodeReadableStream_.on('data', function(data) { + if ('1' in data && self.onDataCallback_) { + var response = self.responseDeserializeFn_(data['1']); + self.onDataCallback_(response); + } + if ('2' in data && self.onStatusCallback_) { + var status = self.rpcStatusParseFn_(data['2']); + self.onStatusCallback_(status); + } + }); + this.xhrNodeReadableStream_.on('end', function() { + if (self.onEndCallback_) { + self.onEndCallback_(); + } + }); +}; + + +/** + * @override + */ +grpc.web.StreamBodyClientReadableStream.prototype.on = function( + eventType, callback) { + // TODO(stanleycheung): change eventType to @enum type + if (eventType == 'data') { + this.onDataCallback_ = callback; + } else if (eventType == 'status') { + this.onStatusCallback_ = callback; + } else if (eventType == 'end') { + this.onEndCallback_ = callback; + } + return this; +}; + + +/** + * Register a callbackl to parse the response + * + * @param {function(?):!RESPONSE} responseDeserializeFn The deserialize + * function for the proto + */ +grpc.web.StreamBodyClientReadableStream.prototype.setResponseDeserializeFn = + function(responseDeserializeFn) { + this.responseDeserializeFn_ = responseDeserializeFn; +}; + + + +/** + * Register a function to parse RPC status response + * + * @param {function(?):!grpc.web.Status} rpcStatusParseFn A function to parse + * the RPC status response + */ +grpc.web.StreamBodyClientReadableStream.prototype.setRpcStatusParseFn = function(rpcStatusParseFn) { + this.rpcStatusParseFn_ = rpcStatusParseFn; +}; + + +/** + * @override + */ +grpc.web.StreamBodyClientReadableStream.prototype.cancel = function() { + this.xhr_.abort(); +}; diff --git a/net/grpc/gateway/examples/echo/Makefile b/net/grpc/gateway/examples/echo/Makefile index 99f5aeeb..37f461ed 100644 --- a/net/grpc/gateway/examples/echo/Makefile +++ b/net/grpc/gateway/examples/echo/Makefile @@ -106,7 +106,7 @@ proto-js: $(PROTOS_PATH)/protos/pair.proto $(PROTOC) -I=. --js_out=$(JS_IMPORT_STYLE):$(OUT_DIR) ./echo.proto $(PROTOC) -I=. --plugin=protoc-gen-grpc-web=$(GRPC_WEB_PLUGIN_PATH) \ - --grpc-web_out=out=$(OUT_DIR)/echo.grpc.pb.js,mode=base64:. ./echo.proto + --grpc-web_out=out=$(OUT_DIR)/echo.grpc.pb.js,mode=grpcweb:. ./echo.proto install: mkdir -p $(HTML_DIR)/$(EXAMPLES_PATH) diff --git a/net/grpc/gateway/examples/echo/echotest.html b/net/grpc/gateway/examples/echo/echotest.html index e0ffa8fa..e17d5932 100644 --- a/net/grpc/gateway/examples/echo/echotest.html +++ b/net/grpc/gateway/examples/echo/echotest.html @@ -30,7 +30,8 @@ addLeftMessage(msg); var unaryRequest = new proto.grpc.gateway.testing.EchoRequest(); unaryRequest.setMessage(msg); - echoService.echo(unaryRequest, {}, function(err, response) { + echoService.echo(unaryRequest, {"custom-header-1": "value1"}, + function(err, response) { if (err) { addRightMessage('Error code: '+err.code+' "'+err.message+'"'); } else { @@ -41,6 +42,23 @@ }); }; + var echoError = function(msg) { + addLeftMessage(msg); + var unaryRequest = + new proto.grpc.gateway.testing.EchoRequest(); + unaryRequest.setMessage(msg); + echoService.echoAbort(unaryRequest, {}, function(err, response) { + if (err) { + addRightMessage('Error received'); + console.log('Error:', err); + console.log('Error code: ' + err.code + + (err.code == grpc.web.StatusCode.ABORTED ? + ' is ' : ' is not ') + + 'StatusCode.ABORTED'); + } + }); + }; + var repeatEcho = function(msg, count) { addLeftMessage(msg); if (count > MAX_STREAM_MESSAGES) count = MAX_STREAM_MESSAGES; @@ -50,7 +68,8 @@ streamRequest.setMessageCount(count); streamRequest.setMessageInterval(INTERVAL); - var stream = echoService.serverStreamingEcho(streamRequest); + var stream = echoService.serverStreamingEcho(streamRequest, + {"custom-header-1": "value1"}); stream.on('data', function(response) { addRightMessage(response.getMessage()); }); @@ -58,6 +77,13 @@ if (status.code != grpc.web.StatusCode.OK) { addRightMessage('Error code: '+status.code+' "'+status.details+'"'); } + if (status.metadata) { + console.log("Received metadata"); + console.log(status.metadata); + } + }); + stream.on('end', function() { + console.log("stream end signal received"); }); }; @@ -70,6 +96,8 @@ var count = msg.substr(0, msg.indexOf(' ')); if (/^\d+$/.test(count)) { repeatEcho(msg.substr(msg.indexOf(' ') + 1), count); + } else if (count == 'err') { + echoError(msg.substr(msg.indexOf(' ') + 1)); } else { echo(msg); } @@ -102,7 +130,8 @@
- +

Example: "Hello", "4 Hello"