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