Skip to main content

structured_proxy/transcode/
mod.rs

1//! REST→gRPC transcoding layer.
2//!
3//! Reads `google.api.http` annotations from proto service descriptors
4//! and builds axum routes that proxy JSON/form requests to gRPC upstream.
5//! The upstream decides the HTTP answer beyond the JSON body where it needs
6//! to: its response metadata becomes response headers, `x-http-code` sets the
7//! status of a successful unary call, and `google.api.HttpBody` carries a raw
8//! body in either direction.
9//!
10//! Generic: works with ANY proto descriptor set. No product-specific code.
11
12pub mod body;
13pub mod codec;
14pub mod error;
15pub(crate) mod httpbody;
16pub mod metadata;
17pub mod request;
18pub(crate) mod response;
19pub(crate) mod rule;
20mod select;
21
22pub use select::RpcSelection;
23
24use axum::body::{Body, Bytes};
25use axum::extract::{Path, RawQuery, State};
26use axum::http::header::{ALLOW, CONTENT_TYPE};
27use axum::http::{HeaderMap, HeaderName, HeaderValue, Method, StatusCode};
28use axum::response::sse::{Event, KeepAlive, Sse};
29use axum::response::{IntoResponse, Response};
30use axum::routing::{MethodFilter, MethodRouter};
31use axum::Extension;
32use axum::Router;
33use futures::{StreamExt, TryStreamExt};
34use prost_reflect::{
35    DescriptorPool, DynamicMessage, FieldDescriptor, MessageDescriptor, MethodDescriptor,
36    SerializeOptions,
37};
38use std::collections::HashMap;
39use std::sync::Arc;
40use std::time::Duration;
41use tonic::client::Grpc;
42use tonic::metadata::MetadataMap;
43
44use crate::client_address::ClientAddress;
45use crate::config::AliasConfig;
46use crate::service::ConnectionInfo;
47use crate::upstream::Upstream;
48use error::{ErrorDetailsPolicy, StatusDetails};
49use response::UpstreamHeaders;
50use rule::RouteMethod;
51
52/// How long the upstream may take to answer a call with its response headers
53/// when the client's `grpc-timeout` asks for no shorter deadline.
54pub const UPSTREAM_DEADLINE: Duration = Duration::from_secs(5);
55
56/// Trait for state types that support REST→gRPC transcoding.
57///
58/// Implement this for your application's state type to use `transcode::routes()`.
59/// Provides the minimal interface needed by transcode handlers. Each request
60/// works on its own clone of the state, so a clone should be cheap.
61pub trait TranscodeState: Clone + Send + Sync + 'static {
62    /// The gRPC service transcoded calls go to.
63    type Upstream: Upstream;
64    /// The upstream, taken out of this request's clone of the state.
65    fn into_upstream(self) -> Self::Upstream;
66    /// Headers to forward from HTTP to gRPC metadata.
67    fn forwarded_headers(&self) -> &[String];
68    /// SSE keep-alive interval (seconds) for server-streaming responses.
69    fn sse_keep_alive_secs(&self) -> u64;
70}
71
72impl<U: Upstream> TranscodeState for crate::ProxyState<U> {
73    type Upstream = U;
74    fn into_upstream(self) -> U {
75        self.upstream
76    }
77    fn forwarded_headers(&self) -> &[String] {
78        &self.forwarded_headers
79    }
80    fn sse_keep_alive_secs(&self) -> u64 {
81        self.sse_keep_alive_secs
82    }
83}
84
85/// Path parameters of a matched route.
86type PathParams = HashMap<String, String>;
87
88/// How the HTTP request body reaches the RPC's input message.
89#[derive(Debug, Clone)]
90enum RequestBody {
91    /// JSON or a form, mapped as the rule's `body` says.
92    Parsed(request::BodyMapping),
93    /// Raw bytes and `Content-Type` into the input message, a `google.api.HttpBody`.
94    RawRoot,
95    /// Raw bytes and `Content-Type` into the HttpBody field `body` names; the
96    /// other fields still come from path and query.
97    RawField {
98        /// The rule's `body` mapping, which keeps the field out of query binding.
99        mapping: request::BodyMapping,
100        field: FieldDescriptor,
101        /// The field's type, a `google.api.HttpBody`.
102        http_body: MessageDescriptor,
103    },
104}
105
106/// What the HTTP response body is made of.
107#[derive(Debug, Clone)]
108enum ResponseShape {
109    /// ProtoJSON of the response message, or of its `response_body` subfield.
110    Json(Option<String>),
111    /// The raw body of a `google.api.HttpBody`: the response message itself
112    /// (empty path) or the field chain `response_body` names.
113    HttpBody(Vec<FieldDescriptor>),
114}
115
116/// Route entry extracted from proto HTTP annotations.
117#[derive(Debug, Clone)]
118struct RouteEntry {
119    /// HTTP path pattern (e.g., "/v1/auth/opaque/login/start").
120    http_path: String,
121    /// The HTTP method(s) the binding answers.
122    http_method: RouteMethod,
123    /// gRPC path (e.g., "/sid.v1.AuthService/OpaqueLoginStart"), parsed once at
124    /// route-build time so each request clones a cheap `Bytes` refcount.
125    grpc_path: axum::http::uri::PathAndQuery,
126    /// Method descriptor for input/output message resolution.
127    method: MethodDescriptor,
128    /// Server-streaming RPC (NDJSON / SSE, or chunked HttpBody).
129    streaming: bool,
130    /// How the request body maps onto the gRPC request message.
131    request_body: RequestBody,
132    /// What the HTTP response body is made of.
133    response: ResponseShape,
134    /// Renderer for the status details of this route's errors; `None` when the
135    /// error-details policy switches them off for the route.
136    error_details: Option<Arc<StatusDetails>>,
137    /// Wrap NDJSON stream lines in `{"result"}` / `{"error"}` envelopes.
138    ndjson_envelope: bool,
139    /// [`codec::has_required_fields`] of the response type, computed once so a
140    /// request does not walk the descriptor.
141    response_has_required: bool,
142    /// Response metadata keys the operator keeps off the HTTP response, on top
143    /// of the ones that never go there.
144    denied_headers: Arc<[HeaderName]>,
145}
146
147impl RouteEntry {
148    fn codec(&self) -> codec::DynamicCodec {
149        codec::DynamicCodec::with_required_check(self.method.output(), self.response_has_required)
150    }
151
152    /// The raw body of `message`, on a route that answers with an HttpBody.
153    fn raw_body(&self, message: DynamicMessage) -> Option<httpbody::RawBody> {
154        match &self.response {
155            ResponseShape::HttpBody(path) => Some(httpbody::take(message, path)),
156            ResponseShape::Json(_) => None,
157        }
158    }
159}
160
161/// How [`routes_with_options`] builds the transcoded routes.
162///
163/// # Examples
164///
165/// ```
166/// use axum::http::HeaderName;
167/// use structured_proxy::transcode::error::ErrorDetailsPolicy;
168/// use structured_proxy::transcode::TranscodeOptions;
169///
170/// let options = TranscodeOptions::default()
171///     .with_error_details(ErrorDetailsPolicy::default().route("/v1/admin/**", false).unwrap())
172///     .with_ndjson_envelope(true)
173///     .with_denied_response_headers([HeaderName::from_static("x-debug-trace")]);
174/// # let _ = options;
175/// ```
176#[derive(Debug, Clone, Default)]
177pub struct TranscodeOptions {
178    pub(crate) error_details: ErrorDetailsPolicy,
179    pub(crate) ndjson_envelope: bool,
180    pub(crate) denied_response_headers: Arc<[HeaderName]>,
181    pub(crate) selection: RpcSelection,
182}
183
184impl TranscodeOptions {
185    /// Transcode only the RPCs `selection` names (every annotated one by
186    /// default).
187    pub fn with_selection(mut self, selection: RpcSelection) -> Self {
188        self.selection = selection;
189        self
190    }
191
192    /// Which routes return `google.rpc.Status` details in their error bodies
193    /// (all of them by default).
194    pub fn with_error_details(mut self, policy: ErrorDetailsPolicy) -> Self {
195        self.error_details = policy;
196        self
197    }
198
199    /// Wrap every NDJSON line of a server-streaming response in an envelope:
200    /// `{"result": <message>}` for data, `{"error": <error body>}` for the
201    /// terminal error (the grpc-gateway stream shape). Off by default, when a
202    /// data line is the bare message and the error line carries an
203    /// `@type: google.rpc.Status` marker instead. Only the envelope keeps the
204    /// two apart for RPCs that stream `google.protobuf.Any`, `Struct`, `Value`
205    /// or `ListValue`, whose messages can carry any keys. SSE is unaffected.
206    pub fn with_ndjson_envelope(mut self, enabled: bool) -> Self {
207        self.ndjson_envelope = enabled;
208        self
209    }
210
211    /// Response metadata keys that never become HTTP response headers, on top
212    /// of gRPC's own keys, hop-by-hop fields and `x-http-code`, which never do.
213    /// Replaces any list set before. Use it to keep internal headers (debug
214    /// traces, backend names) off a public edge; an upstream key not listed
215    /// here reaches the client.
216    pub fn with_denied_response_headers(
217        mut self,
218        names: impl IntoIterator<Item = HeaderName>,
219    ) -> Self {
220        self.denied_response_headers = names.into_iter().collect();
221        self
222    }
223}
224
225/// Build transcoded REST→gRPC routes from a descriptor pool.
226///
227/// Takes a `DescriptorPool` and optional path aliases from config.
228/// Returns an axum Router that transcodes REST requests to gRPC calls. Error
229/// bodies carry `google.rpc.Status` details on every route; use
230/// [`routes_with_options`] to choose per route or to frame NDJSON streams.
231pub fn routes<S: TranscodeState>(pool: &DescriptorPool, aliases: &[AliasConfig]) -> Router<S> {
232    routes_with_options(pool, aliases, &TranscodeOptions::default())
233}
234
235/// [`routes`], built as `options` describe.
236///
237/// A second binding for a method and path already taken (or any binding on a
238/// path a `custom` `*` rule takes, which answers every method) is skipped with
239/// an error in the log.
240pub fn routes_with_options<S: TranscodeState>(
241    pool: &DescriptorPool,
242    aliases: &[AliasConfig],
243    options: &TranscodeOptions,
244) -> Router<S> {
245    let bindings = route_bindings(pool, aliases, &options.selection);
246    if bindings.is_empty() {
247        tracing::warn!("No HTTP-annotated RPCs found in proto descriptors");
248        return Router::new();
249    }
250
251    tracing::info!("Registering {} transcoded REST→gRPC routes", bindings.len());
252
253    // At most two renderers, without and with the opaque-detail extension,
254    // each shared by every route that uses it and built only when one does.
255    // The second is a copy of the first: the descriptor pool inside is shared.
256    let mut status_details: [Option<Arc<StatusDetails>>; 2] = [None, None];
257    // Every binding of one path goes into the same method router, in binding
258    // order.
259    let mut paths: Vec<PathRoutes> = Vec::new();
260    let mut path_index: HashMap<String, usize> = HashMap::new();
261    for mut binding in bindings {
262        let policy = &options.error_details;
263        if policy.enabled_for(&binding.axum_path) {
264            let opaque = policy.opaque_for(&binding.axum_path);
265            let slot = usize::from(opaque);
266            if status_details[slot].is_none() {
267                let base = status_details
268                    .iter()
269                    .flatten()
270                    .next()
271                    .map(|existing| existing.as_ref().clone())
272                    .unwrap_or_else(|| StatusDetails::new(pool));
273                status_details[slot] = Some(Arc::new(base.with_opaque_details(opaque)));
274            }
275            binding.entry.error_details = status_details[slot].clone();
276        }
277        binding.entry.ndjson_envelope = options.ndjson_envelope;
278        binding.entry.denied_headers = options.denied_response_headers.clone();
279        let index = match path_index.get(&binding.axum_path) {
280            Some(&index) => index,
281            None => {
282                path_index.insert(binding.axum_path.clone(), paths.len());
283                paths.push(PathRoutes {
284                    path: binding.axum_path,
285                    methods: Vec::new(),
286                });
287                paths.len() - 1
288            }
289        };
290        paths[index].add(Arc::new(binding.entry));
291    }
292
293    let mut router: Router<S> = Router::new();
294    for path in &paths {
295        router = router.route(&path.path, path.method_router());
296    }
297    router
298}
299
300/// The axum handler serving the route entry `$entry`, for the state type `S`
301/// in scope. A macro because the closure's handler type cannot be named.
302macro_rules! endpoint {
303    ($entry:expr) => {{
304        let entry: Arc<RouteEntry> = $entry;
305        move |state: State<S>,
306              headers: HeaderMap,
307              path_params: Path<PathParams>,
308              raw_query: RawQuery,
309              connection: Option<Extension<ConnectionInfo>>,
310              address: Option<Extension<ClientAddress>>,
311              body: Bytes| {
312            let client = Client {
313                headers,
314                origin: Origin {
315                    connection: connection.map(|Extension(connection)| connection),
316                    address: address.map(|Extension(address)| address),
317                },
318            };
319            handle(state, client, path_params, raw_query, body, entry)
320        }
321    }};
322}
323
324/// The bindings mounted at one axum path.
325struct PathRoutes {
326    path: String,
327    methods: Vec<Arc<RouteEntry>>,
328}
329
330impl PathRoutes {
331    /// Add `entry` unless its method is already answered on this path.
332    fn add(&mut self, entry: Arc<RouteEntry>) {
333        let taken = self.methods.iter().any(|existing| {
334            existing.http_method == entry.http_method
335                || existing.http_method == RouteMethod::Any
336                || entry.http_method == RouteMethod::Any
337        });
338        if taken {
339            tracing::error!(
340                method = entry.http_method.as_str(),
341                path = %self.path,
342                rpc = %entry.grpc_path,
343                "HTTP method and path already bound to another RPC; skipping this binding"
344            );
345            return;
346        }
347        self.methods.push(entry);
348    }
349
350    /// One method router for every binding of the path. Methods axum routes by
351    /// itself are registered directly; any other token (a `custom` rule such
352    /// as `PROPFIND`) is dispatched by a fallback that answers `405` with the
353    /// full `Allow` list (RFC 9110 §15.5.6) for a method nobody binds.
354    fn method_router<S: TranscodeState>(&self) -> MethodRouter<S> {
355        let mut router = MethodRouter::new();
356        let mut extension: Vec<(Method, Arc<RouteEntry>)> = Vec::new();
357        let mut allow: Vec<&str> = Vec::new();
358        for entry in &self.methods {
359            match &entry.http_method {
360                // `add` keeps a `*` binding alone on its path.
361                RouteMethod::Any => return axum::routing::any(endpoint!(entry.clone())),
362                RouteMethod::One(method) => {
363                    allow.push(method.as_str());
364                    match MethodFilter::try_from(method.clone()) {
365                        Ok(filter) => router = router.on(filter, endpoint!(entry.clone())),
366                        Err(_) => extension.push((method.clone(), entry.clone())),
367                    }
368                }
369            }
370        }
371        if extension.is_empty() {
372            return router;
373        }
374        // A GET route answers HEAD too.
375        if allow.contains(&"GET") && !allow.contains(&"HEAD") {
376            allow.push("HEAD");
377        }
378        let allow = HeaderValue::from_str(&allow.join(", "))
379            .expect("method tokens are valid header value characters");
380        let extension: Arc<[(Method, Arc<RouteEntry>)]> = extension.into();
381        // The method decides before anything is extracted, so a request no
382        // binding answers gets its 405 without its body being read.
383        router.fallback(
384            move |State(state): State<S>, request: axum::extract::Request| async move {
385                match extension
386                    .iter()
387                    .find(|(bound, _)| bound == request.method())
388                {
389                    Some((_, entry)) => {
390                        axum::handler::Handler::call(endpoint!(entry.clone()), request, state).await
391                    }
392                    None => (StatusCode::METHOD_NOT_ALLOWED, [(ALLOW, allow)]).into_response(),
393                }
394            },
395        )
396    }
397}
398
399/// One transcode route to mount: the RPC entry that serves it and the axum path
400/// to register it at.
401struct RouteBinding {
402    entry: RouteEntry,
403    axum_path: String,
404}
405
406/// The single source of truth for what [`routes`] mounts: every binding of
407/// every unary and server-streaming RPC, plus its config aliases. Both
408/// [`routes`] (to build handlers) and [`route_paths`] (to enumerate paths for
409/// collision checks) consume this, so the mounted set and the enumerated set
410/// cannot drift apart.
411fn route_bindings(
412    pool: &DescriptorPool,
413    aliases: &[AliasConfig],
414    selection: &RpcSelection,
415) -> Vec<RouteBinding> {
416    let mut bindings = Vec::new();
417    for entry in extract_routes(pool, selection) {
418        for alias in aliases {
419            if let Some(suffix) = entry.http_path.strip_prefix(&alias.to) {
420                if alias.from.ends_with("/{path}") {
421                    let prefix = alias.from.trim_end_matches("/{path}");
422                    bindings.push(RouteBinding {
423                        axum_path: proto_path_to_axum(&format!("{prefix}{suffix}")),
424                        entry: entry.clone(),
425                    });
426                }
427            }
428        }
429        bindings.push(RouteBinding {
430            axum_path: proto_path_to_axum(&entry.http_path),
431            entry,
432        });
433    }
434    bindings
435}
436
437/// The axum paths [`routes_with_options`] would register for this pool,
438/// aliases and selection.
439///
440/// Mirrors the registration (every binding of every selected unary and
441/// server-streaming RPC, and their config aliases) without building handlers,
442/// so callers can detect route collisions before mounting additional routes
443/// (e.g. a forward-auth endpoint).
444///
445/// Each entry is `(method, path)` where `method` is the uppercase HTTP token,
446/// or `*` for a `custom` rule that answers every method, so callers can
447/// distinguish same-path/different-method routes from real conflicts.
448pub fn route_paths(
449    pool: &DescriptorPool,
450    aliases: &[AliasConfig],
451    selection: &RpcSelection,
452) -> Vec<(String, String)> {
453    route_bindings(pool, aliases, selection)
454        .into_iter()
455        .map(|b| (b.entry.http_method.as_str().to_string(), b.axum_path))
456        .collect()
457}
458
459/// JSON serialization options shared by the unary and streaming response paths,
460/// so a given message serializes identically regardless of RPC kind.
461fn response_serialize_options() -> SerializeOptions {
462    SerializeOptions::new()
463        .skip_default_fields(false)
464        .stringify_64_bit_integers(true)
465}
466
467/// Serialize a message straight to JSON bytes, without an intermediate tree.
468fn message_to_json_bytes(
469    msg: &DynamicMessage,
470    opts: &SerializeOptions,
471) -> Result<Vec<u8>, serde_json::Error> {
472    let mut buf = Vec::with_capacity(128);
473    msg.serialize_with_options(&mut serde_json::Serializer::new(&mut buf), opts)?;
474    Ok(buf)
475}
476
477/// Serialize one streamed gRPC message to a compact JSON string.
478fn message_to_json_string(msg: &DynamicMessage, opts: &SerializeOptions) -> Result<String, String> {
479    let buf = message_to_json_bytes(msg, opts).map_err(|e| e.to_string())?;
480    // SAFETY: serde_json's serializer writes only valid UTF-8, the same
481    // guarantee `serde_json::to_string` relies on.
482    Ok(unsafe { String::from_utf8_unchecked(buf) })
483}
484
485/// The unary response body as JSON: the whole message, or the subfield
486/// `response_body` names (JSON `null` when the path does not exist).
487fn json_body(
488    msg: &DynamicMessage,
489    response_body: Option<&str>,
490) -> Result<Vec<u8>, serde_json::Error> {
491    let opts = response_serialize_options();
492    let Some(path) = response_body else {
493        return message_to_json_bytes(msg, &opts);
494    };
495    // Walk the tree by moving each subtree out, so nothing is copied.
496    // `response_body` names proto fields, while the tree is keyed by their
497    // JSON names, so each segment is resolved through the descriptor.
498    let mut value = Some(msg.serialize_with_options(serde_json::value::Serializer, &opts)?);
499    let mut desc = Some(prost_reflect::ReflectMessage::descriptor(msg));
500    for segment in path.split('.') {
501        let field = desc.as_ref().and_then(|d| d.get_field_by_name(segment));
502        desc = match field.as_ref().map(FieldDescriptor::kind) {
503            Some(prost_reflect::Kind::Message(inner)) => Some(inner),
504            _ => None,
505        };
506        value = match (value, field) {
507            (Some(serde_json::Value::Object(mut fields)), Some(field)) => {
508                fields.remove(field.json_name())
509            }
510            _ => None,
511        };
512    }
513    let value = value.unwrap_or_else(|| {
514        tracing::warn!(
515            response_body = %path,
516            "configured response_body path not found in response; returning null"
517        );
518        serde_json::Value::Null
519    });
520    serde_json::to_vec(&value)
521}
522
523/// Whether the client negotiated a Server-Sent Events response via `Accept`.
524///
525/// Considers every `Accept` header line (a client may send more than one) and
526/// every comma-separated media range within each. Matches `text/event-stream`
527/// case-insensitively and honors the quality factor: per RFC 7231 §5.3.1 a
528/// `q=0` weight means the type is explicitly not acceptable, so it does not
529/// select the SSE path.
530fn wants_sse(headers: &HeaderMap) -> bool {
531    headers
532        .get_all(axum::http::header::ACCEPT)
533        .iter()
534        .filter_map(|v| v.to_str().ok())
535        .flat_map(|accept| accept.split(','))
536        .any(accept_range_selects_sse)
537}
538
539/// Whether a single `Accept` media range selects `text/event-stream` with a
540/// non-zero quality factor.
541fn accept_range_selects_sse(range: &str) -> bool {
542    let mut parts = range.split(';');
543    let media = parts.next().unwrap_or("").trim();
544    if !media.eq_ignore_ascii_case("text/event-stream") {
545        return false;
546    }
547    // Default weight is 1.0; only an explicit `q=0` (or unparseable-as-positive)
548    // disqualifies the match. A malformed weight falls back to acceptable.
549    for param in parts {
550        let mut kv = param.splitn(2, '=');
551        if kv.next().unwrap_or("").trim().eq_ignore_ascii_case("q") {
552            let q: f32 = kv.next().unwrap_or("").trim().parse().unwrap_or(1.0);
553            return q > 0.0;
554        }
555    }
556    true
557}
558
559/// Who sent a transcoded request: its headers and where it came from.
560struct Client {
561    headers: HeaderMap,
562    origin: Origin,
563}
564
565/// Where a transcoded request came from: the connection, when the server
566/// recorded one, and the client address the proxy resolved.
567struct Origin {
568    connection: Option<ConnectionInfo>,
569    address: Option<ClientAddress>,
570}
571
572/// Serve one request on a transcoded route.
573async fn handle<S: TranscodeState>(
574    State(proxy_state): State<S>,
575    client: Client,
576    Path(path_params): Path<PathParams>,
577    RawQuery(raw_query): RawQuery,
578    body: Bytes,
579    entry: Arc<RouteEntry>,
580) -> Response {
581    let keep_alive_secs = proxy_state.sse_keep_alive_secs();
582    let Client { headers, origin } = client;
583    let prepared = prepare(
584        proxy_state,
585        &headers,
586        origin,
587        &path_params,
588        raw_query.as_deref(),
589        body,
590        &entry,
591    );
592    let call = match prepared {
593        Ok(call) => call,
594        Err(rejection) => return rejection.into_response(&entry),
595    };
596    if entry.streaming {
597        streaming_call(call, entry, wants_sse(&headers), keep_alive_secs).await
598    } else {
599        unary_call(call, &entry).await
600    }
601}
602
603/// Why a request ends before the upstream is called: it cannot be mapped onto
604/// the RPC (`INVALID_ARGUMENT`, 400).
605struct Unmappable(String);
606
607impl Unmappable {
608    /// The answer, in the error body the upstream's own errors get on the route.
609    fn into_response(self, entry: &RouteEntry) -> Response {
610        let status = tonic::Status::invalid_argument(self.0);
611        error::status_to_response_with_details(&status, entry.error_details.as_deref())
612    }
613}
614
615/// A call ready to be made: the upstream, the request, and how long the
616/// upstream may take to answer it.
617struct Call<U> {
618    upstream: U,
619    request: tonic::Request<DynamicMessage>,
620    deadline: Duration,
621}
622
623impl<U: Upstream> Call<U> {
624    /// Wait for the upstream to take the call, start it, and wait for its
625    /// response headers, all within the one deadline: an upstream under
626    /// backpressure that never frees a slot answers `DEADLINE_EXCEEDED` like
627    /// one that never answers. Every upstream gets the same deadline here, in
628    /// process or remote, rather than whatever its transport enforces.
629    async fn open(
630        self,
631        entry: &RouteEntry,
632    ) -> Result<tonic::Response<tonic::Streaming<DynamicMessage>>, tonic::Status> {
633        let Self {
634            upstream,
635            request,
636            deadline,
637        } = self;
638        let call = async move {
639            let mut client = Grpc::new(upstream);
640            if let Err(e) = client.ready().await {
641                let e: crate::upstream::BoxError = e.into();
642                return Err(tonic::Status::unavailable(format!(
643                    "gRPC upstream not ready: {e}"
644                )));
645            }
646            client
647                .server_streaming(request, entry.grpc_path.clone(), entry.codec())
648                .await
649        };
650        match tokio::time::timeout(deadline, call).await {
651            Ok(result) => result,
652            Err(_) => Err(tonic::Status::deadline_exceeded(
653                "upstream did not answer within the deadline",
654            )),
655        }
656    }
657}
658
659/// Map the request onto the RPC's input message.
660fn prepare<S: TranscodeState>(
661    proxy_state: S,
662    headers: &HeaderMap,
663    origin: Origin,
664    path_params: &PathParams,
665    raw_query: Option<&str>,
666    body: Bytes,
667    entry: &RouteEntry,
668) -> Result<Call<S::Upstream>, Unmappable> {
669    let Origin {
670        connection,
671        address,
672    } = origin;
673    // A request the proxy resolved carries the client-address headers its
674    // forwarding policy wrote; one routed here without that resolution
675    // carries only what its client asserted, which is never forwarded.
676    let request_metadata = match address {
677        Some(_) => {
678            metadata::rewritten_headers_to_grpc_metadata(headers, proxy_state.forwarded_headers())
679        }
680        None => {
681            metadata::try_http_headers_to_grpc_metadata(headers, proxy_state.forwarded_headers())
682        }
683    }
684    .map_err(|e| Unmappable(e.to_string()))?;
685    let message =
686        decode_request(entry, headers, path_params, raw_query, body).map_err(Unmappable)?;
687    let mut request = tonic::Request::new(message);
688    *request.metadata_mut() = request_metadata;
689    // An upstream in process reads the HTTP client's address and TLS
690    // certificates with `Request::remote_addr` / `peer_certs`, and the
691    // resolved client address as a `ClientAddress`; a remote one never sees
692    // request extensions.
693    if let Some(connection) = connection {
694        connection.into_tonic_extensions(request.extensions_mut());
695    }
696    if let Some(address) = address {
697        request.extensions_mut().insert(address);
698    }
699    // Only the client's own deadline travels upstream: a default one would
700    // cut a long server stream short on an upstream that applies
701    // `grpc-timeout` to the whole call.
702    let deadline = metadata::apply_request_deadline(&mut request, headers)
703        .map_or(UPSTREAM_DEADLINE, |client| client.min(UPSTREAM_DEADLINE));
704
705    Ok(Call {
706        upstream: proxy_state.into_upstream(),
707        request,
708        deadline,
709    })
710}
711
712/// A successful unary answer with its initial metadata and trailers kept
713/// apart.
714struct UnaryAnswer {
715    initial: MetadataMap,
716    message: DynamicMessage,
717    trailers: Option<MetadataMap>,
718}
719
720/// Call a unary RPC. tonic's `Grpc::unary` merges the trailers over the
721/// initial metadata, so a key sent in both keeps only its trailer value; the
722/// HTTP response carries both, so the call is made as a one-message stream
723/// instead, reading exactly what `Grpc::unary` reads.
724async fn call_unary<U: Upstream>(
725    call: Call<U>,
726    entry: &RouteEntry,
727) -> Result<UnaryAnswer, tonic::Status> {
728    let (initial, mut stream, _) = call.open(entry).await?.into_parts();
729    let message = match stream.message().await {
730        Ok(Some(message)) => message,
731        Ok(None) => {
732            let status = tonic::Status::internal("Missing response message.");
733            return Err(with_initial(status, initial));
734        }
735        Err(status) => return Err(with_initial(status, initial)),
736    };
737    let trailers = match stream.trailers().await {
738        Ok(trailers) => trailers,
739        Err(status) => return Err(with_initial(status, initial)),
740    };
741    Ok(UnaryAnswer {
742        initial,
743        message,
744        trailers,
745    })
746}
747
748/// A failure that came after the upstream's response headers, with their
749/// metadata put ahead of the failure's own: both belong to the error
750/// response, as tonic's `Grpc::unary` keeps them.
751fn with_initial(mut status: tonic::Status, initial: MetadataMap) -> tonic::Status {
752    let own = std::mem::take(status.metadata_mut()).into_headers();
753    let mut merged = initial.into_headers();
754    let mut last: Option<HeaderName> = None;
755    // A header map yields a name only with the first value of each key.
756    for (name, value) in own {
757        if let Some(name) = name {
758            merged.append(&name, value);
759            last = Some(name);
760        } else if let Some(name) = &last {
761            merged.append(name, value);
762        }
763    }
764    *status.metadata_mut() = MetadataMap::from_headers(merged);
765    status
766}
767
768/// Serve a unary RPC.
769async fn unary_call<U: Upstream>(call: Call<U>, entry: &RouteEntry) -> Response {
770    match call_unary(call, entry).await {
771        Ok(answer) => unary_success(entry, answer),
772        Err(status) => upstream_error(status, entry),
773    }
774}
775
776/// The HTTP response to a successful unary call: the status `x-http-code`
777/// sets (200 otherwise), the forwarded response metadata, and the body.
778fn unary_success(entry: &RouteEntry, answer: UnaryAnswer) -> Response {
779    let mut upstream = UpstreamHeaders::default();
780    upstream.absorb(answer.initial, &entry.denied_headers);
781    if let Some(trailers) = answer.trailers {
782        upstream.absorb(trailers, &entry.denied_headers);
783    }
784    let status = match upstream.status() {
785        Ok(status) => status.unwrap_or(StatusCode::OK),
786        Err(response::InvalidHttpCode) => {
787            tracing::error!(
788                rpc = %entry.grpc_path,
789                "upstream set x-http-code to something other than one integer in 200-599"
790            );
791            return error::malformed_response(entry.error_details.as_deref());
792        }
793    };
794    let (content_type, body) = match &entry.response {
795        ResponseShape::HttpBody(path) => {
796            let raw = httpbody::take(answer.message, path);
797            match content_type_header(&raw.content_type) {
798                Ok(content_type) => (content_type, Body::from(raw.data)),
799                Err(()) => return invalid_content_type(entry),
800            }
801        }
802        ResponseShape::Json(response_body) => {
803            match json_body(&answer.message, response_body.as_deref()) {
804                Ok(json) => (
805                    Some(HeaderValue::from_static("application/json")),
806                    Body::from(json),
807                ),
808                Err(e) => {
809                    tracing::error!("Failed to serialize gRPC response: {e}");
810                    return error::status_to_response_with_details(
811                        &tonic::Status::internal("failed to serialize response"),
812                        entry.error_details.as_deref(),
813                    );
814                }
815            }
816        }
817    };
818    response::build(status, upstream.into_headers(), content_type, body)
819}
820
821/// The `Content-Type` an HttpBody asks for: none when it left the field empty.
822fn content_type_header(content_type: &str) -> Result<Option<HeaderValue>, ()> {
823    if content_type.is_empty() {
824        return Ok(None);
825    }
826    HeaderValue::from_str(content_type)
827        .map(Some)
828        .map_err(|_| ())
829}
830
831/// The answer to an HttpBody whose content type cannot be a header value.
832fn invalid_content_type(entry: &RouteEntry) -> Response {
833    tracing::error!(
834        rpc = %entry.grpc_path,
835        "upstream HttpBody content_type is not a valid header value"
836    );
837    error::malformed_response(entry.error_details.as_deref())
838}
839
840/// The HTTP response to a failed call, carrying the failure's metadata as
841/// headers (a trailers-only response, or the response headers and trailers of
842/// a call that failed after them, see [`with_initial`]) unless its details
843/// were malformed and the answer is the generic `INTERNAL`, which then carries
844/// nothing of the upstream's answer.
845fn upstream_error(mut status: tonic::Status, entry: &RouteEntry) -> Response {
846    let (response, faithful) = error::render_response(&status, entry.error_details.as_deref());
847    let metadata = std::mem::take(status.metadata_mut());
848    if !faithful || metadata.is_empty() {
849        return response;
850    }
851    let mut upstream = UpstreamHeaders::default();
852    upstream.absorb(metadata, &entry.denied_headers);
853    response::with_upstream_headers(response, upstream.into_headers())
854}
855
856/// Serve a server-streaming RPC.
857///
858/// A JSON stream is Server-Sent Events when the client sends
859/// `Accept: text/event-stream`, otherwise newline-delimited JSON (NDJSON); a
860/// gRPC error mid-stream is delivered as an explicit terminal frame before the
861/// stream is closed cleanly, rather than truncating the HTTP body. An HttpBody
862/// stream is the concatenated `data` of its messages. The upstream's initial
863/// metadata becomes response headers; trailers arrive after the headers are
864/// sent and are not forwarded.
865async fn streaming_call<U: Upstream>(
866    call: Call<U>,
867    entry: Arc<RouteEntry>,
868    use_sse: bool,
869    keep_alive_secs: u64,
870) -> Response {
871    let response = match call.open(&entry).await {
872        Ok(response) => response,
873        // A trailers-only rejection, or no answer within the deadline.
874        Err(status) => return upstream_error(status, &entry),
875    };
876    let (initial, stream, _) = response.into_parts();
877    if matches!(entry.response, ResponseShape::HttpBody(_)) {
878        return http_body_stream(stream, entry, initial).await;
879    }
880    let mut upstream = UpstreamHeaders::default();
881    upstream.absorb(initial, &entry.denied_headers);
882    let headers = upstream.into_headers();
883    // Once the upstream accepted the call, the response starts at once rather
884    // than waiting for the first item, so headers and SSE keep-alives are not
885    // held back; an error that comes before the first message is a terminal
886    // frame. The terminal frame renders like the unary error body. The
887    // closure takes over this request's route handle, so the stream keeps it
888    // alive without another refcount.
889    let envelope = entry.ndjson_envelope;
890    let render_error =
891        move |status: &tonic::Status| error::error_body(status, entry.error_details.as_deref());
892    let response = if use_sse {
893        sse_response(stream, render_error, keep_alive_secs)
894    } else {
895        ndjson_response(stream, render_error, envelope)
896    };
897    response::with_upstream_headers(response, headers)
898}
899
900/// A server-streaming HttpBody response: `Content-Type` from the first
901/// message, so the headers wait for it, then every message's `data` as it
902/// arrives. An error before the first message is an ordinary error response,
903/// with the initial metadata the upstream sent before it; after the first
904/// message, the raw body has no in-band error frame, so the body is aborted
905/// and the client sees a truncated transfer instead of a clean end.
906async fn http_body_stream(
907    mut stream: tonic::Streaming<DynamicMessage>,
908    entry: Arc<RouteEntry>,
909    initial: MetadataMap,
910) -> Response {
911    let first = match stream.message().await {
912        Ok(Some(message)) => entry.raw_body(message).unwrap_or_default(),
913        Ok(None) => httpbody::RawBody::default(),
914        Err(status) => return upstream_error(with_initial(status, initial), &entry),
915    };
916    let mut upstream = UpstreamHeaders::default();
917    upstream.absorb(initial, &entry.denied_headers);
918    let headers = upstream.into_headers();
919    let content_type = match content_type_header(&first.content_type) {
920        Ok(content_type) => content_type,
921        Err(()) => return invalid_content_type(&entry),
922    };
923    let rest = stream.map(move |item| match item {
924        Ok(message) => Ok(entry.raw_body(message).unwrap_or_default().data),
925        Err(status) => {
926            tracing::error!(
927                rpc = %entry.grpc_path,
928                code = ?status.code(),
929                "server-streaming HttpBody failed after the response started; aborting the body"
930            );
931            Err(std::io::Error::other(status))
932        }
933    });
934    let chunks = futures::stream::once(futures::future::ready(Ok(first.data)))
935        .chain(rest)
936        .try_filter(|chunk| futures::future::ready(!chunk.is_empty()));
937    response::build(
938        StatusCode::OK,
939        headers,
940        content_type,
941        Body::from_stream(chunks),
942    )
943}
944
945/// One frame of a streaming response: a serialized message, or the error body
946/// (see [`error::error_body`]) that ends the stream.
947///
948/// `Error` is terminal: [`json_frames`] stops the stream right after yielding
949/// it, so an error frame is always the last thing a client sees regardless of
950/// whether it came from a gRPC status or a serialization failure. It stays a
951/// JSON value so each format can add its own framing before serializing it.
952enum StreamFrame {
953    Data(String),
954    Error(serde_json::Value),
955}
956
957/// Type URL marking the terminal error line of an unenveloped NDJSON stream. A
958/// data line is the ProtoJSON of a response message, which carries a top-level
959/// `@type` only when the RPC streams `google.protobuf.Any`; `Struct`, `Value`
960/// and `ListValue` can carry any key at all. For those RPCs no in-band marker
961/// is collision-free, which is what the opt-in envelope
962/// ([`TranscodeOptions::with_ndjson_envelope`]) is for; the unenveloped shape
963/// stays the default so existing NDJSON readers keep working.
964const STATUS_TYPE_URL: &str = "type.googleapis.com/google.rpc.Status";
965
966/// Turn a gRPC message stream into a stream of serialized JSON frames, stopping
967/// after the first error so error frames are unambiguously terminal.
968///
969/// Both a gRPC `Status` (rendered by `render_error`) and a per-message
970/// serialization failure become a terminal [`StreamFrame::Error`]; downstream
971/// messages the upstream might still emit are dropped rather than streamed past
972/// the error. The terminal frame drops the upstream along with the stream
973/// state, so the body ends at once instead of polling an upstream that may
974/// stay open.
975fn json_frames<St, R>(
976    stream: St,
977    render_error: R,
978) -> impl futures::Stream<Item = StreamFrame> + Send + 'static
979where
980    St: futures::Stream<Item = Result<DynamicMessage, tonic::Status>> + Send + Unpin + 'static,
981    R: Fn(&tonic::Status) -> serde_json::Value + Send + 'static,
982{
983    let state = Some((stream, render_error, response_serialize_options()));
984    futures::stream::unfold(state, |state| async move {
985        let (mut stream, render_error, opts) = state?;
986        let error = match stream.next().await? {
987            Ok(msg) => match message_to_json_string(&msg, &opts) {
988                Ok(s) => return Some((StreamFrame::Data(s), Some((stream, render_error, opts)))),
989                Err(e) => tonic::Status::internal(format!("serialization error: {e}")),
990            },
991            Err(status) => status,
992        };
993        Some((StreamFrame::Error(render_error(&error)), None))
994    })
995}
996
997/// Build an NDJSON (`application/x-ndjson`) streaming response.
998///
999/// With `envelope`, every line is wrapped: `{"result": <message>}` for data and
1000/// `{"error": <error body>}` for the terminal error. Without it, a data line is
1001/// the bare message and the error line is the error body plus an
1002/// `@type: google.rpc.Status` marker (see [`STATUS_TYPE_URL`]).
1003fn ndjson_response<St, R>(stream: St, render_error: R, envelope: bool) -> Response
1004where
1005    St: futures::Stream<Item = Result<DynamicMessage, tonic::Status>> + Send + Unpin + 'static,
1006    R: Fn(&tonic::Status) -> serde_json::Value + Send + 'static,
1007{
1008    let byte_stream = json_frames(stream, render_error).map(move |frame| {
1009        let mut line = match frame {
1010            // The message is already serialized; wrap the text instead of
1011            // parsing it back into a value.
1012            StreamFrame::Data(s) if envelope => {
1013                let mut wrapped = String::with_capacity(s.len() + 12);
1014                wrapped.push_str("{\"result\":");
1015                wrapped.push_str(&s);
1016                wrapped.push('}');
1017                wrapped
1018            }
1019            StreamFrame::Data(s) => s,
1020            StreamFrame::Error(body) if envelope => {
1021                serde_json::json!({ "error": body }).to_string()
1022            }
1023            StreamFrame::Error(mut body) => {
1024                if let Some(fields) = body.as_object_mut() {
1025                    fields.insert("@type".into(), STATUS_TYPE_URL.into());
1026                }
1027                body.to_string()
1028            }
1029        };
1030        line.push('\n');
1031        Ok::<Bytes, std::io::Error>(Bytes::from(line))
1032    });
1033
1034    let body = Body::from_stream(byte_stream);
1035    // Body framing (chunked on HTTP/1.1, DATA frames on HTTP/2) is chosen by
1036    // hyper from the protocol version; setting transfer-encoding by hand would
1037    // be redundant on HTTP/1.1 and illegal on HTTP/2.
1038    Response::builder()
1039        .status(StatusCode::OK)
1040        .header(CONTENT_TYPE, "application/x-ndjson")
1041        .body(body)
1042        .unwrap_or_else(|_| StatusCode::INTERNAL_SERVER_ERROR.into_response())
1043}
1044
1045/// Build a Server-Sent Events (`text/event-stream`) streaming response.
1046fn sse_response<St, R>(stream: St, render_error: R, keep_alive_secs: u64) -> Response
1047where
1048    St: futures::Stream<Item = Result<DynamicMessage, tonic::Status>> + Send + Unpin + 'static,
1049    R: Fn(&tonic::Status) -> serde_json::Value + Send + 'static,
1050{
1051    // Terminal errors use the `stream-error` event type, not the reserved
1052    // `error` type that the browser EventSource dispatches for transport
1053    // failures — clients listen for it via addEventListener("stream-error").
1054    let event_stream = json_frames(stream, render_error).map(|frame| {
1055        let event = match frame {
1056            StreamFrame::Data(s) => Event::default().data(s),
1057            StreamFrame::Error(body) => Event::default()
1058                .event("stream-error")
1059                .data(body.to_string()),
1060        };
1061        Ok::<Event, std::convert::Infallible>(event)
1062    });
1063
1064    Sse::new(event_stream)
1065        .keep_alive(KeepAlive::new().interval(std::time::Duration::from_secs(keep_alive_secs)))
1066        .into_response()
1067}
1068
1069/// Request body mapping of the raw-body routes: nothing parsed.
1070static NO_PARSED_BODY: request::BodyMapping = request::BodyMapping::None;
1071
1072/// Map the HTTP request onto the RPC's input message: path parameters, query
1073/// parameters and the route's `body` rule, or the raw body for an HttpBody.
1074/// Unary and server-streaming routes share it, so both bind a request the same
1075/// way. The error is the message of the 400 the caller answers with, before
1076/// the upstream is called.
1077fn decode_request(
1078    entry: &RouteEntry,
1079    headers: &HeaderMap,
1080    path_params: &PathParams,
1081    raw_query: Option<&str>,
1082    body_bytes: Bytes,
1083) -> Result<DynamicMessage, String> {
1084    // A raw field keeps its `body` mapping with no body read, so query binding
1085    // leaves that field to the raw body. An HttpBody input with `body: "*"`
1086    // takes every field from the raw body, so its query binds nothing
1087    // (google/api/http.proto: with `*` there are no HTTP parameters).
1088    let (mapping, body, raw_query) = match &entry.request_body {
1089        RequestBody::Parsed(mapping @ request::BodyMapping::None) => {
1090            (mapping, request::Body::Absent, raw_query)
1091        }
1092        RequestBody::Parsed(mapping) => (
1093            mapping,
1094            request::Body::new(body::content_type(headers), &body_bytes),
1095            raw_query,
1096        ),
1097        RequestBody::RawRoot => (&NO_PARSED_BODY, request::Body::Absent, None),
1098        RequestBody::RawField { mapping, .. } => (mapping, request::Body::Absent, raw_query),
1099    };
1100
1101    let mut message = request::build_request_message(
1102        &entry.method.input(),
1103        mapping,
1104        body,
1105        path_params,
1106        raw_query,
1107    )?;
1108    match &entry.request_body {
1109        RequestBody::Parsed(_) => {}
1110        RequestBody::RawRoot => {
1111            httpbody::fill(&mut message, request_content_type(headers)?, body_bytes);
1112        }
1113        RequestBody::RawField {
1114            field, http_body, ..
1115        } => {
1116            let mut inner = DynamicMessage::new(http_body.clone());
1117            httpbody::fill(&mut inner, request_content_type(headers)?, body_bytes);
1118            message.set_field(field, prost_reflect::Value::Message(inner));
1119        }
1120    }
1121    Ok(message)
1122}
1123
1124/// The request's full `Content-Type` value (parameters included) for an
1125/// HttpBody, empty when absent. `HttpBody.content_type` is a proto string, so
1126/// a value that is not visible ASCII is rejected rather than altered.
1127fn request_content_type(headers: &HeaderMap) -> Result<String, String> {
1128    match headers.get(CONTENT_TYPE) {
1129        None => Ok(String::new()),
1130        Some(value) => value
1131            .to_str()
1132            .map(str::to_owned)
1133            .map_err(|_| "request Content-Type is not a visible ASCII string".to_string()),
1134    }
1135}
1136
1137/// Extract the route entries of every HTTP binding of every selected unary
1138/// and server-streaming RPC. Client-streaming RPCs have no HTTP mapping.
1139fn extract_routes(pool: &DescriptorPool, selection: &RpcSelection) -> Vec<RouteEntry> {
1140    let http_ext = match pool.get_extension_by_name("google.api.http") {
1141        Some(ext) => ext,
1142        None => {
1143            tracing::warn!("google.api.http extension not found in descriptor pool");
1144            return Vec::new();
1145        }
1146    };
1147
1148    let mut entries = Vec::new();
1149
1150    for service in pool.services() {
1151        for method in service.methods() {
1152            if method.is_client_streaming() || !selection.selects(&method) {
1153                continue;
1154            }
1155
1156            let grpc_path = format!("/{}/{}", service.full_name(), method.name());
1157            let grpc_path: axum::http::uri::PathAndQuery = match grpc_path.parse() {
1158                Ok(p) => p,
1159                Err(e) => {
1160                    tracing::error!("skipping route with invalid gRPC path '{grpc_path}': {e}");
1161                    continue;
1162                }
1163            };
1164
1165            let input = method.input();
1166            let output = method.output();
1167            let streaming = method.is_server_streaming();
1168            let response_has_required = codec::has_required_fields(&output);
1169            for binding in rule::http_bindings(&method, &http_ext) {
1170                if streaming {
1171                    tracing::info!(
1172                        "Registering streaming route: {} {} → {}",
1173                        binding.method.as_str(),
1174                        binding.path,
1175                        grpc_path
1176                    );
1177                }
1178                entries.push(RouteEntry {
1179                    http_path: binding.path,
1180                    http_method: binding.method,
1181                    grpc_path: grpc_path.clone(),
1182                    method: method.clone(),
1183                    streaming,
1184                    request_body: request_body(&input, binding.body),
1185                    response: response_shape(&output, binding.response_body),
1186                    // Decided per mounted path in `routes_with_options`.
1187                    error_details: None,
1188                    ndjson_envelope: false,
1189                    response_has_required,
1190                    denied_headers: Arc::default(),
1191                });
1192            }
1193        }
1194    }
1195
1196    entries
1197}
1198
1199/// How a binding's `body` rule reaches `input`: raw into an HttpBody (the
1200/// input itself with `body: "*"`, or the HttpBody field `body` names), parsed
1201/// otherwise. `body` names a field of the input itself, never a dotted path:
1202/// `google/api/http.proto` requires "the referred field must not be a repeated
1203/// field and must be present at the top-level of request message type".
1204fn request_body(input: &MessageDescriptor, mapping: request::BodyMapping) -> RequestBody {
1205    let raw_field = match &mapping {
1206        request::BodyMapping::Root if httpbody::is_http_body(input) => return RequestBody::RawRoot,
1207        request::BodyMapping::Field(name) => httpbody::http_body_field(input, name),
1208        _ => None,
1209    };
1210    match raw_field {
1211        Some((field, http_body)) => RequestBody::RawField {
1212            mapping,
1213            field,
1214            http_body,
1215        },
1216        None => RequestBody::Parsed(mapping),
1217    }
1218}
1219
1220/// What a binding answers with: the raw body of an HttpBody (the output itself,
1221/// or the HttpBody `response_body` names), JSON otherwise.
1222fn response_shape(output: &MessageDescriptor, response_body: Option<String>) -> ResponseShape {
1223    match response_body {
1224        None if httpbody::is_http_body(output) => ResponseShape::HttpBody(Vec::new()),
1225        None => ResponseShape::Json(None),
1226        Some(path) => match httpbody::http_body_path(output, &path) {
1227            Some(fields) => ResponseShape::HttpBody(fields),
1228            None => ResponseShape::Json(Some(path)),
1229        },
1230    }
1231}
1232
1233/// Convert a `google.api.http` path template to axum 0.8 path syntax.
1234///
1235/// The proto `{param}` form IS axum 0.8's native capture syntax, so plain
1236/// single-segment params pass through verbatim. Only field-path templates and
1237/// bare wildcards need rewriting (axum 0.7 used `:param`; 0.8 uses `{param}`
1238/// and rejects any segment starting with `:`):
1239/// - `{name=*}`  (single segment)      -> `{name}`
1240/// - `{name=**}` (multi-segment) -> `{*name}` (axum catch-all)
1241/// - bare `*` segment            -> `{wildcardN}`
1242/// - bare `**` segment           -> `{*wildcardN}` (axum catch-all)
1243pub fn proto_path_to_axum(path: &str) -> String {
1244    let mut out = String::with_capacity(path.len());
1245
1246    let segments = split_top_level(path);
1247    let last = segments.len().saturating_sub(1);
1248    for (idx, segment) in segments.iter().enumerate() {
1249        if idx > 0 {
1250            out.push('/');
1251        }
1252        out.push_str(&convert_segment(segment, idx, idx == last));
1253    }
1254
1255    out
1256}
1257
1258/// Split a path on `/` boundaries that are NOT inside a `{...}` brace span.
1259///
1260/// google.api.http field templates can embed slashes inside a single capture
1261/// (e.g. the AIP-127 resource name `{name=shelves/*/books/*}`), so a naive
1262/// `str::split('/')` would fracture the brace span into invalid fragments.
1263/// Tracking brace depth keeps each capture intact.
1264fn split_top_level(path: &str) -> Vec<&str> {
1265    let mut segments = Vec::new();
1266    let mut depth = 0usize;
1267    let mut start = 0usize;
1268
1269    for (i, ch) in path.char_indices() {
1270        match ch {
1271            '{' => depth += 1,
1272            // Decrement only on a matched brace; a stray `}` (malformed input)
1273            // is treated as a literal rather than driving depth negative.
1274            '}' if depth > 0 => depth -= 1,
1275            '/' if depth == 0 => {
1276                segments.push(&path[start..i]);
1277                start = i + 1;
1278            }
1279            _ => {}
1280        }
1281    }
1282    segments.push(&path[start..]);
1283    segments
1284}
1285
1286/// Convert a single top-level path segment from proto template to axum 0.8 form.
1287///
1288/// `is_last` indicates the terminal segment: axum permits a catch-all capture
1289/// (`{*name}`) only there, so catch-alls in any other position must degrade.
1290fn convert_segment(segment: &str, idx: usize, is_last: bool) -> String {
1291    if let Some(inner) = segment.strip_prefix('{').and_then(|s| s.strip_suffix('}')) {
1292        // Brace capture, possibly with a `name=template` field path.
1293        if let Some((name, template)) = inner.split_once('=') {
1294            return match template {
1295                // Single-segment field path collapses to a plain capture.
1296                "*" => format!("{{{name}}}"),
1297                // Multi-segment catch-all maps to axum's `{*name}` (terminal only).
1298                "**" => catch_all(name, is_last),
1299                // Templates with interspersed literals (`{name=shelves/*/books/*}`)
1300                // have no faithful axum form: axum cannot bind literal segments
1301                // into one capture. Collapse to a catch-all so routing stays
1302                // deterministic and the field still binds to the matched tail,
1303                // and warn so the limitation surfaces instead of mis-routing.
1304                _ => {
1305                    tracing::warn!(
1306                        template = %inner,
1307                        "google.api.http multi-segment field template is not fully \
1308                         supported; routing it as a catch-all capture"
1309                    );
1310                    catch_all(name, is_last)
1311                }
1312            };
1313        }
1314        // Plain `{name}` is already valid axum 0.8 syntax.
1315        return format!("{{{inner}}}");
1316    }
1317
1318    // Bare wildcards: name them by position so multiple wildcards never collide.
1319    match segment {
1320        "**" => catch_all(&format!("wildcard{idx}"), is_last),
1321        "*" => format!("{{wildcard{idx}}}"),
1322        literal => literal.to_string(),
1323    }
1324}
1325
1326/// Emit an axum catch-all `{*name}` when `is_last`, else degrade to a
1327/// single-segment `{name}` capture.
1328///
1329/// axum accepts a catch-all only in the final path segment; a mid-path
1330/// `{*name}` is rejected at `Router::route()`. A non-terminal catch-all comes
1331/// from a malformed or unsupported google.api.http template, so we degrade
1332/// (capturing one segment) and warn rather than panic the whole router.
1333fn catch_all(name: &str, is_last: bool) -> String {
1334    if is_last {
1335        format!("{{*{name}}}")
1336    } else {
1337        tracing::warn!(
1338            capture = %name,
1339            "catch-all in a non-terminal path segment is unrepresentable in axum; \
1340             degrading to a single-segment capture"
1341        );
1342        format!("{{{name}}}")
1343    }
1344}
1345
1346#[cfg(test)]
1347mod tests;