Skip to content

MasonryStack/grpc-descriptor-reflection

Repository files navigation

grpc-descriptor-reflection

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.

Why?

Existing reflection libraries for Node.js gRPC lose extension fields during serialization:

  • @grpc/reflection decodes 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-reflection uses google-protobuf which 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:

  1. Parsing the FileDescriptorSet wire format directly to extract each FileDescriptorProto as a raw byte slice
  2. Using @bufbuild/protobuf only for indexing (building symbol and dependency lookups)
  3. 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.

Installation

npm install grpc-descriptor-reflection

Peer dependencies (you likely already have these):

npm install @grpc/grpc-js @grpc/proto-loader

Generating a Descriptor Set

You need a binary FileDescriptorSet file. Generate one with either buf or protoc:

With buf (recommended)

buf build -o descriptor.bin --as-file-descriptor-set

With protoc

protoc \
  --descriptor_set_out=descriptor.bin \
  --include_imports \
  -I ./proto \
  ./proto/**/*.proto

Important: Use --include_imports (protoc) or the default behavior of buf build to include all transitive dependencies. The reflection service needs the full dependency graph to serve complete responses.

Usage

With vanilla @grpc/grpc-js

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");
});

With NestJS gRPC microservice

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 {}

Filtering advertised services

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 ListServices responses. All symbols remain resolvable via FileContainingSymbol regardless of the filter.

API

new DescriptorReflectionService(descriptorSetPath, options?)

Creates a new reflection service instance.

Parameters:

  • descriptorSetPath (string) - Path to a binary-encoded FileDescriptorSet file.
  • options (DescriptorReflectionServiceOptions) - Optional configuration.
    • services (string[]) - Restrict which services are advertised via ListServices. If omitted, all services in the descriptor set are advertised.

reflection.addToServer(server)

Registers both grpc.reflection.v1.ServerReflection and grpc.reflection.v1alpha.ServerReflection services on the given server.

Parameters:

  • server - A grpc.Server instance or any object with an addService(service, implementation) method.

Supported Reflection Operations

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.

Comparison with @grpc/reflection

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)

How It Works

  1. The constructor reads the binary FileDescriptorSet and parses the protobuf wire format directly to extract each FileDescriptorProto as a raw Uint8Array slice.
  2. 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.
  3. 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.

License

MIT

About

No description, website, or topics provided.

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors