nest icon indicating copy to clipboard operation
nest copied to clipboard

`@nestjs/microservices` can't handle gRPC package versioning.

Open mishio-n opened this issue 1 year ago • 0 comments
trafficstars

Is there an existing issue for this?

  • [X] I have searched the existing issues

Current behavior

As shown in this document, gRPC allows version control by including the version number in the "package".

https://learn.microsoft.com/en-us/aspnet/core/grpc/versioning?view=aspnetcore-3.1#version-number-services

Normally, each implementation should be called according to the requested version.

However, in "@nestjs/microservices", the wrong handler is invoked when a gRPC package with a different version is registered.

Minimum reproduction code

https://github.com/mishio-n/nest-grpc-multi-versioning/

├── package.json
├── pnpm-lock.yaml
├── proto
│   └── hello
│       ├── v1
│       │   └── hello.proto
│       └── v2
│           └── hello.proto
├── src
│   ├── app.module.ts
│   ├── hello
│   │   ├── hello.module.ts
│   │   ├── v1
│   │   │   ├── hello.v1.controller.ts
│   │   │   └── hello_pb.ts
│   │   └── v2
│   │       ├── hello.v2.controller.ts
│   │       └── hello_pb.ts
│   └── main.ts

controller for v1

import { Controller } from '@nestjs/common';
import { GrpcMethod } from '@nestjs/microservices';
import {
  type SayHelloRequest,
  SayHelloResponse,
  VersionResponse,
} from './hello_pb';

@Controller()
export class HelloV1Controller {
  @GrpcMethod('HelloService', 'SayHello')
  sayHello(data: SayHelloRequest): SayHelloResponse {
    return new SayHelloResponse({
      message: `Hello, ${data.name}`,
    });
  }

  @GrpcMethod('HelloService', 'Version')
  version(): VersionResponse {
    return new VersionResponse({
      version: 'v1',
    });
  }
}

controller for v2

import { Controller } from '@nestjs/common';
import { GrpcMethod } from '@nestjs/microservices';
import {
  type SayHelloRequest,
  SayHelloResponse,
  VersionResponse,
} from './hello_pb';

@Controller()
export class HelloV2Controller {
  @GrpcMethod('HelloService', 'SayHello')
  sayHello(data: SayHelloRequest): SayHelloResponse {
    return new SayHelloResponse({
      message: `Hello, ${data.name}`,
    });
  }

  @GrpcMethod('HelloService', 'Version')
  version(): VersionResponse {
    return new VersionResponse({
      version: 'v2',
    });
  }
}

Steps to reproduce

I'm using Buf CLI for gRPC request.

  1. pnpm start:dev
  2. gRPC call for hello.v1.HelloService/Version
buf curl --schema proto --protocol grpc --http2-prior-knowledge http://localhost:3000/hello.v1.HelloService/Version
  1. get response
{
  "version": "2"
}

Since the request is for v1, "v1" is expected to be returned. https://github.com/mishio-n/nest-grpc-multi-versioning/blob/main/src/hello/v1/hello.v1.controller.ts#L21

Expected behavior

After checking the source code of "@nestjs/microservices", I believe this behavior is caused by the following code.

https://github.com/nestjs/nest/blob/master/packages/microservices/server/server-grpc.ts#L447-L455

Here, "route" contains the following information.

"{ service: 'HelloService', rpc: 'SayHello', streaming: 'no_stream' }"

The information comes from the "@GrpcMethod" decorator, and I see no way to include the package name here. So, implementations between different packages will be registered overwriting.

https://github.com/nestjs/nest/blob/15cb568e40f42fb3c40c4cb2ad432b23a5ec7bcd/packages/microservices/decorators/message-pattern.decorator.ts#L101-L103

I may be mistaken, though, How can I handle versioning?

Package

  • [ ] I don't know. Or some 3rd-party package
  • [ ] @nestjs/common
  • [ ] @nestjs/core
  • [X] @nestjs/microservices
  • [ ] @nestjs/platform-express
  • [ ] @nestjs/platform-fastify
  • [ ] @nestjs/platform-socket.io
  • [ ] @nestjs/platform-ws
  • [ ] @nestjs/testing
  • [ ] @nestjs/websockets
  • [ ] Other (see below)

Other package

No response

NestJS version

10.0.0

Packages versions

  "dependencies": {
    "@bufbuild/protobuf": "^1.10.0",
    "@grpc/grpc-js": "^1.10.9",
    "@grpc/proto-loader": "^0.7.13",
    "@nestjs/common": "^10.0.0",
    "@nestjs/core": "^10.0.0",
    "@nestjs/microservices": "^10.3.9",
    "@nestjs/platform-express": "^10.0.0",
    "reflect-metadata": "^0.2.0",
    "rxjs": "^7.8.1"
  },
  "devDependencies": {
    "@nestjs/cli": "^10.0.0",
    "@nestjs/schematics": "^10.0.0",
    "@nestjs/testing": "^10.0.0",
    "@types/express": "^4.17.17",
    "@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",
    "ts-node": "^10.9.1",
    "tsconfig-paths": "^4.2.0",
    "typescript": "^5.1.3"
  },

Node.js version

20.11.1

In which operating systems have you tested?

  • [X] macOS
  • [ ] Windows
  • [ ] Linux

Other

No response

mishio-n avatar Jun 24 '24 14:06 mishio-n