Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

MiddlewareConsumer routing is broken #8108

Closed
oleg-yudovich opened this issue Sep 22, 2021 · 28 comments
Closed

MiddlewareConsumer routing is broken #8108

oleg-yudovich opened this issue Sep 22, 2021 · 28 comments
Labels
needs triage This issue has not been looked into

Comments

@oleg-yudovich
Copy link

oleg-yudovich commented Sep 22, 2021

Bug Report

Current behavior

The MiddlewareConsumer routing is broken when you define home route: /
All routes mapped to the same path '/'

Input Code

Routing module:

export class RoutingModule implements NestModule {
  configure(consumer: MiddlewareConsumer) {
    consumer
      .apply((req, res) => res.send('hit path /'))
      .forRoutes({ path: '/', method: RequestMethod.GET })
      .apply((req, res) => res.send('hit path /test/app'))
      .forRoutes({ path: '/test/app', method: RequestMethod.GET })
      .apply((req, res) => res.send('hit path /:username'))
      .forRoutes({ path: '/:username', method: RequestMethod.GET });
  }
}

App module:

import { Module } from '@nestjs/common';
import { RoutingModule } from './routing.module';

@Module({
  imports: [RoutingModule],
})
export class AppModule {}

Get request to the '/test/app' and '/:username' prints 'hit path /'
Feels like '/' defined as '*'

Expected behavior

The /test/app and /:username routes should be accessible

Possible Solution

Environment


"@nestjs/common": "^8.0.6",
"@nestjs/core": "^8.0.6",
"@nestjs/platform-express": "^8.0.6"

 
For Tooling issues:
- Node version: 14  
- Platform:  Mac/Linux 

Others:

This issue is not reproduced in NestJS 7.X.X - started in 8.X.X.

This flow works as expected with the Controllers:
@Controller()
export class AppController {
  @Get('/')
  homepage(): string {
    return 'hit /';
  }

  @Get('/test/app')
  username(): string {
    return 'hit /test/app';
  }

  @Get('/:username')
  users(): string {
    return 'hit /:username';
  }
}
@oleg-yudovich oleg-yudovich added the needs triage This issue has not been looked into label Sep 22, 2021
@jmcdo29
Copy link
Member

jmcdo29 commented Sep 22, 2021

Please provide a minimum reproduction repository.

@oleg-yudovich
Copy link
Author

oleg-yudovich commented Sep 22, 2021

@jmcdo29
Copy link
Member

jmcdo29 commented Sep 22, 2021

Wouldn't this be due to how regex matches work? i.e. if you want to hit just / then you either need to bind that last or use /$ as the regex to signify the end of the string?

@oleg-yudovich
Copy link
Author

@jmcdo29 I don't think you're right. If I register two routes, let's say /:username and /users so route /users should be registered first. When I register just / why it should be last? This route regex is not overriding /something.
And as I said in the bug description:

  1. Is not reproduced with controllers
  2. Is not reproduced in NestJs 7.X.X
  3. Is not reproduced with a pure Express server
  4. Is not behave like this in any other language and framework

@Zclhlmgqzc
Copy link
Contributor

Zclhlmgqzc commented Sep 23, 2021

@jmcdo29 I don't think you're right. If I register two routes, let's say /:username and /users so route /users should be registered first. When I register just / why it should be last? This route regex is not overriding /something.
And as I said in the bug description:

  1. Is not reproduced with controllers
  2. Is not reproduced in NestJs 7.X.X
    ---> GET /path 7.x app.get(/path) (Router-level middleware)
    now app.use( /path req.method GET )
  3. Is not reproduced with a pure Express server
    ---> see Express middlewares are applied wrong since 8.0.5 #7737 app.use( req.method GET )
  4. Is not behave like this in any other language and framework
    --->fastify middleware fastify.use( req.method GET )

http://127.0.0.1:3001/test => hit path /

const express = require('express')
const app = express()

app.use('/', function (req, res, next) {
    if (req.url === '/') {
        console.log('isRequestMapping')
    }
    if (req.method === 'GET') {
        res.send('hit path /')
    }
})

app.use('/test', function (req, res, next) {
    if (req.url === '/') {
        console.log('isRequestMapping')
    }
    if (req.method === 'GET') {
        res.send('hit path test')
    }
})

app.listen(3001)

http://127.0.0.1:3001/test => hit path /test

const express = require('express')
const app = express()

app.use('/test', function (req, res, next) {
    if (req.url === '/') {
        console.log('isRequestMapping')
    }
    if (req.method === 'GET') {
        res.send('hit path test')
    }
})

app.use('/', function (req, res, next) {
    if (req.url === '/') {
        console.log('isRequestMapping')
    }
    if (req.method === 'GET') {
        res.send('hit path /')
    }
})

app.listen(3001)

the bug description:

Current behavior

forRoutes({ path: '/' }, { path: '/test/app' }, { path: '/:username' }) === forRoutes(AppController)

Expected behavior

forRoutes(AppController) should be equal to [{ path: '/$' }, { path: '/test/app$' }, { path: '/:username$' }]

forRoutes({ path: '/' }, { path: '/test/app' }, { path: '/:username' }) should be not equal to forRoutes(AppController)

@Zclhlmgqzc
Copy link
Contributor

@Zclhlmgqzc I am running a simple express server (express 4.17.1, the same version used in NestJS):

import express from 'express'

const app = express()

app.get('/', (req, res) => {
  res.send('home')
})

app.get('/about', (req, res) => {
  res.send('about')
})

let server = app.listen(3000, () => {
  console.log(`server running at port http://localhost/${server.address().port}`)
})

route http://localhost:3000/about hit /about

app.use != app.get post ...

@oleg-yudovich
Copy link
Author

@Zclhlmgqzc @kamilmysliwiec FYI - this patch does not solve the problem. Please see the example in this simple repo.

@Zclhlmgqzc
Copy link
Contributor

Zclhlmgqzc commented Oct 5, 2021

@Zclhlmgqzc @kamilmysliwiec FYI - this patch does not solve the problem. Please see the example in this simple repo.

forRoutes(AppController) should be equal to [{ path: '/$' }, { path: '/test/app$' }, { path: '/:username$' }]

forRoutes({ path: '/' }, { path: '/test/app' }, { path: '/:username' }) should be not equal to forRoutes(AppController)

so use /$ instead of /

app.use(/) != app.use(/$)

@oleg-yudovich
Copy link
Author

@Zclhlmgqzc @kamilmysliwiec FYI - this patch does not solve the problem. Please see the example in this simple repo.

forRoutes(AppController) should be equal to [{ path: '/$' }, { path: '/test/app$' }, { path: '/:username$' }]

forRoutes({ path: '/' }, { path: '/test/app' }, { path: '/:username' }) should be not equal to forRoutes(AppController)

so use /$ instead of /

app.use(/) != app.use(/$)

So is not really a fix to the issue. Now we have different behavior of controllers routing and middleware consumer routing. Is still a bug IMHO

@oleg-yudovich
Copy link
Author

Do we understand why this issue started in NestJS 8 in the first place?

@Zclhlmgqzc
Copy link
Contributor

Zclhlmgqzc commented Oct 5, 2021

So is not really a fix to the issue. Now we have different behavior of controllers routing and middleware consumer routing. Is still a bug IMHO

I think it's a break change instead of bug
Current behavior:

nest forRoutes(path /) === express use(/)

forRoutes(Controller) === forRoutes(path /$) === express use(/$) ≈≈ express.all(/)

@kamilmysliwiec
Copy link
Member

To avoid introducing a breaking change, we might need to revert all middleware-specific PRs (all changes since 8.0.5) and wait with that till we release v9 unless you have a better idea @Zclhlmgqzc

@Zclhlmgqzc
Copy link
Contributor

Zclhlmgqzc commented Oct 5, 2021

To avoid introducing a breaking change, we might need to revert all middleware-specific PRs (all changes since 8.0.5) and wait with that till we release v9 unless you have a better idea @Zclhlmgqzc

but some breaking change in router-method-factory

forRoutes(7.x path / != 8.x path /)

7.6.18-router-method-factory
8.0.0-router-method-factory

7.x forRoutes('/xx') === 8.0.4 forRoutes(/xx GET) === 8.0.10 forRoutes(/xx$ GET)

7.x forRoutes('/xx') != 8.x forRoutes('/xx')

7.x forRoutes(/xx ALL) === 8.0.4/8.0.10 forRoutes('/xx')

@Zclhlmgqzc
Copy link
Contributor

@kamilmysliwiec see #7759

maybe it can fix this problem

isRequestMapping default true

forRoutes({ / GET }) === forRoutes({ / isRequestMapping true GET }) ≈≈ express.get(/)


forRoutes({ / isRequestMapping false }) === express.use(/)

@oleg-yudovich
Copy link
Author

oleg-yudovich commented Oct 5, 2021

@kamilmysliwiec @Zclhlmgqzc I found the bug is bigger than described. Actually routing engine through MiddlewareConsumer is completely broken. When you define:

export class RoutingModule implements NestModule {
  configure(consumer: MiddlewareConsumer) {
    consumer
      .apply((req, res) => res.send('hit path /test'))
      .forRoutes({ path: '/test', method: RequestMethod.GET })
      .apply((req, res) => res.send('hit path /test/about'))
      .forRoutes({ path: '/test/about', method: RequestMethod.GET });
  }
}

The /test/about will never be reached - /test cathes /test/about
To avoid route /x to catch request of route /x/y we now need define route /x$

@Zclhlmgqzc
Copy link
Contributor

Zclhlmgqzc commented Oct 5, 2021

The /test/about will never be reached - /test cathes /test/about To avoid route /x to catch request of route /x/y we now need define route /x$

try

nest-8.0.0/8.0.10:

// RouterMethodFactory default: return target.use
// In this case the /test/about will never be reached also
export class RoutingModule implements NestModule {
  configure(consumer: MiddlewareConsumer) {
    consumer
      .apply((req, res) => res.send('hit path /test'))
      .forRoutes('/test')
      .apply((req, res) => res.send('hit path /test/about'))
      .forRoutes('/test/about');
  }
}

nest-7.x:

// RouterMethodFactory case RequestMethod.ALL: return target.use
// In this case the /test/about will never be reached also
export class RoutingModule implements NestModule {
  configure(consumer: MiddlewareConsumer) {
    consumer
      .apply((req, res) => res.send('hit path /test'))
      .forRoutes({ path: '/test', method: RequestMethod.ALL })
      .apply((req, res) => res.send('hit path /test/about'))
      .forRoutes({ path: '/test/about', method: RequestMethod.ALL });
  }
}

Express middleware .use and .all/get/post/delete/put (Router-level middleware)
Fastify middleware .use only
nest-8.0.5 use .use instead of the .all(get post...) method ensure the same behavior as fastify

in #8230
new behavior
backward compatibility

@Controller()
export class AppController {
  @All('/xx')
  homepage(): string {
    return 'hit /';
  }
}

forRoutes('/xx') === forRoutes(AppController) === forRoutes({ path: '/xx', isRequestMapping: true }) ≈≈ express.all('/xx')

forRoutes({ path: '/xx', isRequestMapping: false }) === express.use('/xx')

@kamilmysliwiec
Copy link
Member

kamilmysliwiec commented Oct 6, 2021

I found the bug is bigger than described. Actually routing engine through MiddlewareConsumer is completely broken.

@oleg-yudovich this is how fastify/express middleware works. This is not a bug & nor it's "completely broken".

@kamilmysliwiec see #7759 maybe it can fix this problem

@Zclhlmgqzc this PR won't fix this issue.

To move back to the behavior before 8.0.5, we must revert these PRs you've made:

and save them for v9. Would you like to create a revert PR (reverting all changes that were made in the PRs above) and then a PR combining those two so we can have them prepared for v9?

UPDATE:

@Zclhlmgqzc can you revert this PR #7562 then?

We can then create a separate PR

@oleg-yudovich
Copy link
Author

oleg-yudovich commented Oct 6, 2021

@kamilmysliwiec in NestJS 7
.forRoutes({ path: '/test', method: RequestMethod.GET }) used express middleware app.get. Now when I define my route .forRoutes({ path: '/test', method: RequestMethod.GET }) it behaves like app.use. If it not a bug it completely changed routing behavior in NestJS 8 and at least should be in release notes.

What is the proper way of calling express app.get/post/put... in platform-express in nest 8?

@Zclhlmgqzc
Copy link
Contributor

@Zclhlmgqzc this PR won't fix this issue.

@kamilmysliwiec
What's the problem with the PR #8230 ?
I think #8230 had to move back to the behavior before 8.0.5

If must be revert #7562
like this only

+  public createMiddlewareFactory(
+   requestMethod: RequestMethod,
+ ): (path: string, callback: Function) => any {
+    return this.routerMethodFactory
+     .get(this.instance, requestMethod)
+      .bind(this.instance);
-  public createMiddlewareFactory(): (path: string, callback: Function) => any {
-    return this.instance.use.bind(this.instance);
-  }

@kamilmysliwiec
Copy link
Member

kamilmysliwiec commented Oct 6, 2021

What's the problem with the PR #8230 ?

We want to get back to the behavior before v8.0.5 to avoid introducing breaking changes. (using use() should be considered a breaking change as well)

We can save all improvements you made till v9

@Zclhlmgqzc
Copy link
Contributor

Zclhlmgqzc commented Oct 6, 2021

What's the problem with the PR #8230 ?

We want to get back to the behavior before v8.0.5 to avoid introducing breaking changes. (using use() should be considered a breaking change as well)

We can save all improvements you made till v9

but if some one else use forRoutes('/xxx') in nest-8 it will use use()

right ?

@kamilmysliwiec
Copy link
Member

but if some one else use forRoutes('/xxx') in nest-8 it will use use()

We might need to revert this back to .all() as in v7. Still, @All() decorator should use .all() not .use()

@kamilmysliwiec
Copy link
Member

This should be fixed in 8.0.11

@oleg-yudovich
Copy link
Author

Thanks, @kamilmysliwiec, and @Zclhlmgqzc .
Confirming, the issue is fixed.

@ianzone
Copy link

ianzone commented Mar 13, 2023

I'm facing this issue with "@nestjs/core": "^9.3.9", maybe the documentation should emphasize the use of "/$"? cuz intuitively, people would just use "/".
image

@ianzone
Copy link

ianzone commented Mar 13, 2023

and the behavior of "$" is either consistent, as you can see "/docs$" didn't trigger the DocsMiddleware.
image

@KokoTa
Copy link

KokoTa commented Aug 3, 2023

Have the same issue with nest v10.

"@nestjs/common": "^10.0.0",
"@nestjs/core": "^10.0.0",
"@nestjs/platform-fastify": "^10.0.5",

image image

I must be use req.originalUrl to get actual path, req.url always return '/'.
This problem is on fastify mode.

@krylovaaleksandra
Copy link

krylovaaleksandra commented Jan 16, 2024

Have the same issue with nest v10.

"@nestjs/common": "^10.0.0", "@nestjs/core": "^10.0.0", "@nestjs/platform-fastify": "^10.0.5",
image image

I must be use req.originalUrl to get actual path, req.url always return '/'. This problem is on fastify mode.

i have the same problem
i use forRoutes("*") and it's returns "/"

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
needs triage This issue has not been looked into
Projects
None yet
Development

No branches or pull requests

7 participants