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
74 changes: 74 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,78 @@ 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 () => MIDDLEWARE_VALUE);
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 +855

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();
});
});
});
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();
}

// When the request method is "ALL" (or -1 for string routes from
// forRoutes('/prefix')), use prefix matching (end: false) to match
// sub-routes under the given path — this mirrors Express's app.use()
// behavior for prefix-based middleware. For specific HTTP methods
// (from forRoutes(Controller) or forRoutes({ path, method })), use
// exact matching (end: true) to prevent middleware from firing on
// sub-routes when multiple routes are registered for the same
// middleware (the "execute only once" guarantee).
const isMethodAll =
requestMethod === RequestMethod.ALL || (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 || !isMethodAll;
const { regexp: re } = pathToRegexp(normalizedPath, {
end: endMatch,
});
Comment on lines 687 to +711

// 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
Loading