feat: pass http status code to RpcError

This commit is contained in:
wszydlak 2025-02-05 13:23:52 +01:00
parent b5ff5d303d
commit b84dc9c1bc
4 changed files with 111 additions and 11 deletions

View File

@ -194,6 +194,27 @@ testSuite({
assertEquals(3, error.code); 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() { async testRpcDeserializationError() {
const xhr = new XhrIo(); const xhr = new XhrIo();
const client = new GrpcWebClientBase(/* options= */ {}, xhr); const client = new GrpcWebClientBase(/* options= */ {}, xhr);

View File

@ -136,9 +136,8 @@ class GrpcWebClientReadableStream {
const self = this; const self = this;
events.listen(this.xhr_, EventType.READY_STATE_CHANGE, function(e) { events.listen(this.xhr_, EventType.READY_STATE_CHANGE, function(e) {
let contentType = self.xhr_.getStreamingResponseHeader('Content-Type'); const contentType = (self.xhr_.getStreamingResponseHeader('Content-Type') || '').toLowerCase();
if (!contentType) return; if (!contentType.startsWith('application/grpc')) return;
contentType = contentType.toLowerCase();
let byteSource; let byteSource;
if (googString.startsWith(contentType, 'application/grpc-web-text')) { if (googString.startsWith(contentType, 'application/grpc-web-text')) {
@ -152,11 +151,8 @@ class GrpcWebClientReadableStream {
} else if (googString.startsWith(contentType, 'application/grpc')) { } else if (googString.startsWith(contentType, 'application/grpc')) {
byteSource = new Uint8Array( byteSource = new Uint8Array(
/** @type {!ArrayBuffer} */ (self.xhr_.getResponse())); /** @type {!ArrayBuffer} */ (self.xhr_.getResponse()));
} else {
self.handleError_(
new RpcError(StatusCode.UNKNOWN, 'Unknown Content-type received.'));
return;
} }
let messages = null; let messages = null;
try { try {
messages = self.parser_.parse(byteSource); messages = self.parser_.parse(byteSource);
@ -257,11 +253,14 @@ class GrpcWebClientReadableStream {
return; return;
} }
let errorMessage = ErrorCode.getDebugMessage(lastErrorCode); let errorMessage = ErrorCode.getDebugMessage(lastErrorCode);
const errorMetadata = /** @type {!StatusMetadata} */ ({});
if (xhrStatusCode != -1) { if (xhrStatusCode != -1) {
errorMessage += ', http status code: ' + xhrStatusCode; errorMessage += ', http status code: ' + xhrStatusCode;
errorMetadata['httpStatusCode'] = xhrStatusCode;
} }
self.handleError_(new RpcError(grpcStatusCode, errorMessage)); self.handleError_(new RpcError(grpcStatusCode, errorMessage, errorMetadata));
return; return;
} }

View File

@ -1,6 +1,9 @@
declare module "grpc-web" { declare module "grpc-web" {
export interface Metadata { [s: string]: string; } export interface Metadata { [s: string]: string; }
export type StatusMetadata = Metadata & {
httpStatusCode?: number
};
export class AbstractClientBase { export class AbstractClientBase {
thenableCall<REQ, RESP> ( thenableCall<REQ, RESP> (
@ -105,15 +108,15 @@ declare module "grpc-web" {
} }
export class RpcError extends Error { export class RpcError extends Error {
constructor(code: StatusCode, message: string, metadata: Metadata); constructor(code: StatusCode, message: string, metadata: StatusMetadata);
code: StatusCode; code: StatusCode;
metadata: Metadata; metadata: StatusMetadata;
} }
export interface Status { export interface Status {
code: number; code: number;
details: string; details: string;
metadata?: Metadata; metadata?: StatusMetadata;
} }
export enum StatusCode { export enum StatusCode {

View File

@ -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) { it('should receive error', function(done) {
const {EchoServicePromiseClient} = require(genCodePath); const {EchoServicePromiseClient} = require(genCodePath);
const {EchoRequest} = require(protoGenCodePath); 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) { it('should receive error, on http error', function(done) {
done = multiDone(done, 2); done = multiDone(done, 2);
MockXMLHttpRequest.onSend = function(xhr) { 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) { it('should receive error, on http error (streaming)', function(done) {
done = multiDone(done, 2); done = multiDone(done, 2);
MockXMLHttpRequest.onSend = function(xhr) { MockXMLHttpRequest.onSend = function(xhr) {