diff --git a/javascript/net/grpc/web/clientreadablestream.js b/javascript/net/grpc/web/clientreadablestream.js index 0f3f0bb6d..5ba440ef7 100644 --- a/javascript/net/grpc/web/clientreadablestream.js +++ b/javascript/net/grpc/web/clientreadablestream.js @@ -43,10 +43,10 @@ const ClientReadableStream = function() {}; /** - * Register a callback to handle I/O events. + * Register a callback to handle different stream events. * * @param {string} eventType The event type - * @param {function(?)} callback The call back to handle the event with + * @param {function(?)} callback The callback to handle the event with * an optional input object * @return {!ClientReadableStream} this object */ @@ -54,6 +54,17 @@ ClientReadableStream.prototype.on = goog.abstractMethod; +/** + * Remove a particular callback. + * + * @param {string} eventType The event type + * @param {function(?)} callback The callback to remove + * @return {!ClientReadableStream} this object + */ +ClientReadableStream.prototype.removeListener = goog.abstractMethod; + + + /** * Close the stream. */ diff --git a/javascript/net/grpc/web/grpcwebclientbase.js b/javascript/net/grpc/web/grpcwebclientbase.js index 5431efddb..78d80414d 100644 --- a/javascript/net/grpc/web/grpcwebclientbase.js +++ b/javascript/net/grpc/web/grpcwebclientbase.js @@ -213,18 +213,23 @@ GrpcWebClientBase.prototype.startStream_ = function(request, hostname) { * @param {boolean} useUnaryResponse */ GrpcWebClientBase.setCallback_ = function(stream, callback, useUnaryResponse) { + var responseReceived = null; + var errorEmitted = false; + stream.on('data', function(response) { - callback(null, response); + responseReceived = response; }); stream.on('error', function(error) { - if (error.code != StatusCode.OK) { + if (error.code != StatusCode.OK && !errorEmitted) { + errorEmitted = true; callback(error, null); } }); stream.on('status', function(status) { - if (status.code != StatusCode.OK) { + if (status.code != StatusCode.OK && !errorEmitted) { + errorEmitted = true; callback( { code: status.code, @@ -241,11 +246,16 @@ GrpcWebClientBase.setCallback_ = function(stream, callback, useUnaryResponse) { stream.on('metadata', function(metadata) { callback(null, null, null, metadata); }); - - stream.on('end', function() { - callback(null, null); - }); } + + stream.on('end', function() { + if (!errorEmitted) { + callback(null, responseReceived); + } + if (useUnaryResponse) { + callback(null, null); // trigger unaryResponse + } + }); }; /** diff --git a/javascript/net/grpc/web/grpcwebclientreadablestream.js b/javascript/net/grpc/web/grpcwebclientreadablestream.js index bca5b8c99..ed555f59c 100644 --- a/javascript/net/grpc/web/grpcwebclientreadablestream.js +++ b/javascript/net/grpc/web/grpcwebclientreadablestream.js @@ -81,34 +81,39 @@ const GrpcWebClientReadableStream = function(genericTransportInterface) { this.responseDeserializeFn_ = null; /** + * @const * @private - * @type {function(!RESPONSE)|null} The data callback + * @type {!Array} The list of data callbacks */ - this.onDataCallback_ = null; + this.onDataCallbacks_ = []; /** + * @const * @private - * @type {function(!Status)|null} The status callback + * @type {!Array} The list of status callbacks */ - this.onStatusCallback_ = null; + this.onStatusCallbacks_ = []; /** + * @const * @private - * @type {function(!Metadata)|null} The metadata callback + * @type {!Array} The list of metadata callbacks */ - this.onMetadataCallback_ = null; + this.onMetadataCallbacks_ = []; /** + * @const * @private - * @type {function(...):?|null} The error callback + * @type {!Array} The list of error callbacks */ - this.onErrorCallback_ = null; + this.onErrorCallbacks_ = []; /** + * @const * @private - * @type {function(...):?|null} The stream end callback + * @type {!Array} The list of stream end callbacks */ - this.onEndCallback_ = null; + this.onEndCallbacks_ = []; /** * @private @@ -158,7 +163,7 @@ const GrpcWebClientReadableStream = function(genericTransportInterface) { if (data) { var response = self.responseDeserializeFn_(data); if (response) { - self.onDataCallback_(response); + self.sendDataCallbacks_(response); } } } @@ -181,13 +186,11 @@ const GrpcWebClientReadableStream = function(genericTransportInterface) { grpcStatusMessage = trailers[GRPC_STATUS_MESSAGE]; delete trailers[GRPC_STATUS_MESSAGE]; } - if (self.onStatusCallback_) { - self.onStatusCallback_(/** @type {!Status} */({ - code: Number(grpcStatusCode), - details: grpcStatusMessage, - metadata: trailers, - })); - } + self.sendStatusCallbacks_(/** @type {!Status} */({ + code: Number(grpcStatusCode), + details: grpcStatusMessage, + metadata: trailers, + })); } } } @@ -201,14 +204,12 @@ const GrpcWebClientReadableStream = function(genericTransportInterface) { var initialMetadata = /** @type {!Metadata} */ ({}); var responseHeaders = self.xhr_.getResponseHeaders(); - if (self.onMetadataCallback_) { - Object.keys(responseHeaders).forEach((header_) => { - if (!(EXCLUDED_RESPONSE_HEADERS.includes(header_))) { - initialMetadata[header_] = responseHeaders[header_]; - } - }); - self.onMetadataCallback_(initialMetadata); - } + Object.keys(responseHeaders).forEach((header_) => { + if (!(EXCLUDED_RESPONSE_HEADERS.includes(header_))) { + initialMetadata[header_] = responseHeaders[header_]; + } + }); + self.sendMetadataCallbacks_(initialMetadata); // There's an XHR level error if (lastErrorCode != ErrorCode.NO_ERROR) { @@ -228,12 +229,10 @@ const GrpcWebClientReadableStream = function(genericTransportInterface) { if (grpcStatusCode == StatusCode.ABORTED && self.aborted_) { return; } - if (self.onErrorCallback_) { - self.onErrorCallback_({ - code: grpcStatusCode, - message: ErrorCode.getDebugMessage(lastErrorCode) - }); - } + self.sendErrorCallbacks_({ + code: grpcStatusCode, + message: ErrorCode.getDebugMessage(lastErrorCode) + }); return; } @@ -245,16 +244,16 @@ const GrpcWebClientReadableStream = function(genericTransportInterface) { if (GRPC_STATUS_MESSAGE in responseHeaders) { grpcStatusMessage = self.xhr_.getResponseHeader(GRPC_STATUS_MESSAGE); } - if (Number(grpcStatusCode) != StatusCode.OK && self.onErrorCallback_) { - self.onErrorCallback_({ + if (Number(grpcStatusCode) != StatusCode.OK) { + self.sendErrorCallbacks_({ code: Number(grpcStatusCode), message: grpcStatusMessage, metadata: responseHeaders }); errorEmitted = true; } - if (!errorEmitted && self.onStatusCallback_) { - self.onStatusCallback_(/** @type {!Status} */ ({ + if (!errorEmitted) { + self.sendStatusCallbacks_(/** @type {!Status} */ ({ code: Number(grpcStatusCode), details: grpcStatusMessage, metadata: responseHeaders @@ -263,9 +262,7 @@ const GrpcWebClientReadableStream = function(genericTransportInterface) { } if (!errorEmitted) { - if (self.onEndCallback_) { - self.onEndCallback_(); - } + self.sendEndCallbacks_(); } }); }; @@ -279,15 +276,50 @@ GrpcWebClientReadableStream.prototype.on = function( eventType, callback) { // TODO(stanleycheung): change eventType to @enum type if (eventType == 'data') { - this.onDataCallback_ = callback; + this.onDataCallbacks_.push(callback); + } else if (eventType == 'status') { + this.onStatusCallbacks_.push(callback); + } else if (eventType == 'metadata') { + this.onMetadataCallbacks_.push(callback); + } else if (eventType == 'end') { + this.onEndCallbacks_.push(callback); + } else if (eventType == 'error') { + this.onErrorCallbacks_.push(callback); + } + return this; +}; + + +/** + * @private + * @param {!Array} callbacks the internal list of callbacks + * @param {function(?)} callback the callback to remove + */ +GrpcWebClientReadableStream.prototype.removeListenerFromCallbacks_ = function( + callbacks, callback) { + const index = callbacks.indexOf(callback); + if (index > -1) { + callbacks.splice(index, 1); + } +}; + + +/** + * @export + * @override + */ +GrpcWebClientReadableStream.prototype.removeListener = function( + eventType, callback) { + if (eventType == 'data') { + this.removeListenerFromCallbacks_(this.onDataCallbacks_, callback); } else if (eventType == 'status') { - this.onStatusCallback_ = callback; + this.removeListenerFromCallbacks_(this.onStatusCallbacks_, callback); } else if (eventType == 'metadata') { - this.onMetadataCallback_ = callback; + this.removeListenerFromCallbacks_(this.onMetadataCallbacks_, callback); } else if (eventType == 'end') { - this.onEndCallback_ = callback; + this.removeListenerFromCallbacks_(this.onEndCallbacks_, callback); } else if (eventType == 'error') { - this.onErrorCallback_ = callback; + this.removeListenerFromCallbacks_(this.onErrorCallbacks_, callback); } return this; }; @@ -335,5 +367,59 @@ GrpcWebClientReadableStream.prototype.parseHttp1Headers_ = }; +/** + * @private + * @param {!RESPONSE} data The data to send back + */ +GrpcWebClientReadableStream.prototype.sendDataCallbacks_ = function(data) { + for (var i = 0; i < this.onDataCallbacks_.length; i++) { + this.onDataCallbacks_[i](data); + } +}; + + +/** + * @private + * @param {!Status} status The status to send back + */ +GrpcWebClientReadableStream.prototype.sendStatusCallbacks_ = function(status) { + for (var i = 0; i < this.onStatusCallbacks_.length; i++) { + this.onStatusCallbacks_[i](status); + } +}; + + +/** + * @private + * @param {!Metadata} metadata The metadata to send back + */ +GrpcWebClientReadableStream.prototype.sendMetadataCallbacks_ = + function(metadata) { + for (var i = 0; i < this.onMetadataCallbacks_.length; i++) { + this.onMetadataCallbacks_[i](metadata); + } +}; + + +/** + * @private + * @param {?} error The error to send back + */ +GrpcWebClientReadableStream.prototype.sendErrorCallbacks_ = function(error) { + for (var i = 0; i < this.onErrorCallbacks_.length; i++) { + this.onErrorCallbacks_[i](error); + } +}; + + +/** + * @private + */ +GrpcWebClientReadableStream.prototype.sendEndCallbacks_ = function() { + for (var i = 0; i < this.onEndCallbacks_.length; i++) { + this.onEndCallbacks_[i](); + } +}; + exports = GrpcWebClientReadableStream; diff --git a/javascript/net/grpc/web/streambodyclientreadablestream.js b/javascript/net/grpc/web/streambodyclientreadablestream.js index beedfd617..002721a6f 100644 --- a/javascript/net/grpc/web/streambodyclientreadablestream.js +++ b/javascript/net/grpc/web/streambodyclientreadablestream.js @@ -77,31 +77,32 @@ const StreamBodyClientReadableStream = function(genericTransportInterface) { this.xhr_ = genericTransportInterface.xhr; /** + * @const * @private - * @type {function(RESPONSE)|null} The data callback + * @type {!Array} The list of data callback */ - this.onDataCallback_ = null; + this.onDataCallbacks_ = []; /** + * @const * @private - * @type {function(!Status)|null} - * The status callback + * @type {!Array} The list of status callback */ - this.onStatusCallback_ = null; + this.onStatusCallbacks_ = []; /** + * @const * @private - * @type {function(...):?|null} - * The stream end callback + * @type {!Array} The list of stream end callback */ - this.onEndCallback_ = null; + this.onEndCallbacks_ = []; /** + * @const * @private - * @type {function(...):?|null} - * The stream error callback + * @type {!Array} The list of error callback */ - this.onErrorCallback_ = null; + this.onErrorCallbacks_ = []; /** * @private @@ -146,7 +147,7 @@ StreamBodyClientReadableStream.prototype.setUnaryCallback_ = function() { var response = this.responseDeserializeFn_(responseText); var grpcStatus = StatusCode.fromHttpStatus(this.xhr_.getStatus()); if (grpcStatus == StatusCode.OK) { - this.onDataCallback_(response); + this.sendDataCallbacks_(response); } else { this.onErrorResponseCallback_( /** @type {!GrpcWebError} */ ({ @@ -156,7 +157,7 @@ StreamBodyClientReadableStream.prototype.setUnaryCallback_ = function() { } } else if (this.xhr_.getStatus() == 404) { var message = 'Not Found: ' + this.xhr_.getLastUri(); - this.onErrorCallback_({ + this.sendErrorCallbacks_({ code: StatusCode.NOT_FOUND, message: message, }); @@ -164,13 +165,13 @@ StreamBodyClientReadableStream.prototype.setUnaryCallback_ = function() { var rawResponse = this.xhr_.getResponseText(); if (rawResponse) { var status = this.rpcStatusParseFn_(rawResponse); - this.onErrorCallback_({ + this.sendErrorCallbacks_({ code: status.code, message: status.details, metadata: status.metadata, }); } else { - this.onErrorCallback_({ + this.sendErrorCallbacks_({ code: StatusCode.UNAVAILABLE, message: ErrorCode.getDebugMessage(this.xhr_.getLastErrorCode()), }); @@ -186,22 +187,20 @@ StreamBodyClientReadableStream.prototype.setStreamCallback_ = function() { // Add the callback to the underlying stream var self = this; this.xhrNodeReadableStream_.on('data', function(data) { - if ('1' in data && self.onDataCallback_) { + if ('1' in data) { var response = self.responseDeserializeFn_(data['1']); - self.onDataCallback_(response); + self.sendDataCallbacks_(response); } - if ('2' in data && self.onStatusCallback_) { + if ('2' in data) { var status = self.rpcStatusParseFn_(data['2']); - self.onStatusCallback_(status); + self.sendStatusCallbacks_(status); } }); this.xhrNodeReadableStream_.on('end', function() { - if (self.onEndCallback_) { - self.onEndCallback_(); - } + self.sendEndCallbacks_(); }); this.xhrNodeReadableStream_.on('error', function() { - if (!self.onErrorCallback_) return; + if (self.onErrorCallbacks_.length == 0) return; var lastErrorCode = self.xhr_.getLastErrorCode(); if (lastErrorCode === ErrorCode.NO_ERROR && !self.xhr_.isSuccess()) { // The lastErrorCode on the XHR isn't useful in this case, but the XHR @@ -228,7 +227,7 @@ StreamBodyClientReadableStream.prototype.setStreamCallback_ = function() { grpcStatusCode = StatusCode.UNAVAILABLE; } - self.onErrorCallback_({ + self.sendErrorCallbacks_({ code: grpcStatusCode, // TODO(armiller): get the message from the response? // GoogleRpcStatus.deserialize(rawResponse).getMessage()? @@ -246,13 +245,46 @@ StreamBodyClientReadableStream.prototype.on = function( eventType, callback) { // TODO(stanleycheung): change eventType to @enum type if (eventType == 'data') { - this.onDataCallback_ = callback; + this.onDataCallbacks_.push(callback); + } else if (eventType == 'status') { + this.onStatusCallbacks_.push(callback); + } else if (eventType == 'end') { + this.onEndCallbacks_.push(callback); + } else if (eventType == 'error') { + this.onErrorCallbacks_.push(callback); + } + return this; +}; + + +/** + * @private + * @param {!Array} callbacks the internal list of callbacks + * @param {function(?)} callback the callback to remove + */ +StreamBodyClientReadableStream.prototype.removeListenerFromCallbacks_ = function( + callbacks, callback) { + const index = callbacks.indexOf(callback); + if (index > -1) { + callbacks.splice(index, 1); + } +}; + + +/** + * @export + * @override + */ +StreamBodyClientReadableStream.prototype.removeListener = function( + eventType, callback) { + if (eventType == 'data') { + this.removeListenerFromCallbacks_(this.onDataCallbacks_, callback); } else if (eventType == 'status') { - this.onStatusCallback_ = callback; + this.removeListenerFromCallbacks_(this.onStatusCallbacks_, callback); } else if (eventType == 'end') { - this.onEndCallback_ = callback; + this.removeListenerFromCallbacks_(this.onEndCallbacks_, callback); } else if (eventType == 'error') { - this.onErrorCallback_ = callback; + this.removeListenerFromCallbacks_(this.onErrorCallbacks_, callback); } return this; }; @@ -275,8 +307,8 @@ StreamBodyClientReadableStream.prototype.setResponseDeserializeFn = StreamBodyClientReadableStream.prototype.setErrorResponseFn = function( errorResponseFn) { this.onErrorResponseCallback_ = errorResponseFn; - this.onDataCallback_ = (response) => errorResponseFn(null, response); - this.onErrorCallback_ = (error) => errorResponseFn(error, null); + this.onDataCallbacks_.push( (response) => errorResponseFn(null, response) ); + this.onErrorCallbacks_.push( (error) => errorResponseFn(error, null) ); }; /** @@ -299,5 +331,47 @@ StreamBodyClientReadableStream.prototype.cancel = function() { }; +/** + * @private + * @param {!RESPONSE} data The data to send back + */ +StreamBodyClientReadableStream.prototype.sendDataCallbacks_ = function(data) { + for (var i = 0; i < this.onDataCallbacks_.length; i++) { + this.onDataCallbacks_[i](data); + } +}; + + +/** + * @private + * @param {!Status} status The status to send back + */ +StreamBodyClientReadableStream.prototype.sendStatusCallbacks_ = function(status) { + for (var i = 0; i < this.onStatusCallbacks_.length; i++) { + this.onStatusCallbacks_[i](status); + } +}; + + +/** + * @private + * @param {?} error The error to send back + */ +StreamBodyClientReadableStream.prototype.sendErrorCallbacks_ = function(error) { + for (var i = 0; i < this.onErrorCallbacks_.length; i++) { + this.onErrorCallbacks_[i](error); + } +}; + + +/** + * @private + */ +StreamBodyClientReadableStream.prototype.sendEndCallbacks_ = function() { + for (var i = 0; i < this.onEndCallbacks_.length; i++) { + this.onEndCallbacks_[i](); + } +}; + exports = StreamBodyClientReadableStream; diff --git a/packages/grpc-web/externs.js b/packages/grpc-web/externs.js index ffcaa7024..b54c7511f 100644 --- a/packages/grpc-web/externs.js +++ b/packages/grpc-web/externs.js @@ -6,6 +6,7 @@ var module; */ module.ClientReadableStream = function() {}; module.ClientReadableStream.prototype.on = function(eventType, callback) {}; +module.ClientReadableStream.prototype.removeListener = function(eventType, callback) {}; module.ClientReadableStream.prototype.cancel = function() {}; module.GenericClient = function() {}; diff --git a/packages/grpc-web/test/generated_code_test.js b/packages/grpc-web/test/generated_code_test.js index 2a551c6a9..ad5268918 100644 --- a/packages/grpc-web/test/generated_code_test.js +++ b/packages/grpc-web/test/generated_code_test.js @@ -27,6 +27,14 @@ const mockXmlHttpRequest = require('mock-xmlhttprequest'); var MockXMLHttpRequest; +function multiDone(done, count) { + return function() { + count -= 1; + if (count <= 0) { + done(); + } + }; +} describe('protoc generated code', function() { const genCodePath = path.resolve(__dirname, './echo_pb.js'); @@ -195,6 +203,7 @@ describe('grpc-web generated code (commonjs+grpcwebtext)', function() { }); it('should receive trailing metadata', function(done) { + done = multiDone(done, 2); execSync(genCodeCmd); const {EchoServiceClient} = require(genCodePath); const {EchoRequest} = require(protoGenCodePath); @@ -206,12 +215,19 @@ describe('grpc-web generated code (commonjs+grpcwebtext)', function() { 200, {'Content-Type': 'application/grpc-web-text'}, // a single data frame with an 'aaa' message, followed by, // a trailer frame with content 'grpc-status: 0\d\ax-custom-1: ababab' - 'AAAAAAUKA2FhYYAAAAAkZ3JwYy1zdGF0dXM6IDANCngtY3VzdG9tLTE6IGFiYWJhYg0K'); + 'AAAAAAUKA2FhYYAAAAAkZ3JwYy1zdGF0dXM6IDANCngtY3VzdG9tLTE6IGFiYWJhYg0K' + ); }; - var call = echoService.echo(request, {'custom-header-1':'value1'}, - function(err, response) { - assert.equal('aaa', response.getMessage()); - }); + var call = echoService.echo( + request, {'custom-header-1':'value1'}, + function(err, response) { + if (err) { + assert.fail('should not receive error'); + } + assert(response); + assert.equal('aaa', response.getMessage()); + done(); + }); call.on('status', function(status) { assert.equal('object', typeof status.metadata); assert.equal(false, 'grpc-status' in status.metadata); @@ -233,12 +249,54 @@ describe('grpc-web generated code (commonjs+grpcwebtext)', function() { // a trailer frame with content 'grpc-status:10' 'gAAAABBncnBjLXN0YXR1czoxMA0K'); }; - var call = echoService.echo(request, {'custom-header-1':'value1'}, - function(err, response) { - assert.equal(10, err.code); - done(); - }); + var call = echoService.echo( + request, {'custom-header-1':'value1'}, + function(err, response) { + if (response) { + assert.fail('should not have received response'); + } + assert(err); + assert.equal(10, err.code); + done(); + }); + }); + + it('should not receive response on non-ok status', function(done) { + done = multiDone(done, 2); + execSync(genCodeCmd); + const {EchoServiceClient} = require(genCodePath); + const {EchoRequest} = require(protoGenCodePath); + var echoService = new EchoServiceClient('MyHostname', null, null); + var request = new EchoRequest(); + request.setMessage('aaa'); + MockXMLHttpRequest.onSend = function(xhr) { + xhr.respond( + 200, {'Content-Type': 'application/grpc-web-text'}, + // a single data frame with an 'aaa' message, followed by, + // a trailer frame with content 'grpc-status: 2\d\ax-custom-1: ababab' + 'AAAAAAUKA2FhYYAAAAAkZ3JwYy1zdGF0dXM6IDINCngtY3VzdG9tLTE6IGFiYWJhYg0K' + ); + }; + var call = echoService.echo( + request, {'custom-header-1':'value1'}, + function(err, response) { + if (response) { + assert.fail('should not have received response with non-OK status'); + } else { + assert.equal(2, err.code); + } + done(); + }); + call.on('status', function(status) { + assert.equal(2, status.code); + assert.equal('object', typeof status.metadata); + assert.equal(false, 'grpc-status' in status.metadata); + assert.equal(true, 'x-custom-1' in status.metadata); + assert.equal('ababab', status.metadata['x-custom-1']); + done(); + }); }); + }); describe('grpc-web generated code (closure+grpcwebtext)', function() { @@ -262,7 +320,8 @@ describe('grpc-web generated code (closure+grpcwebtext)', function() { `--entry_point=goog:proto.grpc.gateway.testing.EchoAppClient`, `--dependency_mode=PRUNE`, `--js_output_file ./test/generated/compiled.js`, - `--output_wrapper="%output%module.exports = proto.grpc.gateway.testing;"`, + `--output_wrapper="%output%module.exports = `+ + `proto.grpc.gateway.testing;"`, ] ); const closureCmd = "google-closure-compiler " + closureArgs.join(' '); @@ -331,6 +390,241 @@ describe('grpc-web generated code (closure+grpcwebtext)', function() { }); }); +describe('grpc-web generated code: callbacks tests', function() { + const protoGenCodePath = path.resolve(__dirname, './echo_pb.js'); + const genCodePath = path.resolve(__dirname, './echo_grpc_web_pb.js'); + + const genCodeCmd = + 'protoc -I=./test/protos echo.proto ' + + '--js_out=import_style=commonjs:./test ' + + '--grpc-web_out=import_style=commonjs,mode=grpcwebtext:./test'; + + var echoService; + var request; + + before(function() { + MockXMLHttpRequest = mockXmlHttpRequest.newMockXhr() + global.XMLHttpRequest = MockXMLHttpRequest; + + execSync(genCodeCmd); + const {EchoServiceClient} = require(genCodePath); + const {EchoRequest} = require(protoGenCodePath); + echoService = new EchoServiceClient('MyHostname', null, null); + request = new EchoRequest(); + request.setMessage('aaa'); + }); + + after(function() { + if (fs.existsSync(protoGenCodePath)) { + fs.unlinkSync(protoGenCodePath); + } + if (fs.existsSync(genCodePath)) { + fs.unlinkSync(genCodePath); + } + }); + + it('should receive initial metadata callback', function(done) { + done = multiDone(done, 2); + MockXMLHttpRequest.onSend = function(xhr) { + xhr.respond( + 200, { + 'Content-Type': 'application/grpc-web-text', + 'initial-header-1': 'value1', + }, + // a single data frame with message 'aaa' + 'AAAAAAUKA2FhYQ=='); + }; + var call = echoService.echo( + request, {}, + function(err, response) { + if (err) { + assert.fail('should not have received error'); + } else { + assert.equal('aaa', response.getMessage()); + } + done(); + } + ); + call.on('metadata', (metadata) => { + assert('initial-header-1' in metadata); + assert.equal('value1', metadata['initial-header-1']); + done(); + }); + }); + + it('should receive error, on html error', function(done) { + MockXMLHttpRequest.onSend = function(xhr) { + xhr.respond( + 400, {'Content-Type': 'application/grpc-web-text'}); + }; + var call = echoService.echo( + request, {}, + function(err, response) { + if (response) { + assert.fail('should not have received response with non-OK status'); + } else { + assert.equal(3, err.code); // http error 400 mapped to grpc error 3 + } + done(); + } + ); + call.on('status', (status) => { + assert.fail('should not have received a status callback'); + }); + }); + + it('should receive error, on grpc error', function(done) { + done = multiDone(done, 2); + MockXMLHttpRequest.onSend = function(xhr) { + xhr.respond( + 200, {'Content-Type': 'application/grpc-web-text'}, + // a single data frame with an 'aaa' message, followed by, + // a trailer frame with content 'grpc-status: 2\d\ax-custom-1: ababab' + 'AAAAAAUKA2FhYYAAAAAkZ3JwYy1zdGF0dXM6IDINCngtY3VzdG9tLTE6IGFiYWJhYg0K' + ); + }; + var call = echoService.echo( + request, {}, + function(err, response) { + if (response) { + assert.fail('should not have received response with non-OK status'); + } else { + assert.equal(2, err.code); + assert.equal(true, 'x-custom-1' in err.metadata); + assert.equal('ababab', err.metadata['x-custom-1']); + } + done(); + } + ); + // also should receive trailing status callback + call.on('status', (status) => { + // grpc-status should not be part of trailing metadata + assert.equal(false, 'grpc-status' in status.metadata); + assert.equal(true, 'x-custom-1' in status.metadata); + assert.equal('ababab', status.metadata['x-custom-1']); + done(); + }); + }); + + it('should receive error, on response header error', function(done) { + MockXMLHttpRequest.onSend = function(xhr) { + xhr.respond( + 200, { + 'Content-Type': 'application/grpc-web-text', + 'grpc-status': 2, + 'grpc-message': 'some error', + }); + }; + var call = echoService.echo( + request, {}, + function(err, response) { + if (response) { + assert.fail('should not have received response with non-OK status'); + } else { + assert.equal(2, err.code); + assert.equal('some error', err.message); + } + done(); + } + ); + call.on('status', (status) => { + assert.fail('should not receive a trailing status callback'); + }); + }); + + it('should receive status callback', function(done) { + done = multiDone(done, 2); + MockXMLHttpRequest.onSend = function(xhr) { + xhr.respond( + 200, {'Content-Type': 'application/grpc-web-text'}, + // a single data frame with an 'aaa' message, followed by, + // a trailer frame with content 'grpc-status: 0\d\ax-custom-1: ababab' + 'AAAAAAUKA2FhYYAAAAAkZ3JwYy1zdGF0dXM6IDANCngtY3VzdG9tLTE6IGFiYWJhYg0K' + ); + }; + var call = echoService.echo( + request, {}, + function(err, response) { + if (err) { + assert.fail('should not receive error'); + } + assert(response); + assert.equal('aaa', response.getMessage()); + done(); + } + ); + call.on('status', (status) => { + // grpc-status should not be part of trailing metadata + assert.equal(false, 'grpc-status' in status.metadata); + assert.equal(true, 'x-custom-1' in status.metadata); + assert.equal('ababab', status.metadata['x-custom-1']); + done(); + }); + }); + + it('should trigger all callbacks', function(done) { + done = multiDone(done, 3); + MockXMLHttpRequest.onSend = function(xhr) { + xhr.respond( + 400, {'Content-Type': 'application/grpc-web-text'}); + }; + var call = echoService.echo( + request, {}, + function(err, response) { + if (response) { + assert.fail('should not have received response with non-OK status'); + } else { + assert.equal(3, err.code); // http error 400 mapped to grpc error 3 + } + done(); + } + ); + call.on('status', (status) => { + assert.fail('should not have received a status callback'); + }); + call.on('error', (error) => { + assert.equal(3, error.code); + done(); + }); + call.on('error', (error) => { + assert.equal(3, error.code); + done(); + }); + }); + + it('should be able to remove callback', function(done) { + done = multiDone(done, 2); + MockXMLHttpRequest.onSend = function(xhr) { + xhr.respond( + 400, {'Content-Type': 'application/grpc-web-text'}); + }; + var call = echoService.echo( + request, {}, + function(err, response) { + if (response) { + assert.fail('should not have received response with non-OK status'); + } else { + assert.equal(3, err.code); // http error 400 mapped to grpc error 3 + } + done(); + } + ); + call.on('status', (status) => { + assert.fail('should not have received a status callback'); + }); + const callbackA = (error) => { + assert.equal(3, error.code); + done(); + }; + const callbackB = (error) => { + assert.fail('should not be called'); + } + call.on('error', callbackA); + call.on('error', callbackB); + call.removeListener('error', callbackB); + }); + +}); describe('grpc-web generated code (commonjs+dts)', function() { const protoGenCodePath = path.resolve(__dirname, './echo_pb.js');