- Author(s): jtimmons
- Approver: murgatroid99
- Status: In Review
- Implemented in: Node.js
- Last updated: 2023-10-31
- Discussion at: https://groups.google.com/g/grpc-io/c/Ie2jFIHCwrc
Create a canonical implementation of the gRPC reflection API for Node.js based on the logic from the nestjs-grpc-reflection library
Since its introduction in 2017 there have been a variety of external node.js implementations for the gRPC Reflection Specification, each of which is in various states of maintenance. A few examples are linked at the bottom of this section.
This feature was initially requested in grpc/grpc-node#79
- https://gitlab.com/jtimmons/nestjs-grpc-reflection-module
- https://github.com/deeplay-io/nice-grpc/tree/master/packages/nice-grpc-server-reflection
- https://github.com/AckeeCZ/grpc-server-reflection
- Initial reflection proposal: https://github.com/grpc/proposal/blob/master/A15-promote-reflection.md
- Proposal of API for similar library: https://github.com/grpc/proposal/blob/master/L106-node-heath-check-library.md
We are proposing the creation of a new @grpc/reflection
package with the following external interface:
import type { Server as GrpcServer } from '@grpc/grpc-js';
import type { PackageDefinition } from '@grpc/proto-loader';
type MinimalGrpcServer = Pick<GrpcServer, 'addService'>;
interface ReflectionServerOptions {
services?: string[]; // whitelist of fully-qualified service names to expose. Default: expose all
}
export interface ReflectionServer {
constructor(pkg: PackageDefinition, options?: ReflectionServerOptions);
addToServer(server: MinimalGrpcServer);
}
this ReflectionServer
class will be used to expose information about the user's gRPC package according to the gRPC Reflection Specification via their existing gRPC Server. Internally, the class will be responsible for managing incoming requests for each of the various published versions of the gRPC Reflection Specification: at the time of writing, this includes v1
and v1alpha
but may include more in the future. These version-specific handlers will be isolated into their own services in order to preserve backwards-compatibility, and will look like the following:
reflection.v1.ts
import {
ExtensionNumberResponse,
FileDescriptorResponse,
ListServiceResponse,
} from './proto/grpc/reflection/v1/reflection';
export interface ReflectionV1Implementation {
constructor(pkg: PackageDefinition);
listServices(listServices: string): ListServiceResponse;
fileContainingSymbol(symbol: string): FileDescriptorResponse;
fileByFilename(filename: string): FileDescriptorResponse;
fileContainingExtension(symbol: string, field: number): FileDescriptorResponse;
allExtensionNumbersOfType(symbol: string): ExtensionNumberResponse;
}
The user will leverage the library in a way very similar to the gRPC health check service by creating a new class to manage the reflection logic and then adding that to the gRPC server:
import { join } from 'path';
import * as grpc from '@grpc/grpc-js';
import * as protoLoader from '@grpc/proto-loader';
import { ReflectionServer } from '@grpc/reflection';
const pkg = protoLoader.loadSync(join(__dirname, 'sample.proto'));
const reflection = new ReflectionServer(pkg);
const server = new grpc.Server();
const proto = grpc.loadPackageDefinition(pkg) as any;
server.addService(proto.sample.SampleService.service, { ... });
reflection.addToServer(server)
server.bindAsync('0.0.0.0:5001', grpc.ServerCredentials.createInsecure(), () => { server.start(); });
several reflection implementations linked above leverage protoc in order to generate a representation of the proto schema to expose on the API. In this document we propose the use of proto-loader to inspect the schema at runtime instead in order to simplify the developer experience and be consistent with the design of the grpc-health-check
library.
currently not all reflection clients request the v1
version of the spec so we need to include handlers for both v1
and v1alpha
to support both during the transition. For this reason we separate the reflection handling logic itself to allow for reuse across multiple service versions
I (jtimmons) will implement this once the maintainers have approved
ideally we would be able to support the user adding multiple PackageDefinition
objects at a time in a similar way to the gRPC server itself, however due to some internal protobuf behavior discussed in this thread this is currently difficult to accomplish. For that reason we will be restricting the user to loading a single PackageDefinition for the time being to avoid any confusing behavior or bugs. Practically, this will prevent the user from being able to load dynamic gRPC services that they are not aware of at startup time until this can be resolved.
The issue is described in more detail below for completeness:
background: when loading a PackageDefinition
object via the protoLoader.load(...)
function, proto-loader/protobufjs will rename the input .proto
files based on their protobuf package name. For example a file named file.proto in the sample
package will actually be referred to as sample.proto in all FileDescriptorProto
objects in the resulting PackageDefinition
.
This behavior can cause issues when multiple files exist within the same package as there can be confusion about what is the "real" contents of a file (which is critical information for the reflection API). Proto-loader/protobufjs handles this for a single load()
call by unifying all files into a single package-file; for example: if we have files vendor/a.proto and vendor/b.proto which are both in the vendor
protobuf namespace then contents from both files will be combined into a single vendor.proto file descriptor in the PackageDefinition
. The issue arises when we attempt multiple invocations of protoLoader.load()
as each may only fetch a subset of the package (in this example from a.proto in the first invocation and b.proto in the second). In these cases we have multiple different vendor.proto references which breaks the assumptions of the reflection specification in which files are often looked up by name.