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);
},
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);

View File

@ -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;
}

View File

@ -1,6 +1,9 @@
declare module "grpc-web" {
export interface Metadata { [s: string]: string; }
export type StatusMetadata = Metadata & {
httpStatusCode?: number
};
export class AbstractClientBase {
thenableCall<REQ, RESP> (
@ -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 {

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) {
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) {