Skip to content

Commit d25e6c7

Browse files
Merge pull request nestjs#1346 from nestjs/bugfix/1285-throw-error
bugfix(ws/microservices): add catchError() to observables
2 parents ed37b91 + efb367e commit d25e6c7

8 files changed

Lines changed: 177 additions & 22 deletions

File tree

integration/microservices/e2e/sum-nats.spec.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,15 @@ describe('NATS transport', () => {
7373
.expect(200, 'true');
7474
});
7575

76+
it(`/GET (exception)`, () => {
77+
return request(server)
78+
.get('/exception')
79+
.expect(200, {
80+
message: 'test',
81+
status: 'error',
82+
});
83+
});
84+
7685
afterEach(async () => {
7786
await app.close();
7887
});

integration/microservices/src/nats/nats.controller.ts

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
1-
import { Body, Controller, HttpCode, Post, Query } from '@nestjs/common';
1+
import { Body, Controller, Get, HttpCode, Post, Query } from '@nestjs/common';
22
import {
33
Client,
44
ClientProxy,
55
MessagePattern,
6+
RpcException,
67
Transport,
78
} from '@nestjs/microservices';
8-
import { from, Observable, of } from 'rxjs';
9-
import { scan } from 'rxjs/operators';
9+
import { from, Observable, of, throwError } from 'rxjs';
10+
import { catchError, scan } from 'rxjs/operators';
1011

1112
@Controller()
1213
export class NatsController {
@@ -71,4 +72,16 @@ export class NatsController {
7172
streaming(data: number[]): Observable<number> {
7273
return from(data);
7374
}
75+
76+
@Get('exception')
77+
async getError() {
78+
return await this.client
79+
.send<number>('exception', {})
80+
.pipe(catchError(err => of(err)));
81+
}
82+
83+
@MessagePattern('exception')
84+
throwError(): Observable<number> {
85+
return throwError(new RpcException('test'));
86+
}
7487
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import { INestApplication } from '@nestjs/common';
2+
import { Test } from '@nestjs/testing';
3+
import { expect } from 'chai';
4+
import * as io from 'socket.io-client';
5+
import { ErrorGateway } from '../src/error.gateway';
6+
7+
describe('ErrorGateway', () => {
8+
let app: INestApplication;
9+
10+
beforeEach(async () => {
11+
const testingModule = await Test.createTestingModule({
12+
providers: [ErrorGateway],
13+
}).compile();
14+
app = await testingModule.createNestApplication();
15+
await app.listenAsync(3000);
16+
});
17+
18+
it(`should handle error`, async () => {
19+
const ws = io.connect('http://localhost:8080');
20+
ws.emit('push', {
21+
test: 'test',
22+
});
23+
await new Promise(resolve =>
24+
ws.on('exception', data => {
25+
expect(data).to.be.eql({
26+
status: 'error',
27+
message: 'test',
28+
});
29+
resolve();
30+
}),
31+
);
32+
});
33+
34+
afterEach(() => app.close());
35+
});
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import {
2+
SubscribeMessage,
3+
WebSocketGateway,
4+
WsException,
5+
} from '@nestjs/websockets';
6+
import { throwError } from 'rxjs';
7+
8+
@WebSocketGateway(8080)
9+
export class ErrorGateway {
10+
@SubscribeMessage('push')
11+
onPush(client, data) {
12+
return throwError(new WsException('test'));
13+
}
14+
}
Lines changed: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
1+
import { isFunction } from '@nestjs/common/utils/shared.utils';
12
import { ExecutionContextHost } from '@nestjs/core/helpers/execution-context.host';
23
import { Observable } from 'rxjs';
4+
import { catchError } from 'rxjs/operators';
35
import { RpcExceptionsHandler } from '../exceptions/rpc-exceptions-handler';
46

57
export class RpcProxy {
@@ -9,11 +11,30 @@ export class RpcProxy {
911
): (...args) => Promise<Observable<any>> {
1012
return async (...args) => {
1113
try {
12-
return await targetCallback(...args);
13-
} catch (e) {
14-
const host = new ExecutionContextHost(args);
15-
return exceptionsHandler.handle(e, host);
14+
const result = await targetCallback(...args);
15+
return !this.isObservable(result)
16+
? result
17+
: result.pipe(
18+
catchError(error =>
19+
this.handleError(exceptionsHandler, args, error),
20+
),
21+
);
22+
} catch (error) {
23+
return this.handleError(exceptionsHandler, args, error);
1624
}
1725
};
1826
}
27+
28+
handleError<T>(
29+
exceptionsHandler: RpcExceptionsHandler,
30+
args: any[],
31+
error: T,
32+
): Observable<any> {
33+
const host = new ExecutionContextHost(args);
34+
return exceptionsHandler.handle(error, host);
35+
}
36+
37+
isObservable(result: any): boolean {
38+
return result && isFunction(result.subscribe);
39+
}
1940
}

packages/microservices/test/context/rpc-proxy.spec.ts

Lines changed: 25 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
1-
import * as sinon from 'sinon';
21
import { expect } from 'chai';
2+
import { of, throwError } from 'rxjs';
3+
import * as sinon from 'sinon';
34
import { RpcProxy } from '../../context/rpc-proxy';
4-
import { RpcExceptionsHandler } from '../../exceptions/rpc-exceptions-handler';
55
import { RpcException } from '../../exceptions/rpc-exception';
6-
import { Observable, of } from 'rxjs';
6+
import { RpcExceptionsHandler } from '../../exceptions/rpc-exceptions-handler';
77

88
describe('RpcProxy', () => {
99
let routerProxy: RpcProxy;
@@ -18,10 +18,7 @@ describe('RpcProxy', () => {
1818

1919
describe('create', () => {
2020
it('should method return thunk', async () => {
21-
const proxy = await routerProxy.create(
22-
async data => of(true),
23-
handler,
24-
);
21+
const proxy = await routerProxy.create(async data => of(true), handler);
2522
expect(typeof proxy === 'function').to.be.true;
2623
});
2724

@@ -33,5 +30,26 @@ describe('RpcProxy', () => {
3330
await proxy(null);
3431
expectation.verify();
3532
});
33+
34+
it('should attach "catchError" operator when observable was returned', async () => {
35+
const expectation = handlerMock.expects('handle').once();
36+
const proxy = routerProxy.create(async (client, data) => {
37+
return throwError(new RpcException('test'));
38+
}, handler);
39+
(await proxy(null, null)).subscribe(null, () => expectation.verify());
40+
});
41+
});
42+
43+
describe('isObservable', () => {
44+
describe('when observable', () => {
45+
it('should return true', () => {
46+
expect(routerProxy.isObservable(of('test'))).to.be.true;
47+
});
48+
});
49+
describe('when not observable', () => {
50+
it('should return false', () => {
51+
expect(routerProxy.isObservable({})).to.be.false;
52+
});
53+
});
3654
});
3755
});
Lines changed: 29 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,41 @@
1+
import { isFunction } from '@nestjs/common/utils/shared.utils';
12
import { ExecutionContextHost } from '@nestjs/core/helpers/execution-context.host';
3+
import { empty } from 'rxjs';
4+
import { catchError } from 'rxjs/operators';
25
import { WsExceptionsHandler } from '../exceptions/ws-exceptions-handler';
36

47
export class WsProxy {
58
public create(
6-
targetCallback: (...args) => Promise<void>,
9+
targetCallback: (...args) => Promise<any>,
710
exceptionsHandler: WsExceptionsHandler,
8-
): (...args) => Promise<void> {
11+
): (...args) => Promise<any> {
912
return async (...args) => {
1013
try {
11-
return await targetCallback(...args);
12-
} catch (e) {
13-
const host = new ExecutionContextHost(args);
14-
exceptionsHandler.handle(e, host);
14+
const result = await targetCallback(...args);
15+
return !this.isObservable(result)
16+
? result
17+
: result.pipe(
18+
catchError(error => {
19+
this.handleError(exceptionsHandler, args, error);
20+
return empty();
21+
}),
22+
);
23+
} catch (error) {
24+
this.handleError(exceptionsHandler, args, error);
1525
}
1626
};
1727
}
28+
29+
handleError<T>(
30+
exceptionsHandler: WsExceptionsHandler,
31+
args: any[],
32+
error: T,
33+
) {
34+
const host = new ExecutionContextHost(args);
35+
exceptionsHandler.handle(error, host);
36+
}
37+
38+
isObservable(result: any): boolean {
39+
return result && isFunction(result.subscribe);
40+
}
1841
}

packages/websockets/test/context/ws-proxy.spec.ts

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
1-
import * as sinon from 'sinon';
21
import { expect } from 'chai';
2+
import { of, throwError } from 'rxjs';
3+
import * as sinon from 'sinon';
34
import { WsProxy } from '../../context/ws-proxy';
4-
import { WsExceptionsHandler } from '../../exceptions/ws-exceptions-handler';
55
import { WsException } from '../../exceptions/ws-exception';
6+
import { WsExceptionsHandler } from '../../exceptions/ws-exceptions-handler';
67

78
describe('WsProxy', () => {
89
let routerProxy: WsProxy;
@@ -30,5 +31,26 @@ describe('WsProxy', () => {
3031
await proxy(null, null);
3132
expectation.verify();
3233
});
34+
35+
it('should attach "catchError" operator when observable was returned', async () => {
36+
const expectation = handlerMock.expects('handle').once();
37+
const proxy = routerProxy.create(async (client, data) => {
38+
return throwError(new WsException('test'));
39+
}, handler);
40+
(await proxy(null, null)).subscribe(null, () => expectation.verify());
41+
});
42+
});
43+
44+
describe('isObservable', () => {
45+
describe('when observable', () => {
46+
it('should return true', () => {
47+
expect(routerProxy.isObservable(of('test'))).to.be.true;
48+
});
49+
});
50+
describe('when not observable', () => {
51+
it('should return false', () => {
52+
expect(routerProxy.isObservable({})).to.be.false;
53+
});
54+
});
3355
});
3456
});

0 commit comments

Comments
 (0)