Skip to content

Commit dc2582a

Browse files
Merge pull request #10479 from micalevisk/feat/verbose-wrong-controller-error
feat(core): display class's name on request mapping exceptions
2 parents 7ec60ca + ee82c7b commit dc2582a

File tree

7 files changed

+54
-11
lines changed

7 files changed

+54
-11
lines changed

.gitignore

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,13 @@ node_modules/
88
/.devcontainer
99
*.code-workspace
1010

11+
# Vim
12+
[._]*.s[a-v][a-z]
13+
[._]*.sw[a-p]
14+
[._]s[a-rt-v][a-z]
15+
[._]ss[a-gi-z]
16+
[._]sw[a-p]
17+
1118
# bundle
1219
packages/**/*.d.ts
1320
packages/**/*.js
Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
1+
import type { Type } from '@nestjs/common';
12
import { RuntimeException } from './runtime.exception';
23
import { UNKNOWN_REQUEST_MAPPING } from '../messages';
34

45
export class UnknownRequestMappingException extends RuntimeException {
5-
constructor() {
6-
super(UNKNOWN_REQUEST_MAPPING);
6+
constructor(metatype: Type) {
7+
super(UNKNOWN_REQUEST_MAPPING(metatype));
78
}
89
}

packages/core/errors/messages.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -172,8 +172,14 @@ export const INVALID_CLASS_SCOPE_MESSAGE = (
172172
name || 'This class'
173173
} is marked as a scoped provider. Request and transient-scoped providers can't be used in combination with "get()" method. Please, use "resolve()" instead.`;
174174

175+
export const UNKNOWN_REQUEST_MAPPING = (metatype: Type) => {
176+
const className = metatype.name;
177+
return className
178+
? `An invalid controller has been detected. "${className}" does not have the @Controller() decorator but it is being listed in the "controllers" array of some module.`
179+
: `An invalid controller has been detected. Perhaps, one of your controllers is missing the @Controller() decorator.`;
180+
};
181+
175182
export const INVALID_MIDDLEWARE_CONFIGURATION = `An invalid middleware configuration has been passed inside the module 'configure()' method.`;
176-
export const UNKNOWN_REQUEST_MAPPING = `An invalid controller has been detected. Perhaps, one of your controllers is missing @Controller() decorator.`;
177183
export const UNHANDLED_RUNTIME_EXCEPTION = `Unhandled Runtime Exception.`;
178184
export const INVALID_EXCEPTION_FILTER = `Invalid exception filters (@UseFilters()).`;
179185
export const MICROSERVICES_PACKAGE_NOT_FOUND_EXCEPTION = `Unable to load @nestjs/microservices package. (Please make sure that it's already installed.)`;

packages/core/router/router-explorer.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -116,7 +116,7 @@ export class RouterExplorer {
116116
const path = Reflect.getMetadata(PATH_METADATA, metatype);
117117

118118
if (isUndefined(path)) {
119-
throw new UnknownRequestMappingException();
119+
throw new UnknownRequestMappingException(metatype);
120120
}
121121
if (Array.isArray(path)) {
122122
return path.map(p => addLeadingSlash(p));

packages/core/test/exceptions/external-exception-filter-context.spec.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { expect } from 'chai';
22
import * as sinon from 'sinon';
3+
import { ExceptionFilter } from '../../../common';
34
import { Catch } from '../../../common/decorators/core/catch.decorator';
45
import { UseFilters } from '../../../common/decorators/core/exception-filters.decorator';
56
import { ApplicationConfig } from '../../application-config';
@@ -13,7 +14,10 @@ describe('ExternalExceptionFilterContext', () => {
1314

1415
class CustomException {}
1516
@Catch(CustomException)
16-
class ExceptionFilter {
17+
class ExceptionFilter implements ExceptionFilter {
18+
public catch(exc, res) {}
19+
}
20+
class ClassWithNoMetadata implements ExceptionFilter {
1721
public catch(exc, res) {}
1822
}
1923

@@ -59,6 +63,11 @@ describe('ExternalExceptionFilterContext', () => {
5963
exceptionFilter.reflectCatchExceptions(new ExceptionFilter()),
6064
).to.be.eql([CustomException]);
6165
});
66+
it('should return an empty array when metadata was found', () => {
67+
expect(
68+
exceptionFilter.reflectCatchExceptions(new ClassWithNoMetadata()),
69+
).to.be.eql([]);
70+
});
6271
});
6372
describe('createConcreteContext', () => {
6473
class InvalidFilter {}

packages/core/test/exceptions/external-exceptions-handler.spec.ts

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,22 +2,23 @@ import { expect } from 'chai';
22
import { of } from 'rxjs';
33
import * as sinon from 'sinon';
44
import { ExternalExceptionsHandler } from '../../exceptions/external-exceptions-handler';
5+
import { ExternalExceptionFilter } from '../../exceptions/external-exception-filter';
56

67
describe('ExternalExceptionsHandler', () => {
78
let handler: ExternalExceptionsHandler;
89

910
beforeEach(() => {
1011
handler = new ExternalExceptionsHandler();
12+
13+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
14+
// @ts-ignore The 'logger' property is private but we want to avoid showing useless error logs
15+
ExternalExceptionFilter.logger.error = () => {};
1116
});
1217

1318
describe('next', () => {
14-
it('should method returns expected stream with message when exception is unknown', async () => {
19+
it('should method returns expected stream with message when exception is unknown', () => {
1520
const error = new Error();
16-
try {
17-
await handler.next(error, null);
18-
} catch (err) {
19-
expect(err).to.be.eql(error);
20-
}
21+
expect(() => handler.next(error, null)).to.throw(error);
2122
});
2223
describe('when "invokeCustomFilters" returns value', () => {
2324
const observable$ = of(true);
@@ -49,6 +50,7 @@ describe('ExternalExceptionsHandler', () => {
4950
describe('when filters array is not empty', () => {
5051
let filters, funcSpy;
5152
class TestException {}
53+
class AnotherTestException {}
5254

5355
beforeEach(() => {
5456
funcSpy = sinon.spy();
@@ -73,6 +75,12 @@ describe('ExternalExceptionsHandler', () => {
7375
});
7476
});
7577
describe('when filter does not exists in filters array', () => {
78+
beforeEach(() => {
79+
filters = [
80+
{ exceptionMetatypes: [AnotherTestException], func: funcSpy },
81+
];
82+
(handler as any).filters = filters;
83+
});
7684
it('should not call funcSpy', () => {
7785
handler.invokeCustomFilters(new TestException(), null);
7886
expect(funcSpy.notCalled).to.be.true;

packages/core/test/router/router-explorer.spec.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import { RoutePathFactory } from '../../router/route-path-factory';
1919
import { RouterExceptionFilters } from '../../router/router-exception-filters';
2020
import { RouterExplorer } from '../../router/router-explorer';
2121
import { NoopHttpAdapter } from '../utils/noop-adapter.spec';
22+
import { UnknownRequestMappingException } from '../../errors/exceptions/unknown-request-mapping.exception';
2223

2324
describe('RouterExplorer', () => {
2425
@Controller('global')
@@ -51,6 +52,8 @@ describe('RouterExplorer', () => {
5152
public getTestUsingArray() {}
5253
}
5354

55+
class ClassWithMissingControllerDecorator {}
56+
5457
let routerBuilder: RouterExplorer;
5558
let injector: Injector;
5659
let exceptionsFilter: RouterExceptionFilters;
@@ -315,6 +318,15 @@ describe('RouterExplorer', () => {
315318
'/global-alias',
316319
]);
317320
});
321+
322+
it("should throw UnknownRequestMappingException when missing the `@Controller()` decorator in the class, displaying class's name", () => {
323+
expect(() =>
324+
routerBuilder.extractRouterPath(ClassWithMissingControllerDecorator),
325+
).to.throw(
326+
UnknownRequestMappingException,
327+
/ClassWithMissingControllerDecorator/,
328+
);
329+
});
318330
});
319331

320332
describe('createRequestScopedHandler', () => {

0 commit comments

Comments
 (0)