A gRPC server reflection service for @grpc/grpc-js that serves raw FileDescriptorProto bytes from a pre-built descriptor set, preserving all extensions and custom options byte-for-byte.
Existing reflection libraries for Node.js gRPC lose extension fields during serialization:
-
@grpc/reflectiondecodes descriptors through protobufjs and re-encodes them. protobufjs silently drops all extension fields (google.api.http,openapiv2_operation, custom options, etc.) because they are not declared fields on the standard option message types. -
grpc-server-reflectionusesgoogle-protobufwhich performs a similar decode/re-encode round-trip, with no guarantee of byte-for-byte fidelity.
This means any downstream consumer relying on reflection (API gateways, documentation generators, gRPC-to-HTTP transcoders) will see methods without HTTP bindings, missing OpenAPI annotations, or stripped custom options.
grpc-descriptor-reflection solves this by:
- Parsing the
FileDescriptorSetwire format directly to extract eachFileDescriptorProtoas a raw byte slice - Using
@bufbuild/protobufonly for indexing (building symbol and dependency lookups) - Serving the original raw bytes to reflection clients without any re-encoding
The result is byte-for-byte identical output to what buf build or protoc produced.
npm install grpc-descriptor-reflectionPeer dependencies (you likely already have these):
npm install @grpc/grpc-js @grpc/proto-loaderYou need a binary FileDescriptorSet file. Generate one with either buf or protoc:
buf build -o descriptor.bin --as-file-descriptor-setprotoc \
--descriptor_set_out=descriptor.bin \
--include_imports \
-I ./proto \
./proto/**/*.protoImportant: Use
--include_imports(protoc) or the default behavior ofbuf buildto include all transitive dependencies. The reflection service needs the full dependency graph to serve complete responses.
import * as grpc from "@grpc/grpc-js";
import { DescriptorReflectionService } from "grpc-descriptor-reflection";
const server = new grpc.Server();
// Add your application services...
// server.addService(MyService, myImplementation);
// Add reflection (registers both v1 and v1alpha endpoints)
const reflection = new DescriptorReflectionService("path/to/descriptor.bin");
reflection.addToServer(server);
server.bindAsync("0.0.0.0:50051", grpc.ServerCredentials.createInsecure(), () => {
console.log("Server listening on port 50051");
});import { DescriptorReflectionService } from "grpc-descriptor-reflection";
const reflection = new DescriptorReflectionService(
join(__dirname, "..", "descriptor.bin"),
);
@Module({
imports: [
ClientsModule.register([
{
name: "ORDERS_PACKAGE",
transport: Transport.GRPC,
options: {
package: "orders.v1",
protoPath: join(__dirname, "../proto/orders/v1/orders.proto"),
url: "0.0.0.0:50051",
onLoadPackageDefinition: (_pkg, server) => {
reflection.addToServer(server);
},
},
},
]),
],
})
export class AppModule {}By default, all services found in the descriptor set are advertised via ListServices. You can restrict which services are listed:
const reflection = new DescriptorReflectionService("descriptor.bin", {
services: ["mypackage.MyService", "mypackage.AdminService"],
});Note: The filter only affects
ListServicesresponses. All symbols remain resolvable viaFileContainingSymbolregardless of the filter.
Creates a new reflection service instance.
Parameters:
descriptorSetPath(string) - Path to a binary-encodedFileDescriptorSetfile.options(DescriptorReflectionServiceOptions) - Optional configuration.services(string[]) - Restrict which services are advertised viaListServices. If omitted, all services in the descriptor set are advertised.
Registers both grpc.reflection.v1.ServerReflection and grpc.reflection.v1alpha.ServerReflection services on the given server.
Parameters:
server- Agrpc.Serverinstance or any object with anaddService(service, implementation)method.
| Operation | Description |
|---|---|
ListServices |
Lists all (or filtered) services |
FileContainingSymbol |
Returns file descriptors for a fully-qualified symbol |
FileByFilename |
Returns a file descriptor by its proto filename |
FileContainingExtension |
Returns the file containing an extension field |
AllExtensionNumbersOfType |
Returns all extension field numbers for a type |
All responses include transitive dependencies.
| Feature | @grpc/reflection |
grpc-descriptor-reflection |
|---|---|---|
Preserves google.api.http |
No | Yes |
Preserves openapiv2_operation |
No | Yes |
| Preserves custom options | No | Yes |
| Byte-for-byte fidelity | No | Yes |
| Input | Proto file paths | Pre-built descriptor set |
| Encoding library | protobufjs (lossy) | Raw wire format (lossless) |
- The constructor reads the binary
FileDescriptorSetand parses the protobuf wire format directly to extract eachFileDescriptorProtoas a rawUint8Arrayslice. - It then decodes the set with
@bufbuild/protobuf(which does preserve extensions) solely to build indexes: symbol-to-file mappings, dependency graphs, and extension registries. - When a reflection client requests a file, the service serves the original raw bytes extracted in step 1, along with all transitive dependencies.
Since the raw bytes are never decoded and re-encoded, every extension field, custom option, and annotation is preserved exactly as the proto compiler produced it.
MIT