skip to content

What can a caller look up through gRPC server reflection, and what does the server send back?

level: middleimportance: should knowfreq 50%

answer

  1. the server hands out its own contract
  2. ask by file name or by symbol
  3. descriptors come back, not source text
  4. you get the imports along with it
  5. one open call carries every lookup

basics

~20 s

Server reflection answers descriptor lookups at runtime: a file by name, the file containing a symbol, the file containing an extension of a type, and the extension numbers known for a type. The server returns file descriptors, including the transitive imports of what was asked for.

solid answer

~50 s

Reflection is a service a gRPC server exposes so that callers can obtain its contract from the server itself rather than from a build. Four lookups are defined: by **file name**, by **symbol** (a fully qualified service, method, message or enum, answered with the file that declares it), by **extension** of a given type and number, and for the **extension numbers** a server knows for a type. What comes back is descriptor data — `FileDescriptorProto` messages — for the requested file together with its transitive imports, because a descriptor is only usable once every type it mentions can be resolved. The exchange is a bidirectional streaming call, so a caller can carry many related lookups on one open call and have them all answered by the same server. This is what lets a generic debugging client compose a request for a method it was never compiled against.

code

pseudocode · 10 lines
pseudocode
call = open a bidirectional call to the reflection service

send    lookup by file name: "seedvault/catalogue.proto"
receive file descriptors: that file + every file it imports

send    lookup by symbol: "seedvault.catalogue.v1.Catalogue.GetAccession"
receive file descriptors: the file declaring that symbol + its imports

// both answers came from the one server holding this call
close the send direction; the call ends

go deeper

for a junior

Know that a gRPC server can hand out its own contract at runtime, which is how a tool with no generated code can still call it. Naming that capability and what it returns is enough here.

for a middle

Describe the lookups — by file name, by symbol, by extension — and say that descriptors come back with their transitive imports because a descriptor is only usable once its types resolve.

for a senior

Explain why the exchange is one bidirectional stream: several lookups on one call, answered by one server. Then name what reflection does not promise — no complete inventory, no authorisation, and a description of the answering instance only.

for a principal

The call to own is whether reflection is registered outside a debugging context at all. It trades operator convenience against publishing the full shape of an API to anything that can reach the port, and that is an estate-wide default, not a per-service whim.

## What reflection is for The normal gRPC path is contract-first: a schema is compiled at build time, both ends are generated from it, and the caller's binary already contains the types. That falls apart the moment someone needs to talk to a server they have no build for — a curator debugging the catalogue service from a laptop that has never seen its schema, an operator poking at a service during an incident, a generic tool that must work against whatever is in front of it. Server reflection closes that gap by making the contract itself something you can ask the server for at runtime. The server registers a reflection service beside its own, and a caller queries it for descriptors. ## The four lookups A caller can ask for: - **A file by name** — give the path of a schema file as the server knows it, get that file's descriptor. - **The file containing a symbol** — give a fully qualified name (a service, a method, a message, an enum), get the file that declares it. This is the lookup a debugging client uses most, because the symbol is the one thing it already knows: it is the same name that addresses the method. - **The file containing an extension** — give a type and an extension field number, get the file that defines that extension. - **The extension numbers known for a type** — get the set of extension numbers the server is aware of for a given type. The last two exist because a descriptor for a message does not, on its own, tell you what has been declared as an extension of it elsewhere; that knowledge lives in whichever file declared the extension. ## What comes back, and why the imports come with it The answer is **descriptor data**, not source: `FileDescriptorProto` messages, the compiled form of a schema file. There is no `.proto` text, no comments, no formatting — the server never held those. Crucially, the answer carries the requested file **together with its transitive imports**. This is not a convenience, it is a necessity. A file that imports another for a message type or an enum is meaningless on its own: a field typed as a message from an imported file cannot be decoded, or even described, without that file. Returning the closure lets the caller assemble a complete, resolvable type pool from one lookup instead of chasing each import as a new round trip. ## Why the exchange is one bidirectional stream Reflection is not a unary method. The caller opens a bidirectional streaming call and sends lookup requests on it, reading answers as they come. Two reasons: 1. **A session is many lookups.** A caller usually chases several related things — a symbol, then another symbol referenced by the first — and carrying them on one open call avoids per-call setup for each one. 2. **Affinity.** One call is one stream on one connection, and therefore one server. With separate unary calls, a client-side balancer is free to scatter them across a pool, and in a fleet running more than one build the answers might not describe the same schema. Keeping a session on one call keeps its answers mutually consistent. ## What reflection does not promise Three limits worth stating, because each is assumed away regularly: - **It is not an inventory guarantee.** A server is under no obligation to expose its entire surface through reflection; what you can look up is what that build registered. - **It grants nothing.** Reflection describes a contract; it does not authorise a call. Every request a caller composes from a descriptor still goes through the server's ordinary checks. - **It describes the answering server.** A descriptor is a fact about the instance that produced it, not about a service name in the abstract — which is what makes reflection against a rolling fleet subtle. ## Versions, and whether to expose it Reflection shipped for years in the package `grpc.reflection.v1alpha`, and a stable `v1` package with the same lookups was added later. A server may register one, the other, or both, so a generic client commonly tries the stable package and falls back to the alpha one. Say which you assume when you answer, because a server built a few years ago may serve only the alpha package. Whether to register reflection at all is a judgement. It publishes the complete shape of an API — every service, method and message — to anything that can reach the port, which is precisely what someone probing a service wants. Treating it as a default-on convenience is a decision made by not making one.

  • Why does a reflection answer include the imports of the file that was asked for?
    A descriptor is only useful if every type it mentions can be resolved. A file that imports another for a message type or an enum describes nothing on its own, so the server returns the requested file together with its transitive imports. The caller can then assemble a complete type pool in one exchange instead of discovering a missing import and issuing another lookup for it.
  • Why is reflection a bidirectional streaming call rather than a series of unary ones?
    Two reasons. A caller normally needs several related lookups, and one open call avoids the setup cost of each. More importantly the call is one stream on one connection, so every lookup in a session is answered by the same server; separate unary calls could be scattered across a pool by a client-side balancer, and in a fleet running two builds the answers might not agree.
  • Does reflection let a caller invoke a method it would otherwise be refused?
    No. Reflection describes the contract and grants nothing. A caller that learns a method's name and message shapes still has to make an ordinary call, which passes through exactly the same checks as a call from a compiled client. What reflection does change is how much of the surface an unauthenticated peer can see, which is a reason to decide deliberately whether to register it.

saying these in an interview costs you the question

  • Thinks reflection returns .proto source text.
  • Expects a single descriptor with no imports and then cannot resolve a field's type.
  • Assumes every server that speaks gRPC answers reflection lookups.
  • Believes reflection lets a caller bypass the server's authorisation checks.
  • Treats a reflection answer as a guaranteed complete inventory of the server's surface.