Skip to content

Server Reflection ​

Overview

Server Reflection allows clients to discover services, methods, and message types at runtime without access to .proto files. Connectum implements the gRPC Server Reflection Protocol (v1 and v1alpha) through the @connectum/reflection package.

This page owns operational enablement and client-tool usage. Implementing a new server extension belongs to Custom protocols.

Why Use Reflection? ​

  • grpcurl: List and call services without providing .proto files
  • Postman / Insomnia / Warthog: Auto-discover gRPC services for manual testing
  • buf curl: Test ConnectRPC services with automatic schema resolution
  • Service registries: Dynamic service discovery in microservice architectures
  • Development: Explore APIs interactively during development

Production consideration

Server Reflection exposes your service schema to any client that can connect. In production environments, consider disabling it or restricting access.

Installation ​

bash
npm install @connectum/reflection
bash
pnpm add @connectum/reflection
bash
bun add @connectum/reflection

Peer dependencies: @connectum/core, @bufbuild/protobuf ^2.16.0 and @connectrpc/connect ^2.2.0; @connectum/core raises the working minimum to 2.3.0 (see Peer dependencies on protobuf and Connect).

Quick Setup ​

typescript
import { createServer } from '@connectum/core';
import { Reflection } from '@connectum/reflection';
import { greeterService } from './services/greeterService.ts';

const server = createServer({
  services: [greeterService],
  port: 5000,
  // grpcurl and buf curl use HTTP/2; plaintext HTTP/1.1 is the default.
  allowHTTP1: false,
  protocols: [Reflection()],
});

await server.start();

That is all you need for plaintext h2c. The Reflection() factory creates a ProtocolRegistration that registers grpc.reflection.v1.ServerReflection and grpc.reflection.v1alpha.ServerReflection on your server.

How It Works ​

When you pass Reflection() to the protocols array, Connectum:

  1. Collects the file descriptors of all registered services, yours and those of the other protocols, once every protocol has registered
  2. Indexes those descriptors and their transitive imports by file name, by fully-qualified symbol and by extension
  3. Registers the v1 and v1alpha ServerReflection services on the ConnectRouter
  4. Clients can then query the reflection service to discover available services

Reflection collects its descriptors after every service and every protocol has registered, so it has access to all of them. The position of Reflection() in the protocols array does not matter: [Healthcheck(), Reflection()] and [Reflection(), Healthcheck()] both list grpc.health.v1.Health, and so does a custom protocol placed after Reflection().

Protocol Behavior ​

The answers follow the reflection protocol:

RequestAnswer
list_servicesThe mounted services: application services and the services of every other protocol, whatever its position in protocols. Services that are declared but not mounted (in an imported file, or next to a mounted service in the same file) are not listed.
file_by_filename, file_containing_symbol, file_containing_extensionThe requested file first, then each transitive import (well-known types included) not yet sent on the same stream.
file_containing_symbolResolves services, methods (pkg.Service.Method), messages, fields, oneofs, enums, enum values and extensions. Enum values are named in the scope that contains their enum (pkg.LEVEL_HIGH, not pkg.Level.LEVEL_HIGH).
all_extension_numbers_of_typebase_type_name set to the requested type, numbers in ascending order.
Unknown file, symbol, extension or typeerror_response with NOT_FOUND (5), naming what was not found. The message repeats at most the first 128 characters of the name, with … after a cut name; original_request carries the request in full.
Request with no query seterror_response with INVALID_ARGUMENT (3).

An error answers one request only; the stream stays open for the next one.

Describing the reflection service ​

The two reflection services are not listed, but they are mounted and can be described like any other service. file_containing_symbol for grpc.reflection.v1.ServerReflection (or grpc.reflection.v1alpha.ServerReflection, one of its methods or messages) and file_by_filename for grpc/reflection/v1/reflection.proto (or the v1alpha file) are answered:

bash
grpcurl -plaintext localhost:5000 describe grpc.reflection.v1.ServerReflection

Request size ​

Reflection has no request size limit of its own. A request message is read up to the server-wide readMaxBytes, which is unset by default; a request above a limit you set ends the call with resource_exhausted. A reflection request carries a file name, a symbol or a type name, so a limit of a few kilobytes is enough for it.

Open streams and shutdown ​

A client may keep a ServerReflectionInfo stream open after the last answer, as grpcurl in interactive mode, an IDE plugin or Postman do. The stream follows the call's abort signal: when the server stops, the client cancels or the deadline passes, the handler stops waiting for the next request and ends the stream. server.stop() therefore does not wait for shutdown.timeout because of an idle reflection session; a stream that is waiting for the client's next request ends without an error.

Using grpcurl with Reflection ​

grpcurl is the most common tool for interacting with reflection-enabled gRPC services.

List All Services ​

bash
grpcurl -plaintext localhost:5000 list

Output:

greeter.v1.GreeterService

The reflection service does not list itself: it is infrastructure, not part of your API. Clients still reach it, which is how list works, and can describe it. The health service is listed when Healthcheck() is in protocols, before or after Reflection().

Describe a Service ​

bash
grpcurl -plaintext localhost:5000 describe greeter.v1.GreeterService

Output:

greeter.v1.GreeterService is a service:
service GreeterService {
  rpc SayHello ( .greeter.v1.SayHelloRequest ) returns ( .greeter.v1.SayHelloResponse );
}

Describe a Method ​

bash
grpcurl -plaintext localhost:5000 describe greeter.v1.GreeterService.SayHello

Output:

greeter.v1.GreeterService.SayHello is a method:
rpc SayHello ( .greeter.v1.SayHelloRequest ) returns ( .greeter.v1.SayHelloResponse );

Fields (greeter.v1.SayHelloRequest.name), enums and enum values can be described the same way.

Describe a Message Type ​

bash
grpcurl -plaintext localhost:5000 describe greeter.v1.SayHelloRequest

Output:

greeter.v1.SayHelloRequest is a message:
message SayHelloRequest {
  string name = 1;
}

Call a Method ​

With reflection enabled, grpcurl does not need -proto or -protoset flags:

bash
grpcurl -plaintext \
  -d '{"name": "Alice"}' \
  localhost:5000 \
  greeter.v1.GreeterService/SayHello

With TLS ​

The setup above is plaintext. To use these TLS commands, configure createServer() with TLS as described in TLS configuration. With TLS, the default allowHTTP1: true negotiates HTTP/1.1 or HTTP/2 through ALPN.

bash
# Self-signed (development)
grpcurl -insecure localhost:5000 list

# Trust the self-signed certificate created in the TLS guide
grpcurl -cacert keys/server.crt localhost:5000 list

Using buf curl with Reflection ​

buf curl supports ConnectRPC protocol and can use reflection:

bash
# List services
buf curl --protocol connect --http2-prior-knowledge \
  http://localhost:5000 --list-services

# Call a method
buf curl --protocol connect --http2-prior-knowledge \
  -d '{"name": "Alice"}' \
  http://localhost:5000/greeter.v1.GreeterService/SayHello

Using Postman / Insomnia / Warthog ​

Postman, Insomnia, and Warthog support gRPC Server Reflection:

  1. Create a new gRPC request
  2. Enter the server URL: localhost:5000
  3. Click "Use Server Reflection" (or similar)
  4. The tool discovers and lists all available services and methods
  5. Select a method, fill in the request payload, and send

Conditional Reflection ​

Enable reflection only in non-production environments:

typescript
import { createServer } from '@connectum/core';
import { Healthcheck, healthcheckManager, ServingStatus } from '@connectum/healthcheck';
import { Reflection } from '@connectum/reflection';

const protocols = [Healthcheck({ httpEnabled: true })];

// Only enable reflection in development
if (process.env.NODE_ENV !== 'production') {
  protocols.push(Reflection());
}

const server = createServer({
  services: [routes],
  port: 5000,
  protocols,
});

server.on('ready', () => {
  healthcheckManager.update(ServingStatus.SERVING);
});

await server.start();

Adding Reflection at Runtime ​

You can add reflection before starting the server using addProtocol():

typescript
const server = createServer({
  services: [routes],
  port: 5000,
});

// Conditionally add reflection
if (config.enableReflection) {
  server.addProtocol(Reflection());
}

await server.start();

WARNING

addProtocol() can only be called before server.start(). Attempting to add protocols after the server has started throws an error.

Complete Example ​

typescript
import { createServer } from '@connectum/core';
import { Healthcheck, healthcheckManager, ServingStatus } from '@connectum/healthcheck';
import { Reflection } from '@connectum/reflection';
import { createDefaultInterceptors } from '@connectum/interceptors';
import { greeterServiceRoutes } from './services/greeterService.ts';
import { orderServiceRoutes } from './services/orderService.ts';

const server = createServer({
  services: [greeterServiceRoutes, orderServiceRoutes],
  port: 5000,
  allowHTTP1: false,
  protocols: [
    Healthcheck({ httpEnabled: true }),
    Reflection(),
  ],
  interceptors: createDefaultInterceptors(),
  shutdown: { autoShutdown: true },
});

server.on('ready', () => {
  const port = server.address?.port;
  console.log(`Server ready on port ${port}`);
  console.log(`  grpcurl -plaintext localhost:${port} list`);
  healthcheckManager.update(ServingStatus.SERVING);
});

await server.start();

After starting, verify everything is discoverable:

bash
# List all services
grpcurl -plaintext localhost:5000 list
# greeter.v1.GreeterService
# order.v1.OrderService
# grpc.health.v1.Health

# Describe the order service
grpcurl -plaintext localhost:5000 describe order.v1.OrderService

collectFileProtos Utility ​

The collectFileProtos utility function is also exported for advanced use cases. It collects file descriptor protos with their dependency tree:

typescript
import { collectFileProtos } from '@connectum/reflection';

This is used internally by the Reflection() factory to collect the descriptors it serves.

Protocol Registration Details ​

Under the hood, Reflection() returns a ProtocolRegistration object:

typescript
{
  name: 'reflection',
  setup(context) {
    // Before the first register: keeps the snapshot of what was registered before Reflection
  },
  register(router) {
    // Once per router: mounts the v1 and v1alpha reflection services; they read the index per request
  },
  ready(context) {
    // Once, after the last protocol has registered: context.services and
    // context.registry hold every mounted service and file; indexes their descriptors
  },
}

The context passed to ready is a snapshot taken by @connectum/core after the last protocol has registered, so the listing does not depend on the order of protocols. Because the index is built once in ready and shared by every router, HTTP clients and in-process clients (server.localClient(), ctx.call) see the same listing. If the first route materialization fails, the retry runs setup and ready again and rebuilds the index.

A protocol that wraps Reflection() and delegates to it must forward ready as well as setup and register. A wrapper that forwards only setup and register gets the listing older versions produced: the services registered before the wrapper.