connectrpc_reflection/lib.rs
1//! gRPC server reflection for `connectrpc`.
2//!
3//! Wire-compatible with [`grpc.reflection.v1.ServerReflection`] and its
4//! `v1alpha` predecessor, so `grpcurl`, `buf curl`, Postman, `grpcui`,
5//! and every other reflection-aware client just works — over gRPC,
6//! gRPC-Web, and the Connect protocol alike.
7//!
8//! # Quick start
9//!
10//! Emit a descriptor set from your build script alongside code
11//! generation:
12//!
13//! ```ignore
14//! // build.rs
15//! connectrpc_build::Config::new()
16//! .files(&["proto/app.proto"])
17//! .includes(&["proto/"])
18//! .emit_descriptor_set("app.fds.bin")
19//! .compile()
20//! .unwrap();
21//! ```
22//!
23//! then embed it and mount the service:
24//!
25//! ```no_run
26//! use connectrpc::Router;
27//! use connectrpc_reflection::{Reflector, install};
28//!
29//! // In real code: include_bytes!(concat!(env!("OUT_DIR"), "/app.fds.bin"))
30//! # fn descriptor_set_bytes() -> &'static [u8] { &[] }
31//! let reflector = Reflector::from_descriptor_set_bytes(descriptor_set_bytes()).unwrap();
32//! let router = install(Router::new(), reflector);
33//! ```
34//!
35//! [`install`] registers both protocol versions; use the generated
36//! extension traits directly if you want only one (and see
37//! [Request limits](#request-limits) below for the extra call that needs).
38//!
39//! Alternatively, when your buffa codegen has reflection enabled, skip
40//! the build-script step and serve straight from the generated package's
41//! descriptor pool:
42//!
43//! ```ignore
44//! let reflector =
45//! Reflector::from_descriptor_pool(myapp::proto::descriptor_pool().clone()).unwrap();
46//! ```
47//!
48//! The bytes path needs only `emit_descriptor_set` — reflection codegen
49//! is **not** required — and answers with the compiler's original
50//! per-file descriptor bytes; the pool path re-encodes (semantically
51//! faithful, unknown fields preserved). See [`Reflector`] for the
52//! trade-off.
53//!
54//! # Request limits
55//!
56//! A `ServerReflectionRequest` is a host plus one symbol, file name or type
57//! name, so the reflection routes do not need the multi-megabyte request
58//! ceiling a `connectrpc` service allows by default. This crate sizes them
59//! to [`MAX_REQUEST_BYTES`] (16 KiB) per request *message* through per-route
60//! [`Limits`](connectrpc::Limits) — see [`request_limits`] for the exact
61//! profile; `ServerReflectionInfo` is a bidirectional stream, so the bound
62//! is per message rather than per call. A larger message ends the stream
63//! with `resource_exhausted`. The profile *replaces* the service-wide limits
64//! on these routes, whether those are looser or tighter.
65//!
66//! * [`install`] applies [`request_limits`] for you, to both versions.
67//! * Registering a [`ReflectionService`] any other way — one version through
68//! the generated [`ServerReflectionExt::register`](ServerReflectionExt), or
69//! [`Router::add_service`](connectrpc::Router::add_service) — does not, so
70//! follow it with [`apply_request_limits`]`(router, `[`request_limits`]`())`;
71//! it covers whichever versions are mounted.
72//! * To tune the reflection routes specifically, call
73//! [`apply_request_limits`] with your own `Limits` after either path; the
74//! later call wins.
75//!
76//! ```no_run
77//! use connectrpc::{Limits, Router};
78//! use connectrpc_reflection::{Reflector, apply_request_limits, install};
79//!
80//! # fn descriptor_set_bytes() -> &'static [u8] { &[] }
81//! let reflector = Reflector::from_descriptor_set_bytes(descriptor_set_bytes()).unwrap();
82//! let router = install(Router::new(), reflector);
83//! // Optional: hold reflection messages to 1 KiB instead of the bundled 16 KiB.
84//! let router = apply_request_limits(router, Limits::default().with_max_message_size(1024));
85//! # drop(router);
86//! ```
87//!
88//! # What gets exposed
89//!
90//! Everything in the descriptor set: all files, their transitive
91//! imports, and every service compiled into it — whether or not the
92//! corresponding handlers are mounted on the router. Use
93//! [`Reflector::with_services`] to curate the advertised service list,
94//! and [`Reflector::service_names`] to inspect it. Build the set from
95//! the same protos you serve, and remember that reflection
96//! intentionally publishes your schema: gate or omit the service on
97//! deployments where that is not wanted.
98//!
99//! The reflection service is **self-describing**: queries about
100//! `grpc.reflection.*` fall back to the crate's own descriptors, and
101//! `ListServices` advertises the reflection services alongside yours.
102//! This matches grpc-go (where the reflection proto is always
103//! registered) and is what schema-free callers like `buf curl` need to
104//! invoke `ServerReflectionInfo` directly. Use
105//! [`Reflector::with_services`] to advertise a different list — the
106//! override is verbatim, so omitting the reflection names de-lists them
107//! (they stay resolvable as symbols).
108//!
109//! # Cargo features
110//!
111//! * **`client`** (on by default) — re-exports the generated
112//! `ServerReflectionClient` for querying a reflection server
113//! (integration tests, CLI tooling). Pulls in `connectrpc`'s `client`
114//! feature; server-only deployments opt out with
115//! `default-features = false`.
116//!
117//! [`grpc.reflection.v1.ServerReflection`]: https://github.com/grpc/grpc-proto/blob/master/grpc/reflection/v1/reflection.proto
118#![cfg_attr(docsrs, feature(doc_cfg))]
119
120mod reflector;
121mod service;
122
123#[path = "generated/connect/mod.rs"]
124mod connect;
125// `message_response`'s variants all end in `Response` (proto field names);
126// buffa 0.7's generated allow-list does not yet cover this lint firing on
127// oneofs.
128#[allow(clippy::enum_variant_names)]
129#[path = "generated/buffa/mod.rs"]
130mod proto;
131
132pub use reflector::{ReflectionError, Reflector};
133pub use service::{
134 MAX_REQUEST_BYTES, ReflectionService, apply_request_limits, install, request_limits,
135};
136
137/// The wire-format `FileDescriptorSet` for this crate's protos
138/// (`grpc.reflection.v1` and `v1alpha`, from the public Buf Schema
139/// Registry's `buf.build/grpc/grpc` module).
140///
141/// Every [`Reflector`] already consults these descriptors as a built-in
142/// fallback, so the reflection service describes and lists itself with
143/// no setup. The constant is exposed for other uses — e.g. registering
144/// the reflection schema with a different protobuf runtime, the way
145/// tonic-reflection's constant of the same name is consumed.
146pub const FILE_DESCRIPTOR_SET: &[u8] = include_bytes!("../descriptor/reflection.fds.bin");
147
148/// Fully-qualified name of the v1 reflection service.
149pub use connect::grpc::reflection::v1::SERVER_REFLECTION_SERVICE_NAME;
150/// Generated v1 service trait and registration extension, for callers
151/// that mount a single protocol version by hand:
152///
153/// ```no_run
154/// use std::sync::Arc;
155/// use connectrpc::Router;
156/// use connectrpc_reflection::{Reflector, ReflectionService, ServerReflectionExt};
157///
158/// # fn descriptor_set_bytes() -> &'static [u8] { &[] }
159/// let reflector = Reflector::from_descriptor_set_bytes(descriptor_set_bytes()).unwrap();
160/// let service = Arc::new(ReflectionService::new(reflector));
161/// // v1 only; `apply_request_limits` bounds whichever versions are mounted.
162/// let router = service.register(Router::new());
163/// let router = connectrpc_reflection::apply_request_limits(
164/// router,
165/// connectrpc_reflection::request_limits(),
166/// );
167/// ```
168pub use connect::grpc::reflection::v1::{ServerReflection, ServerReflectionExt};
169
170/// Fully-qualified name of the v1alpha reflection service.
171pub use connect::grpc::reflection::v1alpha::SERVER_REFLECTION_SERVICE_NAME as SERVER_REFLECTION_V1ALPHA_SERVICE_NAME;
172/// Generated v1alpha service trait and registration extension, renamed to
173/// avoid colliding with the v1 items, for callers that mount the legacy
174/// protocol version by hand.
175pub use connect::grpc::reflection::v1alpha::{
176 ServerReflection as ServerReflectionV1alpha, ServerReflectionExt as ServerReflectionV1alphaExt,
177};
178
179/// Generated client for querying a `grpc.reflection.v1.ServerReflection`
180/// server.
181#[cfg(feature = "client")]
182#[cfg_attr(docsrs, doc(cfg(feature = "client")))]
183pub use connect::grpc::reflection::v1::ServerReflectionClient;
184
185/// Re-exports of the generated `grpc.reflection.*` wire types — request
186/// and response messages, their oneof modules, and the method `Spec`
187/// constants. Everything a downstream crate needs to drive
188/// `ServerReflectionClient` (gated on the `client` feature) or inspect
189/// responses without regenerating the protos.
190pub mod wire {
191 /// `grpc.reflection.v1` wire types.
192 pub mod v1 {
193 pub use crate::connect::grpc::reflection::v1::SERVER_REFLECTION_SERVER_REFLECTION_INFO_SPEC;
194 pub use crate::proto::grpc::reflection::v1::{
195 ErrorResponse, ExtensionNumberResponse, ExtensionRequest, FileDescriptorResponse,
196 ListServiceResponse, ServerReflectionRequest, ServerReflectionResponse,
197 ServiceResponse, server_reflection_request, server_reflection_response,
198 };
199 }
200 /// `grpc.reflection.v1alpha` wire types.
201 pub mod v1alpha {
202 pub use crate::connect::grpc::reflection::v1alpha::SERVER_REFLECTION_SERVER_REFLECTION_INFO_SPEC;
203 pub use crate::proto::grpc::reflection::v1alpha::{
204 ErrorResponse, ExtensionNumberResponse, ExtensionRequest, FileDescriptorResponse,
205 ListServiceResponse, ServerReflectionRequest, ServerReflectionResponse,
206 ServiceResponse, server_reflection_request, server_reflection_response,
207 };
208 }
209}