Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
152 changes: 152 additions & 0 deletions integration/hello-world/e2e/middleware-fastify.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -793,4 +793,156 @@ describe('Middleware (FastifyAdapter)', () => {
});
});
});

describe('should apply middleware to routes registered via Fastify plugin with prefix', () => {
const MIDDLEWARE_VALUE = 'middleware_applied';

@Controller()
class PluginPrefixController {
@Get('other')
other() {
return 'other_route';
}
}

@Module({
controllers: [PluginPrefixController],
})
class PluginPrefixModule implements NestModule {
configure(consumer: MiddlewareConsumer) {
consumer
.apply((req, res, next) => {
req.middlewareApplied = true;
next();
})
.forRoutes('/my-prefix');
}
}

beforeEach(async () => {
const fastifyAdapter = new FastifyAdapter();

app = (
await Test.createTestingModule({
imports: [PluginPrefixModule],
}).compile()
).createNestApplication<NestFastifyApplication>(fastifyAdapter);

// Register a Fastify plugin with a prefix (simulating third-party plugin)
fastifyAdapter.getInstance().register(
async instance => {
instance.get('/', async req => {
return (req.raw as any).middlewareApplied
? MIDDLEWARE_VALUE
: 'no_middleware';
});
instance.get('/sub-route', async req => {
return (req.raw as any).middlewareApplied
? MIDDLEWARE_VALUE
: 'no_middleware';
});
},
{ prefix: '/my-prefix' },
);

await app.init();
await app.getHttpAdapter().getInstance().ready();
});

it(`forRoutes('/my-prefix') should match the prefix root`, () => {
return app
.inject({
method: 'GET',
url: '/my-prefix',
})
.then(({ payload }) => expect(payload).to.be.eql(MIDDLEWARE_VALUE));
});
Comment on lines +833 to +859
Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in b19ccea. The handler now sets req.raw.middlewareApplied = true and the assertion checks that flag, so the test actually verifies the middleware ran. Also added a new test covering that forRoutes({ path: '/a', method: RequestMethod.ALL }) does NOT match /a/b — confirming exact-match semantics for ALL.


it(`forRoutes('/my-prefix') should match sub-routes under the prefix`, () => {
return app
.inject({
method: 'GET',
url: '/my-prefix/sub-route',
})
.then(({ payload }) => expect(payload).to.be.eql(MIDDLEWARE_VALUE));
});

afterEach(async () => {
await app.close();
});
});

describe('RequestMethod.ALL should use exact matching (not prefix)', () => {
@Controller()
class AllMethodController {
@Get('/a')
getA(@Req() request: any) {
return {
middlewareApplied: !!(request.raw as any).middlewareApplied,
};
}

@Get('/a/b')
getAB(@Req() request: any) {
return {
middlewareApplied: !!(request.raw as any).middlewareApplied,
};
}
}

@Module({
controllers: [AllMethodController],
})
class AllMethodModule implements NestModule {
configure(consumer: MiddlewareConsumer) {
consumer
.apply((req, res, next) => {
req.middlewareApplied = true;
next();
})
.forRoutes({ path: '/a', method: RequestMethod.ALL });
}
}

beforeEach(async () => {
app = (
await Test.createTestingModule({
imports: [AllMethodModule],
}).compile()
).createNestApplication<NestFastifyApplication>(new FastifyAdapter());

await app.init();
await app.getHttpAdapter().getInstance().ready();
});

it(`forRoutes({ path: '/a', method: ALL }) should match /a exactly`, () => {
return app
.inject({
method: 'GET',
url: '/a',
})
.then(({ payload }) =>
expect(JSON.parse(payload)).to.deep.equal({
middlewareApplied: true,
}),
);
});

it(`forRoutes({ path: '/a', method: ALL }) should NOT match /a/b (no prefix matching)`, () => {
return app
.inject({
method: 'GET',
url: '/a/b',
})
.then(({ payload }) =>
expect(JSON.parse(payload)).to.deep.equal({
middlewareApplied: false,
}),
);
});

afterEach(async () => {
await app.close();
});
});
});
20 changes: 16 additions & 4 deletions packages/platform-fastify/adapters/fastify-adapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -675,6 +675,18 @@ export class FastifyAdapter<
if (!this.isMiddieRegistered) {
await this.registerMiddie();
}

// Only string routes from forRoutes('/prefix') — represented internally
// as method -1 — should use prefix matching (end: false) to match
// sub-routes under the given path. This mirrors Express's app.use()
// behavior for prefix-based middleware.
//
// RequestMethod.ALL maps to the HTTP adapter's .all() which is exact-match
// semantics (match all HTTP methods on this exact path). Using prefix
// matching for ALL would cause forRoutes({ path: '/a', method: ALL }) to
// incorrectly match '/a/b/c', breaking the "execute only once" guarantee.
const isStringRoute = (requestMethod as number) === -1;

return (path: string, callback: Function) => {
const hasEndOfStringCharacter = path.endsWith('$');
path = hasEndOfStringCharacter ? path.slice(0, -1) : path;
Expand All @@ -693,10 +705,10 @@ export class FastifyAdapter<
}

try {
let { regexp: re } = pathToRegexp(normalizedPath);
re = hasEndOfStringCharacter
? new RegExp(re.source + '$', re.flags)
: re;
const endMatch = hasEndOfStringCharacter || !isStringRoute;
const { regexp: re } = pathToRegexp(normalizedPath, {
end: endMatch,
});

// The following type assertion is valid as we use import('@fastify/middie') rather than require('@fastify/middie')
// ref https://github.com/fastify/middie/pull/55
Expand Down