Merge pull request #13368 from ssilve1989/refactor/cleanup-grpc-call-handling

refactor(microservices): prevent grpc write promise from throwing
This commit is contained in:
Kamil Mysliwiec
2024-06-03 13:04:40 +02:00
committed by GitHub
5 changed files with 104 additions and 25 deletions

View File

@@ -4,11 +4,15 @@ import { INestApplication } from '@nestjs/common';
import { MicroserviceOptions, Transport } from '@nestjs/microservices';
import { Test } from '@nestjs/testing';
import { fail } from 'assert';
import { expect } from 'chai';
import { expect, use } from 'chai';
import * as chaiAsPromised from 'chai-as-promised';
import { join } from 'path';
import * as sinon from 'sinon';
import * as request from 'supertest';
import { GrpcController } from '../src/grpc/grpc.controller';
use(chaiAsPromised);
describe('GRPC transport', () => {
let server;
let app: INestApplication;
@@ -32,6 +36,7 @@ describe('GRPC transport', () => {
],
},
});
// Start gRPC microservice
await app.startAllMicroservices();
await app.init();
@@ -149,6 +154,50 @@ describe('GRPC transport', () => {
expect(receivedIds).to.deep.equal(expectedIds);
});
describe('streaming calls that error', () => {
// We want to assert that the application does not crash when an error is encountered with an unhandledRejection
// the best way to do that is to listen for the unhandledRejection event and fail the test if it is called
let processSpy: sinon.SinonSpy;
beforeEach(() => {
processSpy = sinon.spy();
process.on('unhandledRejection', processSpy);
});
afterEach(() => {
process.off('unhandledRejection', processSpy);
});
it('should not crash when replying with an error', async () => {
const call = new Promise<void>((resolve, reject) => {
const stream = client.streamDivide({
data: [{ dividend: 1, divisor: 0 }],
});
stream.on('data', () => {
fail('Stream should not have emitted any data');
});
stream.on('error', err => {
if (err.code !== GRPC.status.CANCELLED) {
reject(err);
}
});
stream.on('end', () => {
resolve();
});
});
await expect(call).to.eventually.be.rejectedWith(
'3 INVALID_ARGUMENT: dividing by 0 is not possible',
);
// if this fails the application has crashed
expect(processSpy.called).to.be.false;
});
});
after(async () => {
await app.close();
});

View File

@@ -10,7 +10,7 @@ import {
RpcException,
} from '@nestjs/microservices';
import { join } from 'path';
import { Observable, of, catchError } from 'rxjs';
import { Observable, of, catchError, from, mergeMap } from 'rxjs';
class ErrorHandlingProxy extends ClientGrpcProxy {
serializeError(err) {
@@ -107,6 +107,17 @@ export class GrpcController {
};
}
// contrived example meant to show when an error is encountered, like dividing by zero, the
// application does not crash and the error is returned appropriately to the client
@GrpcMethod('Math', 'StreamDivide')
streamDivide({
data,
}: {
data: { dividend: number; divisor: number }[];
}): Observable<any> {
return from(data).pipe(mergeMap(request => this.divide(request)));
}
@GrpcMethod('Math2')
async sum2({ data }: { data: number[] }): Promise<any> {
return of({

View File

@@ -7,7 +7,9 @@ service Math {
rpc SumStream(stream RequestSum) returns(stream SumResult);
rpc SumStreamPass(stream RequestSum) returns(stream SumResult);
rpc Divide (RequestDivide) returns (DivideResult);
rpc StreamLargeMessages(Empty) returns (stream BackpressureData) {}
rpc StreamLargeMessages(Empty) returns (stream BackpressureData);
/* Given a series of dividend and divisor, stream back the division results for each */
rpc StreamDivide (StreamDivideRequest) returns (stream StreamDivideResponse);
}
message BackpressureData {
@@ -33,3 +35,11 @@ message RequestDivide {
message DivideResult {
int32 result = 1;
}
message StreamDivideRequest {
repeated RequestDivide data = 1;
}
message StreamDivideResponse {
DivideResult data = 1;
}