nestjs / nest

A progressive Node.js framework for building efficient, scalable, and enterprise-grade server-side applications with TypeScript/JavaScript πŸš€
https://nestjs.com
MIT License
66.9k stars 7.55k forks source link

Injected @Body in POST request handler is `undefined` #13107

Closed fdbatista closed 7 months ago

fdbatista commented 7 months ago

Is there an existing issue for this?

Current behavior

Hello.

I am trying to apply validations to a request, following the official docs. This is the request I try to send. Content-Type header is application/json:

Screenshot 2024-01-28 175931

I have a middleware that captures correctly the parameters:

Screenshot 2024-01-28 175159

However, when the request reaches its controller function, the body is empty:

Screenshot 2024-01-28 175220

This is my bootstrap function:

async function bootstrap() {
  const app = await NestFactory.create(AppModule);

  app.enableVersioning({
    type: VersioningType.URI,
  });

  app.useGlobalPipes(
    new ValidationPipe({
      transform: true,
    }),
  );

  await app.listen(3000);
}

If I remove completely the payload, the request still hits the controller, which means it is bypassing the validation rules, which are the exact same as the ones from the official docs:

export class LoginDto {
  @IsEmail()
  email: string;

  @IsNotEmpty()
  password: string;
}

I organized the controllers into custom folders by module, so this controller is located under src\modules\auth.

Any ideas why this could be happening? Thanks.

Minimum reproduction code

https://github.com/ahk-reminder/backend

Steps to reproduce

No response

Expected behavior

The controller function should receive a valid payload.

Package

Other package

No response

NestJS version

No response

Packages versions

{
"dependencies": {
    "@nestjs/common": "^10.0.0",
    "@nestjs/config": "^3.1.1",
    "@nestjs/core": "^10.0.0",
    "@nestjs/platform-express": "^10.0.0",
    "@nestjs/typeorm": "^10.0.1",
    "body-parser": "^1.20.2",
    "class-transformer": "^0.5.1",
    "class-validator": "^0.14.1",
    "mysql2": "^3.9.0",
    "reflect-metadata": "^0.1.13",
    "rxjs": "^7.8.1",
    "typeorm": "^0.3.20"
  },
  "devDependencies": {
    "@nestjs/cli": "^10.0.0",
    "@nestjs/schematics": "^10.0.0",
    "@nestjs/testing": "^10.3.1",
    "@types/express": "^4.17.21",
    "@types/jest": "^29.5.2",
    "@types/node": "^20.3.1",
    "@types/supertest": "^6.0.0",
    "@typescript-eslint/eslint-plugin": "^6.0.0",
    "@typescript-eslint/parser": "^6.0.0",
    "eslint": "^8.42.0",
    "eslint-config-prettier": "^9.0.0",
    "eslint-plugin-prettier": "^5.0.0",
    "jest": "^29.5.0",
    "prettier": "^3.0.0",
    "source-map-support": "^0.5.21",
    "supertest": "^6.3.3",
    "ts-jest": "^29.1.0",
    "ts-loader": "^9.4.3",
    "tsconfig-paths": "^4.2.0",
    "typescript": "^5.1.3"
  },
}

Node.js version

v20.9.0

In which operating systems have you tested?

Other

No response

fdbatista commented 7 months ago

I just added a new module, service and controller, and requests just work. So I guess I will just replace the buggy one with this newly-added and that's it.

Closing.

fdbatista commented 7 months ago

Really weird stuff happening here. As soon as I renamed the new module to auth, @Body stopped working again. So I had to give up and name the module authentication until I figure out why it's happening.

micalevisk commented 7 months ago

looks like your repository is private

fdbatista commented 7 months ago

looks like your repository is private

@micalevisk Yes, sorry. Just updated its visibility I am building this from the ground up as an exercise to learn NestJS. If u take a look at it, please note that I removed the problematic auth module, so right now, to reproduce the issue, you'd have to rename the authentication one back to auth.

micalevisk commented 7 months ago

after doing

curl "http://localhost:3000/v1/auth/login" -X POST -H 'content-type: application/json' --data '{"email":"foo@bar.com","password":"bar"}'

and with a console.log({loginDto}) at AuthenticationController#login

I got this output on my terminal:

image

which is expected, right? the JSON body was parsed to object

fdbatista commented 7 months ago

Yeap.

after doing

curl "http://localhost:3000/v1/auth/login" -X POST -H 'content-type: application/json' --data '{"email":"foo@bar.com","password":"bar"}'

and with a console.log({loginDto}) at AuthenticationController#login

I got this output on my terminal:

image

which is expected, right? the JSON body was parsed to object

Yes, that is correct. But if I rename the authorization module, service and controller to just auth it fails.

micalevisk commented 7 months ago

let's do it again:

git clone git@github.com:ahk-reminder/backend.git
cd backend
## then remove the `TypeOrmModule.forRootAsync` from `AppModule` because it shouldn't be needed in order to reproduce this issue. 
## then add a console.log at src/modules/authentication/authentication.controller.ts
npm i
npm start

curl "http://localhost:3000/v1/auth/login" -X POST -H 'content-type: application/json' --data '{"email":"foo@bar.com","password":"bar"}'
# got a 404, as expected since there's no route registered in this 'auth' path

curl "http://localhost:3000/v1/authentication/login" -X POST -H 'content-type: application/json' --data '{"email":"foo@bar.com","password":"bar"}'
# works as expected

please edit your repro to reproduce the issue you're reporting. Everything looks fine so far.

fdbatista commented 7 months ago

let's do it again:

git clone git@github.com:ahk-reminder/backend.git
cd backend
## then remove the `TypeOrmModule.forRootAsync` from `AppModule` because it shouldn't be needed in order to reproduce this issue. 
## then add a console.log at src/modules/authentication/authentication.controller.ts
npm i
npm start

curl "http://localhost:3000/v1/auth/login" -X POST -H 'content-type: application/json' --data '{"email":"foo@bar.com","password":"bar"}'
# got a 404, as expected since there's no route registered in this 'auth' path

curl "http://localhost:3000/v1/authentication/login" -X POST -H 'content-type: application/json' --data '{"email":"foo@bar.com","password":"bar"}'
# works as expected

please edit your repro to reproduce the issue you're reporting. Everything looks fine so far.

Nevermind. It works with the current setup, so thanks for keeping track :)

mattezell commented 7 months ago

I'm still trying to trace this, but we're now having the same issue as originally reported in one of our long running Nestjs APIs...

Due to org changes, our CI/CD pipeline removes the pacakge-lock.json - at which time 10.3.1 versions of various @nestjs libs began resolving on npm install.

One of our long working controllers now reports that it can't read a property of undefined when accessing the body - and when we inspect "@Body rb" in the controller, it is undefined...

In my debugging this afternoon, I see that we're landing here before hitting our controller without the expected body object: https://github.com/nestjs/nest/blob/4818900e517244e276729751d14ae653b16e12e9/packages/core/interceptors/interceptors-consumer.ts#L23

If I revert back to a 10.0.5, everything behaves as expected...

Any ideas @kamilmysliwiec ?

mattezell commented 7 months ago

This appears to be related to the version bump of reflect-metadata in 10.3.0, where reflect-metadata was changed from 0.1.13 to 0.1.14.

0.1.14 introduced the following change that is causing this issue in our project: https://github.com/rbuckton/reflect-metadata/commit/31dde5fba00afaea8c08bc9f13b186c069879566

Specifically, changes to OrdinaryHasOwnMetadata, which is now calling a new GetMetadataProvider method that behaves differently than the previous GetOrCreateMetadataMap method.

micalevisk commented 7 months ago

let's make this open until we fix that reflect-metadata thing, to avoid new issues

mattezell commented 7 months ago

Thanks @micalevisk

So I've been back at it again this morning, doing a bit more diagnostics.

First off, apologies for any thrashing or confusion that I've introduced in my investigation... Hunting down dependency conflicts of dependency's dependencies is enough to make your head spin...

That said, I should have likely began my investigation by running an 'npm ls reflect-metadata' since I was pretty certain this is where the breakdown with populating our controller method's arg was happening.

I don't believe this is actually a problem with the latest release of NestJS itself - and instead was introduced to our project as a result of typeorm being a top level dependency in our project (before my time, so not sure why it was added, though I suspect it can be removed).

With typeorm as a top level dep in our project, and with it being ref'd via the ^, typeorm upgraded to 0.3.20 on a clean install (no package-lock.json), which resulted in a different, and apparently conflicting, version of reflect-metadata was also introduced (see ls output).

my-db-service@2.0.59 /home/matt/w/MY-DB-Service-Bad
β”œβ”€β”¬ @nestjs/axios@3.0.0
β”‚ └── reflect-metadata@0.1.14 deduped
β”œβ”€β”¬ @nestjs/common@10.3.1
β”‚ └── reflect-metadata@0.1.14 deduped
β”œβ”€β”¬ @nestjs/config@3.1.1
β”‚ └── reflect-metadata@0.1.14 deduped
β”œβ”€β”¬ @nestjs/core@10.3.1
β”‚ └── reflect-metadata@0.1.14 deduped
β”œβ”€β”¬ @nestjs/terminus@10.2.1
β”‚ └── reflect-metadata@0.1.14 deduped
β”œβ”€β”¬ @nestjs/typeorm@10.0.1
β”‚ └── reflect-metadata@0.1.14 deduped
β”œβ”€β”¬ my-api-paginate-library@2.0.0
β”‚ β”œβ”€β”¬ @nestjs/common@10.0.4
β”‚ β”‚ └── reflect-metadata@0.1.13 deduped
β”‚ β”œβ”€β”¬ @nestjs/graphql@12.0.7
β”‚ β”‚ β”œβ”€β”¬ @nestjs/mapped-types@2.0.2
β”‚ β”‚ β”‚ └── reflect-metadata@0.1.14 deduped
β”‚ β”‚ └── reflect-metadata@0.1.14 deduped
β”‚ └── reflect-metadata@0.1.13
β”œβ”€β”¬ my-api-response-library@2.0.0
β”‚ └── reflect-metadata@0.1.14 deduped
β”œβ”€β”¬ my-nest-logging-library@2.0.0
β”‚ └── reflect-metadata@0.1.14 deduped
β”œβ”€β”¬ my-request-caching-library@2.0.0
β”‚ └── reflect-metadata@0.1.14 deduped
β”œβ”€β”€ reflect-metadata@0.1.14
└─┬ typeorm@0.3.20
  └── reflect-metadata@0.2.1

So in walking through with the debugger, I initially saw the top level node_modules/reflect-metadata/Reflect.js as being in play, and so incorrectly associated the issue with the recent upgrade to NestJS... But when digging in further this morning, I noticed that later down the chain, the 0.2.1 nested instance somehow assumed control from /node_modules/typeorm/node_modules/reflect-metadata/...

Apparently, this newer version of 0.2.1 introduces some notion of a Metadata Provider and Registry in order to support multiple imported version of reflect-metadata - which seemingly did the opposite of that in the case of our project, resulting in Nest.js no longer properly populating the controller method's @Body decorated arg...

https://github.com/rbuckton/reflect-metadata/commit/31dde5fba00afaea8c08bc9f13b186c069879566

By setting our top level reference of typeorm to the specific "0.3.19" version, which was the release immediately preceding their bump of reflect-metadata to 0.2.1... Now with the top level typeorm using 0.1.14 (deuped, so actually 0.1.13), all is well in our world..

my-db-service@2.0.59 /home/matt/w/MY-DB-Service-Bad
β”œβ”€β”¬ @nestjs/axios@3.0.0
β”‚ └── reflect-metadata@0.1.14 deduped
β”œβ”€β”¬ @nestjs/common@10.3.1
β”‚ └── reflect-metadata@0.1.14 deduped
β”œβ”€β”¬ @nestjs/config@3.1.1
β”‚ └── reflect-metadata@0.1.14 deduped
β”œβ”€β”¬ @nestjs/core@10.3.1
β”‚ └── reflect-metadata@0.1.14 deduped
β”œβ”€β”¬ @nestjs/terminus@10.2.1
β”‚ └── reflect-metadata@0.1.14 deduped
β”œβ”€β”¬ @nestjs/typeorm@10.0.1
β”‚ └── reflect-metadata@0.1.14 deduped
β”œβ”€β”¬ my-api-paginate-library@2.0.0
β”‚ β”œβ”€β”¬ @nestjs/common@10.0.4
β”‚ β”‚ └── reflect-metadata@0.1.13 deduped
β”‚ β”œβ”€β”¬ @nestjs/graphql@12.0.7
β”‚ β”‚ β”œβ”€β”¬ @nestjs/mapped-types@2.0.2
β”‚ β”‚ β”‚ └── reflect-metadata@0.1.14 deduped
β”‚ β”‚ └── reflect-metadata@0.1.14 deduped
β”‚ └── reflect-metadata@0.1.13
β”œβ”€β”¬ my-api-response-library@2.0.0
β”‚ └── reflect-metadata@0.1.14 deduped
β”œβ”€β”¬ my-nest-logging-library@2.0.0
β”‚ └── reflect-metadata@0.1.14 deduped
β”œβ”€β”¬ my-request-caching-library@2.0.0
β”‚ └── reflect-metadata@0.1.14 deduped
β”œβ”€β”€ reflect-metadata@0.1.14
└─┬ typeorm@0.3.19
  └── reflect-metadata@0.1.14 deduped

Now that the fire is out, I will look into if there's any reason for us to have typeorm as a direct import, since we are leveraging @nestjs/typeorm (I'm suspecting not based on a quick test by generating a new NestJS project and adding @nestjs/typeorm working as expected without a top level import).

Thanks for the attention on this - hoping this helps someone else who might stumble here from a similar issue...

kamilmysliwiec commented 7 months ago

I left a comment on this in the typeorm repository here https://github.com/typeorm/typeorm/issues/10671#issuecomment-1931628948

I believe this PR should fix this issue https://github.com/nestjs/nest/pull/12943 (updating the peer dependency constraint making it OK to use v0.2 and share it with other packages)

kamilmysliwiec commented 7 months ago

This should be fixed in 10.3.2 (reflect-metadata v0.2 is now allowed which should lead to package managers using a deduped version of the package)

micalevisk commented 7 months ago

seems to be fixed https://github.com/typeorm/typeorm/issues/10671#issuecomment-1934303151

SylvainSimon commented 7 months ago

I am allowing myself to comment in this ticket. I'm using NestJS as a websocket server with socket.io

The decorators of the event methods (eg: @MessageBody()) are completely ignored if reflect-metadata is not updated to 0.2 when switching to NestJS 10. Everything worked fine in 9 and suddenly my decorators no longer had any kind of importance

Two days wasted.. If it can help anyone :)

danbaghilovici commented 6 months ago

I can confirm what @SylvainSimon said. To fix this issue you need to explicitly set the version of reflect metadata to version ^0.2. Old versions of nest cli will put the version to ^0.1.13 which will break validation decorators (in my case @Body and @Query were ignored). Setting the versions of nest core libraries to ^10.3.2 won't work either (suggested from @micalevisk link from the last comment)

// ignored when 'reflect-metadata' is still on 0.1.13
"@nestjs/common": "^10.3.2",
"@nestjs/core": "^10.3.2",
// works with any nestjs core lib
"reflect-metadata": "^0.2.0",

If you upgrade your nest cli to the latest version with npm i -g @nestjs/cli you will get the latest version of 'reflect-metadata' and so you won't encounter this issue anymore.