From 8e016e8bb01fd181edf1cbfaf91ac5e3368b6521 Mon Sep 17 00:00:00 2001 From: mag123c Date: Mon, 14 Jul 2025 10:03:23 +0900 Subject: [PATCH 1/4] fix(testing): auto-init fastify adapter for middleware registration --- .../fastify-middleware-before-init.spec.ts | 147 ++++++++++++++++++ packages/testing/testing-module.ts | 16 +- 2 files changed, 162 insertions(+), 1 deletion(-) create mode 100644 integration/hello-world/e2e/fastify-middleware-before-init.spec.ts diff --git a/integration/hello-world/e2e/fastify-middleware-before-init.spec.ts b/integration/hello-world/e2e/fastify-middleware-before-init.spec.ts new file mode 100644 index 000000000..bb183fe17 --- /dev/null +++ b/integration/hello-world/e2e/fastify-middleware-before-init.spec.ts @@ -0,0 +1,147 @@ +import { + Controller, + Get, + Injectable, + MiddlewareConsumer, + Module, + NestModule, +} from '@nestjs/common'; +import { + FastifyAdapter, + NestFastifyApplication, +} from '@nestjs/platform-fastify'; +import { Test } from '@nestjs/testing'; +import { expect } from 'chai'; + +describe('Middleware before init (FastifyAdapter)', () => { + let app: NestFastifyApplication; + + @Injectable() + class TestService { + getData(): string { + return 'test_data'; + } + } + + @Controller() + class TestController { + constructor(private readonly testService: TestService) {} + + @Get('test') + test() { + return { data: this.testService.getData() }; + } + + @Get('health') + health() { + return { status: 'ok' }; + } + } + + @Module({ + controllers: [TestController], + providers: [TestService], + }) + class TestModule implements NestModule { + configure(consumer: MiddlewareConsumer) { + consumer + .apply((req, res, next) => { + req.headers['x-middleware'] = 'applied'; + next(); + }) + .forRoutes('*'); + } + } + + describe('should work when middleware is registered before init', () => { + beforeEach(async () => { + const module = await Test.createTestingModule({ + imports: [TestModule], + }).compile(); + + app = module.createNestApplication( + new FastifyAdapter(), + ); + + // This should work without throwing an error + // Previously this would throw: TypeError: this.instance.use is not a function + app.use((req, res, next) => { + req.headers['x-global-middleware'] = 'applied'; + next(); + }); + + await app.init(); + await app.getHttpAdapter().getInstance().ready(); + }); + + it('should handle middleware registration before init', () => { + return app + .inject({ + method: 'GET', + url: '/health', + }) + .then(({ statusCode, payload }) => { + expect(statusCode).to.equal(200); + expect(JSON.parse(payload)).to.deep.equal({ status: 'ok' }); + }); + }); + + it('should process global middleware', () => { + return app + .inject({ + method: 'GET', + url: '/test', + }) + .then(({ statusCode, payload }) => { + expect(statusCode).to.equal(200); + expect(JSON.parse(payload)).to.deep.equal({ data: 'test_data' }); + }); + }); + + afterEach(async () => { + await app.close(); + }); + }); + + describe('should work with multiple middleware registrations before init', () => { + beforeEach(async () => { + const module = await Test.createTestingModule({ + imports: [TestModule], + }).compile(); + + app = module.createNestApplication( + new FastifyAdapter(), + ); + + // Register multiple middlewares before init + app.use((req, res, next) => { + req.headers['x-first-middleware'] = 'applied'; + next(); + }); + + app.use('/test', (req, res, next) => { + req.headers['x-scoped-middleware'] = 'applied'; + next(); + }); + + await app.init(); + await app.getHttpAdapter().getInstance().ready(); + }); + + it('should handle multiple middleware registrations', () => { + return app + .inject({ + method: 'GET', + url: '/test', + }) + .then(({ statusCode, payload }) => { + expect(statusCode).to.equal(200); + expect(JSON.parse(payload)).to.deep.equal({ data: 'test_data' }); + }); + }); + + afterEach(async () => { + await app.close(); + }); + }); +}); diff --git a/packages/testing/testing-module.ts b/packages/testing/testing-module.ts index 79d2e86c3..787398639 100644 --- a/packages/testing/testing-module.ts +++ b/packages/testing/testing-module.ts @@ -78,7 +78,21 @@ export class TestingModule extends NestApplicationContext { this.graphInspector, appOptions, ); - return this.createAdapterProxy(instance, httpAdapter); + const proxy = this.createAdapterProxy(instance, httpAdapter); + + // Auto-initialize adapters that have an init method for testing convenience + if (typeof (httpAdapter as any)?.init === 'function') { + const originalInit = (proxy as any).init; + (proxy as any).init = async function (this: any) { + await (httpAdapter as any).init(); + if (originalInit) { + return originalInit.call(this); + } + return this; + }; + } + + return proxy; } public createNestMicroservice( From 2cf298630bb93200b118a95bebee569e295587ca Mon Sep 17 00:00:00 2001 From: mag123c Date: Mon, 14 Jul 2025 20:29:37 +0900 Subject: [PATCH 2/4] refactor: extract adapter init logic to private method --- packages/testing/testing-module.ts | 32 +++++++++++++++++------------- 1 file changed, 18 insertions(+), 14 deletions(-) diff --git a/packages/testing/testing-module.ts b/packages/testing/testing-module.ts index 787398639..0582b7f25 100644 --- a/packages/testing/testing-module.ts +++ b/packages/testing/testing-module.ts @@ -79,20 +79,7 @@ export class TestingModule extends NestApplicationContext { appOptions, ); const proxy = this.createAdapterProxy(instance, httpAdapter); - - // Auto-initialize adapters that have an init method for testing convenience - if (typeof (httpAdapter as any)?.init === 'function') { - const originalInit = (proxy as any).init; - (proxy as any).init = async function (this: any) { - await (httpAdapter as any).init(); - if (originalInit) { - return originalInit.call(this); - } - return this; - }; - } - - return proxy; + return this.patchAppProxyInit(proxy, httpAdapter); } public createNestMicroservice( @@ -128,6 +115,23 @@ export class TestingModule extends NestApplicationContext { Logger.overrideLogger(options.logger); } + private patchAppProxyInit( + proxy: T, + httpAdapter: HttpServer | AbstractHttpAdapter, + ): T { + if (typeof (httpAdapter as any)?.init === 'function') { + const originalInit = (proxy as any).init; + (proxy as any).init = async function (this: any) { + await (httpAdapter as any).init(); + if (originalInit) { + return originalInit.call(this); + } + return this; + }; + } + return proxy; + } + private createAdapterProxy(app: NestApplication, adapter: HttpServer): T { return new Proxy(app, { get: (receiver: Record, prop: string) => { From d602f74c3719abe9880e7db869553ea387f3753e Mon Sep 17 00:00:00 2001 From: mag123c Date: Tue, 15 Jul 2025 09:58:09 +0900 Subject: [PATCH 3/4] fix(platform-fastify): improve middleware registration error message --- .../fastify-middleware-before-init.spec.ts | 95 +++++++------------ .../adapters/fastify-adapter.ts | 16 ++++ packages/testing/testing-module.ts | 20 +--- 3 files changed, 53 insertions(+), 78 deletions(-) diff --git a/integration/hello-world/e2e/fastify-middleware-before-init.spec.ts b/integration/hello-world/e2e/fastify-middleware-before-init.spec.ts index bb183fe17..f41ca002c 100644 --- a/integration/hello-world/e2e/fastify-middleware-before-init.spec.ts +++ b/integration/hello-world/e2e/fastify-middleware-before-init.spec.ts @@ -53,7 +53,7 @@ describe('Middleware before init (FastifyAdapter)', () => { } } - describe('should work when middleware is registered before init', () => { + describe('should throw helpful error when middleware is registered before init', () => { beforeEach(async () => { const module = await Test.createTestingModule({ imports: [TestModule], @@ -63,72 +63,49 @@ describe('Middleware before init (FastifyAdapter)', () => { new FastifyAdapter(), ); - // This should work without throwing an error - // Previously this would throw: TypeError: this.instance.use is not a function + // This should throw a helpful error message + let errorMessage = ''; + try { + app.use((req, res, next) => { + req.headers['x-global-middleware'] = 'applied'; + next(); + }); + } catch (error) { + errorMessage = error.message; + } + + expect(errorMessage).to.equal('this.instance.use is not a function'); + // The helpful error message is logged, not thrown + }); + + it('should display clear error message', () => { + // Test is complete in beforeEach + }); + }); + + describe('should work when app is initialized before middleware registration', () => { + beforeEach(async () => { + const module = await Test.createTestingModule({ + imports: [TestModule], + }).compile(); + + app = module.createNestApplication( + new FastifyAdapter(), + ); + + // Initialize app first + await app.init(); + + // Now middleware registration should work app.use((req, res, next) => { req.headers['x-global-middleware'] = 'applied'; next(); }); - await app.init(); await app.getHttpAdapter().getInstance().ready(); }); - it('should handle middleware registration before init', () => { - return app - .inject({ - method: 'GET', - url: '/health', - }) - .then(({ statusCode, payload }) => { - expect(statusCode).to.equal(200); - expect(JSON.parse(payload)).to.deep.equal({ status: 'ok' }); - }); - }); - - it('should process global middleware', () => { - return app - .inject({ - method: 'GET', - url: '/test', - }) - .then(({ statusCode, payload }) => { - expect(statusCode).to.equal(200); - expect(JSON.parse(payload)).to.deep.equal({ data: 'test_data' }); - }); - }); - - afterEach(async () => { - await app.close(); - }); - }); - - describe('should work with multiple middleware registrations before init', () => { - beforeEach(async () => { - const module = await Test.createTestingModule({ - imports: [TestModule], - }).compile(); - - app = module.createNestApplication( - new FastifyAdapter(), - ); - - // Register multiple middlewares before init - app.use((req, res, next) => { - req.headers['x-first-middleware'] = 'applied'; - next(); - }); - - app.use('/test', (req, res, next) => { - req.headers['x-scoped-middleware'] = 'applied'; - next(); - }); - - await app.init(); - await app.getHttpAdapter().getInstance().ready(); - }); - - it('should handle multiple middleware registrations', () => { + it('should register middleware successfully after init', () => { return app .inject({ method: 'GET', diff --git a/packages/platform-fastify/adapters/fastify-adapter.ts b/packages/platform-fastify/adapters/fastify-adapter.ts index 370a4e4c2..1ca8582ad 100644 --- a/packages/platform-fastify/adapters/fastify-adapter.ts +++ b/packages/platform-fastify/adapters/fastify-adapter.ts @@ -670,6 +670,22 @@ export class FastifyAdapter< return 'fastify'; } + public use(...args: any[]) { + // Fastify requires @fastify/middie plugin to be registered before middleware can be used. + // Unlike Express, middleware registration in Fastify must happen after initialization. + // We provide a helpful error message to guide developers to call app.init() first. + if (!this.isMiddieRegistered) { + Logger.warn( + 'Middleware registration requires the "@fastify/middie" plugin to be registered first. ' + + 'Make sure to call app.init() before registering middleware with the Fastify adapter. ' + + 'See https://github.com/nestjs/nest/issues/15310 for more details.', + FastifyAdapter.name, + ); + throw new TypeError('this.instance.use is not a function'); + } + return super.use(...args); + } + protected registerWithPrefix( factory: | FastifyPluginCallback diff --git a/packages/testing/testing-module.ts b/packages/testing/testing-module.ts index 0582b7f25..79d2e86c3 100644 --- a/packages/testing/testing-module.ts +++ b/packages/testing/testing-module.ts @@ -78,8 +78,7 @@ export class TestingModule extends NestApplicationContext { this.graphInspector, appOptions, ); - const proxy = this.createAdapterProxy(instance, httpAdapter); - return this.patchAppProxyInit(proxy, httpAdapter); + return this.createAdapterProxy(instance, httpAdapter); } public createNestMicroservice( @@ -115,23 +114,6 @@ export class TestingModule extends NestApplicationContext { Logger.overrideLogger(options.logger); } - private patchAppProxyInit( - proxy: T, - httpAdapter: HttpServer | AbstractHttpAdapter, - ): T { - if (typeof (httpAdapter as any)?.init === 'function') { - const originalInit = (proxy as any).init; - (proxy as any).init = async function (this: any) { - await (httpAdapter as any).init(); - if (originalInit) { - return originalInit.call(this); - } - return this; - }; - } - return proxy; - } - private createAdapterProxy(app: NestApplication, adapter: HttpServer): T { return new Proxy(app, { get: (receiver: Record, prop: string) => { From 07291c21aa4197cd768b2b6e5e32ced4ca302fc5 Mon Sep 17 00:00:00 2001 From: mag123c Date: Tue, 15 Jul 2025 21:10:35 +0900 Subject: [PATCH 4/4] feat(platform-fastify): implement lazy middleware registration --- .../fastify-middleware-before-init.spec.ts | 45 ++++++++++++------- .../adapters/fastify-adapter.ts | 23 +++++----- 2 files changed, 41 insertions(+), 27 deletions(-) diff --git a/integration/hello-world/e2e/fastify-middleware-before-init.spec.ts b/integration/hello-world/e2e/fastify-middleware-before-init.spec.ts index f41ca002c..cf2287885 100644 --- a/integration/hello-world/e2e/fastify-middleware-before-init.spec.ts +++ b/integration/hello-world/e2e/fastify-middleware-before-init.spec.ts @@ -46,14 +46,14 @@ describe('Middleware before init (FastifyAdapter)', () => { configure(consumer: MiddlewareConsumer) { consumer .apply((req, res, next) => { - req.headers['x-middleware'] = 'applied'; + res.setHeader('x-middleware', 'applied'); next(); }) .forRoutes('*'); } } - describe('should throw helpful error when middleware is registered before init', () => { + describe('should queue middleware when registered before init', () => { beforeEach(async () => { const module = await Test.createTestingModule({ imports: [TestModule], @@ -63,23 +63,34 @@ describe('Middleware before init (FastifyAdapter)', () => { new FastifyAdapter(), ); - // This should throw a helpful error message - let errorMessage = ''; - try { - app.use((req, res, next) => { - req.headers['x-global-middleware'] = 'applied'; - next(); - }); - } catch (error) { - errorMessage = error.message; - } + // Register middleware before init - should be queued + app.use((req, res, next) => { + res.setHeader('x-global-middleware', 'applied'); + next(); + }); - expect(errorMessage).to.equal('this.instance.use is not a function'); - // The helpful error message is logged, not thrown + // Now init the app - queued middleware should be registered + await app.init(); + await app.getHttpAdapter().getInstance().ready(); }); - it('should display clear error message', () => { - // Test is complete in beforeEach + it('should apply queued middleware after init', () => { + return app + .inject({ + method: 'GET', + url: '/test', + }) + .then(({ statusCode, payload, headers }) => { + expect(statusCode).to.equal(200); + expect(JSON.parse(payload)).to.deep.equal({ data: 'test_data' }); + // Verify both module-level and global middleware were applied + expect(headers['x-middleware']).to.equal('applied'); + expect(headers['x-global-middleware']).to.equal('applied'); + }); + }); + + afterEach(async () => { + await app.close(); }); }); @@ -98,7 +109,7 @@ describe('Middleware before init (FastifyAdapter)', () => { // Now middleware registration should work app.use((req, res, next) => { - req.headers['x-global-middleware'] = 'applied'; + res.setHeader('x-global-middleware', 'applied'); next(); }); diff --git a/packages/platform-fastify/adapters/fastify-adapter.ts b/packages/platform-fastify/adapters/fastify-adapter.ts index 1ca8582ad..c2650d227 100644 --- a/packages/platform-fastify/adapters/fastify-adapter.ts +++ b/packages/platform-fastify/adapters/fastify-adapter.ts @@ -146,6 +146,7 @@ export class FastifyAdapter< private _isParserRegistered: boolean; private isMiddieRegistered: boolean; + private pendingMiddlewares: Array<{ args: any[] }> = []; private versioningOptions?: VersioningOptions; private readonly versionConstraint = { name: 'version', @@ -256,6 +257,14 @@ export class FastifyAdapter< return; } await this.registerMiddie(); + + // Register any pending middlewares that were added before init + if (this.pendingMiddlewares.length > 0) { + for (const { args } of this.pendingMiddlewares) { + (this.instance.use as any)(...args); + } + this.pendingMiddlewares = []; + } } public listen(port: string | number, callback?: () => void): void; @@ -672,18 +681,12 @@ export class FastifyAdapter< public use(...args: any[]) { // Fastify requires @fastify/middie plugin to be registered before middleware can be used. - // Unlike Express, middleware registration in Fastify must happen after initialization. - // We provide a helpful error message to guide developers to call app.init() first. + // If middie is not registered yet, we queue the middleware and register it later during init. if (!this.isMiddieRegistered) { - Logger.warn( - 'Middleware registration requires the "@fastify/middie" plugin to be registered first. ' + - 'Make sure to call app.init() before registering middleware with the Fastify adapter. ' + - 'See https://github.com/nestjs/nest/issues/15310 for more details.', - FastifyAdapter.name, - ); - throw new TypeError('this.instance.use is not a function'); + this.pendingMiddlewares.push({ args }); + return this; } - return super.use(...args); + return (this.instance.use as any)(...args); } protected registerWithPrefix(