diff --git a/integration/hello-world/e2e/express-multiple.spec.ts b/integration/hello-world/e2e/express-multiple.spec.ts index 62516e0185a..3020cad2fe4 100644 --- a/integration/hello-world/e2e/express-multiple.spec.ts +++ b/integration/hello-world/e2e/express-multiple.spec.ts @@ -16,21 +16,25 @@ describe('Hello world (express instance with multiple applications)', () => { const module2 = await Test.createTestingModule({ imports: [ApplicationModule], }).compile(); + const module3 = await Test.createTestingModule({ + imports: [ApplicationModule], + }).compile(); const adapter = new ExpressAdapter(express()); apps = [ - module1.createNestApplication(adapter), + module1.createNestApplication(adapter).setGlobalPrefix('app1'), module2.createNestApplication(adapter).setGlobalPrefix('/app2'), + module3.createNestApplication(adapter), ]; await Promise.all(apps.map(app => app.init())); server = adapter.getInstance(); }); - it(`/GET`, () => { + it(`/GET (app1)`, () => { return request(server) - .get('/hello') + .get('/app1/hello') .expect(200) .expect('Hello world!'); }); @@ -42,9 +46,9 @@ describe('Hello world (express instance with multiple applications)', () => { .expect('Hello world!'); }); - it(`/GET (Promise/async)`, () => { + it(`/GET (app1 Promise/async)`, () => { return request(server) - .get('/hello/async') + .get('/app1/hello/async') .expect(200) .expect('Hello world!'); }); @@ -56,9 +60,9 @@ describe('Hello world (express instance with multiple applications)', () => { .expect('Hello world!'); }); - it(`/GET (Observable stream)`, () => { + it(`/GET (app1 Observable stream)`, () => { return request(server) - .get('/hello/stream') + .get('/app1/hello/stream') .expect(200) .expect('Hello world!'); }); @@ -70,6 +74,39 @@ describe('Hello world (express instance with multiple applications)', () => { .expect('Hello world!'); }); + it(`/GET (app1 NotFound)`, () => { + return request(server) + .get('/app1/cats') + .expect(404) + .expect({ + statusCode: 404, + error: 'Not Found', + message: 'Cannot GET /cats', + }); + }); + + it(`/GET (app2 NotFound)`, () => { + return request(server) + .get('/app2/cats') + .expect(404) + .expect({ + statusCode: 404, + error: 'Not Found', + message: 'Cannot GET /cats', + }); + }); + + it(`/GET (app3 NotFound)`, () => { + return request(server) + .get('/app3/cats') + .expect(404) + .expect({ + statusCode: 404, + error: 'Not Found', + message: 'Cannot GET /app3/cats', + }); + }); + afterEach(async () => { await Promise.all(apps.map(app => app.close())); }); diff --git a/integration/hello-world/e2e/fastify-multiple.spec.ts b/integration/hello-world/e2e/fastify-multiple.spec.ts index d8dc60b1043..714159d0fbd 100644 --- a/integration/hello-world/e2e/fastify-multiple.spec.ts +++ b/integration/hello-world/e2e/fastify-multiple.spec.ts @@ -1,8 +1,7 @@ -/* Temporarily disabled due to various regressions - import { FastifyAdapter, NestFastifyApplication } from '@nestjs/platform-fastify'; import { Test } from '@nestjs/testing'; import { expect } from 'chai'; +import { fail } from 'assert'; import { ApplicationModule } from '../src/app.module'; describe('Hello world (fastify adapter with multiple applications)', () => { @@ -16,25 +15,29 @@ describe('Hello world (fastify adapter with multiple applications)', () => { const module2 = await Test.createTestingModule({ imports: [ApplicationModule], }).compile(); + const module3 = await Test.createTestingModule({ + imports: [ApplicationModule], + }).compile(); adapter = new FastifyAdapter(); apps = [ - module1.createNestApplication(adapter), + module1.createNestApplication(adapter).setGlobalPrefix('app1'), module2 .createNestApplication(adapter, { bodyParser: false, }) .setGlobalPrefix('/app2'), + module3.createNestApplication(adapter), ]; await Promise.all(apps.map(app => app.init())); }); - it(`/GET`, () => { + it(`/GET (app1)`, () => { return adapter .inject({ method: 'GET', - url: '/hello', + url: '/app1/hello', }) .then(({ payload }) => expect(payload).to.be.eql('Hello world!')); }); @@ -48,11 +51,11 @@ describe('Hello world (fastify adapter with multiple applications)', () => { .then(({ payload }) => expect(payload).to.be.eql('Hello world!')); }); - it(`/GET (Promise/async)`, () => { + it(`/GET (app1 Promise/async)`, () => { return adapter .inject({ method: 'GET', - url: '/hello/async', + url: '/app1/hello/async', }) .then(({ payload }) => expect(payload).to.be.eql('Hello world!')); }); @@ -66,11 +69,11 @@ describe('Hello world (fastify adapter with multiple applications)', () => { .then(({ payload }) => expect(payload).to.be.eql('Hello world!')); }); - it(`/GET (Observable stream)`, () => { + it(`/GET (app1 Observable stream)`, () => { return adapter .inject({ method: 'GET', - url: '/hello/stream', + url: '/app1/hello/stream', }) .then(({ payload }) => expect(payload).to.be.eql('Hello world!')); }); @@ -84,8 +87,59 @@ describe('Hello world (fastify adapter with multiple applications)', () => { .then(({ payload }) => expect(payload).to.be.eql('Hello world!')); }); + it(`/GET (app1 NotFound)`, () => { + return adapter + .inject({ + method: 'GET', + url: '/app1/cats', + }) + .then( + ({ payload }) => { + expect(payload).to.be.eql(JSON.stringify({ + statusCode: 404, + error: 'Not Found', + message: 'Cannot GET /app1/cats', + })); + }, + ); + }); + + it(`/GET (app2 NotFound)`, () => { + return adapter + .inject({ + method: 'GET', + url: '/app2/cats', + }) + .then( + ({ payload }) => { + expect(payload).to.be.eql(JSON.stringify({ + statusCode: 404, + error: 'Not Found', + message: 'Cannot GET /app2/cats', + })); + }, + ); + }); + + it(`/GET (app3 NotFound)`, () => { + return adapter + .inject({ + method: 'GET', + url: '/app3/cats', + }) + .then( + ({ payload }) => { + expect(payload).to.be.eql(JSON.stringify({ + statusCode: 404, + error: 'Not Found', + message: 'Cannot GET /app3/cats', + })); + }, + ); + }); + afterEach(async () => { await Promise.all(apps.map(app => app.close())); await adapter.close(); }); -});*/ +}); diff --git a/packages/common/interfaces/http/http-server.interface.ts b/packages/common/interfaces/http/http-server.interface.ts index b8107b31673..835444e7fed 100644 --- a/packages/common/interfaces/http/http-server.interface.ts +++ b/packages/common/interfaces/http/http-server.interface.ts @@ -59,8 +59,8 @@ export interface HttpServer { getRequestMethod?(request: TRequest): string; getRequestUrl?(request: TResponse): string; getInstance(): any; - registerParserMiddleware(): any; - enableCors(options: CorsOptions): any; + registerParserMiddleware(prefix?: string): any; + enableCors(options: CorsOptions, prefix?: string): any; getHttpServer(): any; initHttpServer(options: NestApplicationOptions): void; close(): any; diff --git a/packages/core/adapters/http-adapter.ts b/packages/core/adapters/http-adapter.ts index 8039dda3989..2b10295aa45 100644 --- a/packages/core/adapters/http-adapter.ts +++ b/packages/core/adapters/http-adapter.ts @@ -92,6 +92,7 @@ export abstract class AbstractHttpAdapter< abstract redirect(response, statusCode: number, url: string); abstract setErrorHandler(handler: Function, prefix?: string); abstract setNotFoundHandler(handler: Function, prefix?: string); + abstract setRootNotFoundHandler(handler: Function); abstract setHeader(response, name: string, value: string); abstract registerParserMiddleware(prefix?: string); abstract enableCors(options: CorsOptions, prefix?: string); @@ -99,4 +100,7 @@ export abstract class AbstractHttpAdapter< requestMethod: RequestMethod, ): (path: string, callback: Function) => any; abstract getType(): string; + abstract addNestInstanceBaseUrl(baseUrl?: string): void; + abstract getNotFoundCallback(baseUrl?: string); + abstract getRootNotFoundCallback(); } diff --git a/packages/core/nest-application.ts b/packages/core/nest-application.ts index 160ff2f8904..9c70500ed7e 100644 --- a/packages/core/nest-application.ts +++ b/packages/core/nest-application.ts @@ -140,7 +140,8 @@ export class NestApplication extends NestApplicationContext const useBodyParser = this.appOptions && this.appOptions.bodyParser !== false; - useBodyParser && this.registerParserMiddleware(); + useBodyParser && + this.registerParserMiddleware(this.config.getGlobalPrefix()); await this.registerModules(); await this.registerRouter(); @@ -153,8 +154,8 @@ export class NestApplication extends NestApplicationContext return this; } - public registerParserMiddleware() { - this.httpAdapter.registerParserMiddleware(); + public registerParserMiddleware(prefix?: string) { + this.httpAdapter.registerParserMiddleware(prefix); } public async registerRouter() { @@ -216,7 +217,7 @@ export class NestApplication extends NestApplicationContext } public enableCors(options?: CorsOptions): void { - this.httpAdapter.enableCors(options); + this.httpAdapter.enableCors(options, this.config.getGlobalPrefix()); } public async listen( diff --git a/packages/core/router/routes-resolver.ts b/packages/core/router/routes-resolver.ts index 737e1bf8061..52236ce4708 100644 --- a/packages/core/router/routes-resolver.ts +++ b/packages/core/router/routes-resolver.ts @@ -78,15 +78,25 @@ export class RoutesResolver implements Resolver { public registerNotFoundHandler() { const applicationRef = this.container.getHttpAdapterRef(); - const callback = (req: TRequest, res: TResponse) => { - const method = applicationRef.getRequestMethod(req); - const url = applicationRef.getRequestUrl(req); - throw new NotFoundException(`Cannot ${method} ${url}`); - }; + applicationRef.addNestInstanceBaseUrl(this.config.getGlobalPrefix()); + + const callback = applicationRef.getNotFoundCallback( + this.config.getGlobalPrefix(), + ); const handler = this.routerExceptionsFilter.create({}, callback, undefined); const proxy = this.routerProxy.createProxy(callback, handler); applicationRef.setNotFoundHandler && applicationRef.setNotFoundHandler(proxy, this.config.getGlobalPrefix()); + + const rootCallback = applicationRef.getRootNotFoundCallback(); + const rootHandler = this.routerExceptionsFilter.create( + {}, + rootCallback, + undefined, + ); + const rootProxy = this.routerProxy.createProxy(rootCallback, rootHandler); + applicationRef.setRootNotFoundHandler && + applicationRef.setRootNotFoundHandler(rootProxy); } public registerExceptionHandler() { diff --git a/packages/core/test/router/routes-resolver.spec.ts b/packages/core/test/router/routes-resolver.spec.ts index cdbf3de9e1f..55b9bca04eb 100644 --- a/packages/core/test/router/routes-resolver.spec.ts +++ b/packages/core/test/router/routes-resolver.spec.ts @@ -251,6 +251,28 @@ describe('RoutesResolver', () => { describe('registerNotFoundHandler', () => { it('should register not found handler', () => { + let applicationRef = { + addNestInstanceBaseUrl: (baseUrl: string) => { + return; + }, + getNotFoundCallback: (baseUrl?: string) => { + return (req, res, next) => { + next(); + }; + }, + getRootNotFoundCallback: () => { + return (req, res, next) => { + next(); + }; + }, + setNotFoundHandler: sinon.spy(), + setRootNotFoundHandler: sinon.spy(), + }; + + sinon + .stub((routesResolver as any).container, 'getHttpAdapterRef') + .callsFake(() => applicationRef); + routesResolver.registerNotFoundHandler(); expect(applicationRef.setNotFoundHandler.called).to.be.true; diff --git a/packages/core/test/utils/noop-adapter.spec.ts b/packages/core/test/utils/noop-adapter.spec.ts index c2e73d09fd6..c17400474ba 100644 --- a/packages/core/test/utils/noop-adapter.spec.ts +++ b/packages/core/test/utils/noop-adapter.spec.ts @@ -1,5 +1,6 @@ import { RequestMethod } from '@nestjs/common'; import { AbstractHttpAdapter } from '../../adapters'; +import { Func } from 'mocha'; export class NoopHttpAdapter extends AbstractHttpAdapter { constructor(instance: any) { @@ -18,11 +19,15 @@ export class NoopHttpAdapter extends AbstractHttpAdapter { redirect(response: any, statusCode: number, url: string) {} setErrorHandler(handler: Function, prefix = '/'): any {} setNotFoundHandler(handler: Function, prefix = '/'): any {} + setRootNotFoundHandler(handler: Function): any {} setHeader(response: any, name: string, value: string): any {} - registerParserMiddleware(): any {} - enableCors(options: any): any {} + registerParserMiddleware(prefix?: string): any {} + enableCors(options: any, prefix?: string): any {} createMiddlewareFactory(requestMethod: RequestMethod): any {} getType() { return ''; } + addNestInstanceBaseUrl(baseUrl?: string): void {} + getNotFoundCallback(baseUrl?: string) {} + getRootNotFoundCallback() {} } diff --git a/packages/platform-express/adapters/express-adapter.ts b/packages/platform-express/adapters/express-adapter.ts index cea4d1bb1e0..68e2df44ef2 100644 --- a/packages/platform-express/adapters/express-adapter.ts +++ b/packages/platform-express/adapters/express-adapter.ts @@ -1,4 +1,4 @@ -import { RequestMethod } from '@nestjs/common'; +import { RequestMethod, NotFoundException } from '@nestjs/common'; import { CorsOptions } from '@nestjs/common/interfaces/external/cors-options.interface'; import { NestApplicationOptions } from '@nestjs/common/interfaces/nest-application-options.interface'; import { isFunction, isNil, isObject } from '@nestjs/common/utils/shared.utils'; @@ -13,6 +13,7 @@ import { ServeStaticOptions } from '../interfaces/serve-static-options.interface export class ExpressAdapter extends AbstractHttpAdapter { private readonly routerMethodFactory = new RouterMethodFactory(); + private nestInstanceBaseUrlMap: string[] = []; constructor(instance?: any) { super(instance || express()); @@ -41,11 +42,19 @@ export class ExpressAdapter extends AbstractHttpAdapter { } public setErrorHandler(handler: Function, prefix?: string) { - return this.use(handler); + if (!prefix) { + return this.use(handler); + } + return this.use(prefix.charAt(0) !== '/' ? '/' + prefix : prefix, handler); + } + + public setNotFoundHandler(handler: Function, prefix = '/') { + const baseUrl = prefix.charAt(0) !== '/' ? '/' + prefix : prefix; + return this.use(baseUrl, handler); } - public setNotFoundHandler(handler: Function, prefix?: string) { - return this.use(handler); + public setRootNotFoundHandler(handler: Function) { + return this.use('/', handler); } public setHeader(response: any, name: string, value: string) { @@ -108,8 +117,18 @@ export class ExpressAdapter extends AbstractHttpAdapter { return request.url; } - public enableCors(options: CorsOptions) { - return this.use(cors(options)); + private getRequestOriginalUrl(request: any): string { + return request.originalUrl; + } + + public enableCors(options: CorsOptions, prefix?: string) { + if (!prefix) { + return this.use(cors(options)); + } + return this.use( + prefix.charAt(0) !== '/' ? '/' + prefix : prefix, + cors(options), + ); } public createMiddlewareFactory( @@ -132,20 +151,79 @@ export class ExpressAdapter extends AbstractHttpAdapter { this.httpServer = http.createServer(this.getInstance()); } - public registerParserMiddleware() { + public registerParserMiddleware(prefix?: string) { const parserMiddleware = { jsonParser: bodyParser.json(), urlencodedParser: bodyParser.urlencoded({ extended: true }), }; Object.keys(parserMiddleware) .filter(parser => !this.isMiddlewareApplied(parser)) - .forEach(parserKey => this.use(parserMiddleware[parserKey])); + .forEach(parserKey => { + if (prefix) { + this.use( + prefix.charAt(0) !== '/' ? '/' + prefix : prefix, + parserMiddleware[parserKey], + ); + } else { + this.use(parserMiddleware[parserKey]); + } + }); } public getType(): string { return 'express'; } + public addNestInstanceBaseUrl(baseUrl?: string) { + if (!baseUrl) { + this.nestInstanceBaseUrlMap.push('/'); + return; + } + this.nestInstanceBaseUrlMap.push( + baseUrl.charAt(0) !== '/' ? '/' + baseUrl : baseUrl, + ); + } + + public getNotFoundCallback(baseUrl = '/') { + const prefix = baseUrl.charAt(0) !== '/' ? '/' + baseUrl : baseUrl; + return ( + req: TRequest, + res: TResponse, + next: Function, + ) => { + const method = this.getRequestMethod(req); + const url = this.getRequestUrl(req); + const originalUrl = this.getRequestOriginalUrl(req); + const matchBaseUrl = this.nestInstanceBaseUrlMap + .filter( + nestInstanceBaseUrl => originalUrl.indexOf(nestInstanceBaseUrl) == 0, + ) + .reduce((a, b) => (a.length > b.length ? a : b)); + + if (matchBaseUrl === prefix) { + throw new NotFoundException(`Cannot ${method} ${url}`); + } + next(); + }; + } + + public getRootNotFoundCallback() { + const thisInstanceNo = this.nestInstanceBaseUrlMap.length; + return ( + req: TRequest, + res: TResponse, + next: Function, + ) => { + const method = this.getRequestMethod(req); + const url = this.getRequestUrl(req); + + if (thisInstanceNo === this.nestInstanceBaseUrlMap.length) { + throw new NotFoundException(`Cannot ${method} ${url}`); + } + next(); + }; + } + private isMiddlewareApplied(name: string): boolean { const app = this.getInstance(); return ( diff --git a/packages/platform-fastify/adapters/fastify-adapter.ts b/packages/platform-fastify/adapters/fastify-adapter.ts index b433e03c141..9d8bad66840 100644 --- a/packages/platform-fastify/adapters/fastify-adapter.ts +++ b/packages/platform-fastify/adapters/fastify-adapter.ts @@ -1,4 +1,4 @@ -import { HttpStatus, RequestMethod } from '@nestjs/common'; +import { HttpStatus, RequestMethod, NotFoundException } from '@nestjs/common'; import { CorsOptions } from '@nestjs/common/interfaces/external/cors-options.interface'; import { NestApplicationOptions } from '@nestjs/common/interfaces/nest-application-options.interface'; import { loadPackage } from '@nestjs/common/utils/load-package.util'; @@ -10,6 +10,9 @@ import * as Reply from 'fastify/lib/reply'; import * as pathToRegexp from 'path-to-regexp'; export class FastifyAdapter extends AbstractHttpAdapter { + private nestInstanceCount; + private nestInstanceBaseUrlMap: string[]; + constructor( instanceOrOptions: | TInstance @@ -27,6 +30,7 @@ export class FastifyAdapter extends AbstractHttpAdapter { : fastify((instanceOrOptions as any) as fastify.ServerOptions); super(instance); + this.nestInstanceBaseUrlMap = []; } public listen(port: string | number, callback?: () => void); @@ -70,14 +74,41 @@ export class FastifyAdapter extends AbstractHttpAdapter { handler: Parameters[0], prefix?: string, ) { - return this.instance.setErrorHandler(handler); + if (!prefix) { + return this.instance.setErrorHandler(handler); + } + return this.registerWithPrefix( + async (instance: fastify.FastifyInstance): Promise => { + instance.setErrorHandler(handler); + }, + prefix.charAt(0) !== '/' ? '/' + prefix : prefix, + ); } public setNotFoundHandler( handler: Parameters[0], - prefix?: string, + prefix = '/', ) { - return this.instance.setNotFoundHandler(handler); + const baseUrl = prefix.charAt(0) !== '/' ? '/' + prefix : prefix; + return this.instance.register( + (instance, options, done) => { + instance.setNotFoundHandler(handler); + done(); + }, + { prefix: baseUrl }, + ); + } + + public setRootNotFoundHandler(handler: Function) { + if (!this.nestInstanceBaseUrlMap.includes('/')) { + return this.instance.register( + (instance, options, done) => { + instance.setNotFoundHandler(handler); + done(); + }, + { prefix: '/' }, + ); + } } public getHttpServer(): TServer { @@ -142,12 +173,30 @@ export class FastifyAdapter extends AbstractHttpAdapter { return request.raw.url; } - public enableCors(options: CorsOptions) { - this.register(cors, options); + public enableCors(options: CorsOptions, prefix?: string) { + if (!prefix) { + this.register(cors, options); + return; + } + this.registerWithPrefix( + async (instance: fastify.FastifyInstance): Promise => { + instance.register(cors, (options as unknown) as {}); + }, + prefix.charAt(0) !== '/' ? '/' + prefix : prefix, + ); } - public registerParserMiddleware() { - this.register(formBody); + public registerParserMiddleware(prefix?: string) { + if (!prefix) { + this.register(formBody); + return; + } + this.registerWithPrefix( + async (instance: fastify.FastifyInstance): Promise => { + instance.register(formBody); + }, + prefix.charAt(0) !== '/' ? '/' + prefix : prefix, + ); } public createMiddlewareFactory( @@ -182,10 +231,42 @@ export class FastifyAdapter extends AbstractHttpAdapter { return 'fastify'; } + public addNestInstanceBaseUrl(baseUrl?: string) { + if (!baseUrl) { + this.nestInstanceBaseUrlMap.push('/'); + return; + } + this.nestInstanceBaseUrlMap.push( + baseUrl.charAt(0) !== '/' ? '/' + baseUrl : baseUrl, + ); + } + + public getNotFoundCallback(baseUrl?: string): Function { + return (req, res, next) => { + const method = this.getRequestMethod(req); + const url = this.getRequestUrl(req); + throw new NotFoundException(`Cannot ${method} ${url}`); + }; + } + + public getRootNotFoundCallback() { + return (req, res, next) => { + const method = this.getRequestMethod(req); + const url = this.getRequestUrl(req); + throw new NotFoundException(`Cannot ${method} ${url}`); + }; + } + protected registerWithPrefix>( factory: T, prefix = '/', ): ReturnType { - return this.instance.register(factory, { prefix }); + return this.instance.register( + (instance, options, done) => { + factory; + done(); + }, + { prefix }, + ); } }