From b84dc9c1bc6da00f6bf5fd760cd74ba0762494e1 Mon Sep 17 00:00:00 2001 From: wszydlak Date: Wed, 5 Feb 2025 13:23:52 +0100 Subject: [PATCH] feat: pass http status code to RpcError --- .../net/grpc/web/grpcwebclientbase_test.js | 21 +++++ .../grpc/web/grpcwebclientreadablestream.js | 15 ++-- packages/grpc-web/index.d.ts | 9 ++- packages/grpc-web/test/generated_code_test.js | 77 +++++++++++++++++++ 4 files changed, 111 insertions(+), 11 deletions(-) diff --git a/javascript/net/grpc/web/grpcwebclientbase_test.js b/javascript/net/grpc/web/grpcwebclientbase_test.js index e361440..c5f593c 100644 --- a/javascript/net/grpc/web/grpcwebclientbase_test.js +++ b/javascript/net/grpc/web/grpcwebclientbase_test.js @@ -194,6 +194,27 @@ testSuite({ assertEquals(3, error.code); }, + async testRpcErrorWithHttpStatusCode() { + const xhr = new XhrIo(); + const client = new GrpcWebClientBase(/* options= */ {}, xhr); + const methodDescriptor = createMethodDescriptor((bytes) => new MockReply()); + + const error = await new Promise((resolve, reject) => { + client.rpcCall( + 'urlurl', new MockRequest(), /* metadata= */ {}, methodDescriptor, + (error, response) => { + assertNull(response); + resolve(error); + }); + // This decodes to "grpc-status: 3" + xhr.simulateResponse(505, '', {'Content-Type': 'text/html'}); + }); + assertTrue(error instanceof RpcError); + assert('metadata' in error); + assert('httpStatusCode' in error.metadata); + assertEquals(505, error.metadata.httpStatusCode); + }, + async testRpcDeserializationError() { const xhr = new XhrIo(); const client = new GrpcWebClientBase(/* options= */ {}, xhr); diff --git a/javascript/net/grpc/web/grpcwebclientreadablestream.js b/javascript/net/grpc/web/grpcwebclientreadablestream.js index 554914f..62b3317 100644 --- a/javascript/net/grpc/web/grpcwebclientreadablestream.js +++ b/javascript/net/grpc/web/grpcwebclientreadablestream.js @@ -136,9 +136,8 @@ class GrpcWebClientReadableStream { const self = this; events.listen(this.xhr_, EventType.READY_STATE_CHANGE, function(e) { - let contentType = self.xhr_.getStreamingResponseHeader('Content-Type'); - if (!contentType) return; - contentType = contentType.toLowerCase(); + const contentType = (self.xhr_.getStreamingResponseHeader('Content-Type') || '').toLowerCase(); + if (!contentType.startsWith('application/grpc')) return; let byteSource; if (googString.startsWith(contentType, 'application/grpc-web-text')) { @@ -152,11 +151,8 @@ class GrpcWebClientReadableStream { } else if (googString.startsWith(contentType, 'application/grpc')) { byteSource = new Uint8Array( /** @type {!ArrayBuffer} */ (self.xhr_.getResponse())); - } else { - self.handleError_( - new RpcError(StatusCode.UNKNOWN, 'Unknown Content-type received.')); - return; } + let messages = null; try { messages = self.parser_.parse(byteSource); @@ -257,11 +253,14 @@ class GrpcWebClientReadableStream { return; } let errorMessage = ErrorCode.getDebugMessage(lastErrorCode); + + const errorMetadata = /** @type {!StatusMetadata} */ ({}); if (xhrStatusCode != -1) { errorMessage += ', http status code: ' + xhrStatusCode; + errorMetadata['httpStatusCode'] = xhrStatusCode; } - self.handleError_(new RpcError(grpcStatusCode, errorMessage)); + self.handleError_(new RpcError(grpcStatusCode, errorMessage, errorMetadata)); return; } diff --git a/packages/grpc-web/index.d.ts b/packages/grpc-web/index.d.ts index 09fb671..9886c6f 100644 --- a/packages/grpc-web/index.d.ts +++ b/packages/grpc-web/index.d.ts @@ -1,6 +1,9 @@ declare module "grpc-web" { export interface Metadata { [s: string]: string; } + export type StatusMetadata = Metadata & { + httpStatusCode?: number + }; export class AbstractClientBase { thenableCall ( @@ -105,15 +108,15 @@ declare module "grpc-web" { } export class RpcError extends Error { - constructor(code: StatusCode, message: string, metadata: Metadata); + constructor(code: StatusCode, message: string, metadata: StatusMetadata); code: StatusCode; - metadata: Metadata; + metadata: StatusMetadata; } export interface Status { code: number; details: string; - metadata?: Metadata; + metadata?: StatusMetadata; } export enum StatusCode { diff --git a/packages/grpc-web/test/generated_code_test.js b/packages/grpc-web/test/generated_code_test.js index b7622be..e25b41a 100644 --- a/packages/grpc-web/test/generated_code_test.js +++ b/packages/grpc-web/test/generated_code_test.js @@ -118,6 +118,29 @@ describe('grpc-web generated code: promise-based client', function() { }); }); + it('should receive error, on http error - Content-Type not matching application/grpc*', function(done) { + const {EchoServicePromiseClient} = require(genCodePath); + const {EchoRequest} = require(protoGenCodePath); + MockXMLHttpRequest.onSend = function(xhr) { + xhr.respond( + 505, {'Content-Type': 'text/html'}); + }; + var echoService = new EchoServicePromiseClient('MyHostname', null, null); + var request = new EchoRequest(); + request.setMessage('aaa'); + + echoService.echo(request, {}) + .then((response) => { + assert.fail('should not receive response'); + }) + .catch((error) => { + assert('metadata' in error); + assert('httpStatusCode' in error.metadata); + assert.equal(505, error.metadata.httpStatusCode); + done(); + }); + }); + it('should receive error', function(done) { const {EchoServicePromiseClient} = require(genCodePath); const {EchoRequest} = require(protoGenCodePath); @@ -613,6 +636,36 @@ describe('grpc-web generated code: callbacks tests', function() { }); }); + it('should receive error, on http error - Content-Type not matching application/grpc*', function(done) { + done = multiDone(done, 2); + MockXMLHttpRequest.onSend = function(xhr) { + xhr.respond( + 505, {'Content-Type': 'text/html'}); + }; + var call = echoService.echo( + request, {}, + function(err, response) { + if (response) { + assert.fail('should not have received response with non-OK status'); + } else { + assert('metadata' in err); + assert('httpStatusCode' in err.metadata); + assert.equal(505, err.metadata.httpStatusCode); + } + done(); + } + ); + call.on('status', (status) => { + assert('metadata' in status); + assert('httpStatusCode' in status.metadata); + assert.equal(505, status.metadata.httpStatusCode); + done(); + }); + call.on('error', (error) => { + assert.fail('error callback should not be called for unary calls'); + }); + }); + it('should receive error, on http error', function(done) { done = multiDone(done, 2); MockXMLHttpRequest.onSend = function(xhr) { @@ -802,6 +855,30 @@ describe('grpc-web generated code: callbacks tests', function() { }); }); + it('should receive error, on http error (streaming) - Content-Type not matching application/grpc*', function(done) { + done = multiDone(done, 2); + MockXMLHttpRequest.onSend = function(xhr) { + xhr.respond( + 505, {'Content-Type': 'text/html'}); + }; + var call = echoService.serverStreamingEcho(request, {}); + call.on('data', (response) => { + assert.fail('should not receive data response'); + }); + call.on('status', (status) => { + assert('metadata' in status); + assert('httpStatusCode' in status.metadata); + assert.equal(505, status.metadata.httpStatusCode); + done(); + }); + call.on('error', (error) => { + assert('metadata' in error); + assert('httpStatusCode' in error.metadata); + assert.equal(505, error.metadata.httpStatusCode); + done(); + }); + }); + it('should receive error, on http error (streaming)', function(done) { done = multiDone(done, 2); MockXMLHttpRequest.onSend = function(xhr) {