1pub 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
52pub const UPSTREAM_DEADLINE: Duration = Duration::from_secs(5);
55
56pub trait TranscodeState: Clone + Send + Sync + 'static {
62 type Upstream: Upstream;
64 fn into_upstream(self) -> Self::Upstream;
66 fn forwarded_headers(&self) -> &[String];
68 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
85type PathParams = HashMap<String, String>;
87
88#[derive(Debug, Clone)]
90enum RequestBody {
91 Parsed(request::BodyMapping),
93 RawRoot,
95 RawField {
98 mapping: request::BodyMapping,
100 field: FieldDescriptor,
101 http_body: MessageDescriptor,
103 },
104}
105
106#[derive(Debug, Clone)]
108enum ResponseShape {
109 Json(Option<String>),
111 HttpBody(Vec<FieldDescriptor>),
114}
115
116#[derive(Debug, Clone)]
118struct RouteEntry {
119 http_path: String,
121 http_method: RouteMethod,
123 grpc_path: axum::http::uri::PathAndQuery,
126 method: MethodDescriptor,
128 streaming: bool,
130 request_body: RequestBody,
132 response: ResponseShape,
134 error_details: Option<Arc<StatusDetails>>,
137 ndjson_envelope: bool,
139 response_has_required: bool,
142 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 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#[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 pub fn with_selection(mut self, selection: RpcSelection) -> Self {
188 self.selection = selection;
189 self
190 }
191
192 pub fn with_error_details(mut self, policy: ErrorDetailsPolicy) -> Self {
195 self.error_details = policy;
196 self
197 }
198
199 pub fn with_ndjson_envelope(mut self, enabled: bool) -> Self {
207 self.ndjson_envelope = enabled;
208 self
209 }
210
211 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
225pub fn routes<S: TranscodeState>(pool: &DescriptorPool, aliases: &[AliasConfig]) -> Router<S> {
232 routes_with_options(pool, aliases, &TranscodeOptions::default())
233}
234
235pub 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 let mut status_details: [Option<Arc<StatusDetails>>; 2] = [None, None];
257 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
300macro_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
324struct PathRoutes {
326 path: String,
327 methods: Vec<Arc<RouteEntry>>,
328}
329
330impl PathRoutes {
331 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 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 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 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 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
399struct RouteBinding {
402 entry: RouteEntry,
403 axum_path: String,
404}
405
406fn 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
437pub 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
459fn response_serialize_options() -> SerializeOptions {
462 SerializeOptions::new()
463 .skip_default_fields(false)
464 .stringify_64_bit_integers(true)
465}
466
467fn 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
477fn 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 Ok(unsafe { String::from_utf8_unchecked(buf) })
483}
484
485fn 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 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
523fn 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
539fn 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 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
559struct Client {
561 headers: HeaderMap,
562 origin: Origin,
563}
564
565struct Origin {
568 connection: Option<ConnectionInfo>,
569 address: Option<ClientAddress>,
570}
571
572async 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
603struct Unmappable(String);
606
607impl Unmappable {
608 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
615struct Call<U> {
618 upstream: U,
619 request: tonic::Request<DynamicMessage>,
620 deadline: Duration,
621}
622
623impl<U: Upstream> Call<U> {
624 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
659fn 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 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 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 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
712struct UnaryAnswer {
715 initial: MetadataMap,
716 message: DynamicMessage,
717 trailers: Option<MetadataMap>,
718}
719
720async 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
748fn 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 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
768async 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
776fn 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
821fn 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
831fn 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
840fn 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
856async 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 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 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
900async 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
945enum StreamFrame {
953 Data(String),
954 Error(serde_json::Value),
955}
956
957const STATUS_TYPE_URL: &str = "type.googleapis.com/google.rpc.Status";
965
966fn 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
997fn 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 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 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
1045fn 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 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
1069static NO_PARSED_BODY: request::BodyMapping = request::BodyMapping::None;
1071
1072fn 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 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
1124fn 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
1137fn 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 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
1199fn 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
1220fn 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
1233pub 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
1258fn 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 '}' 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
1286fn 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 if let Some((name, template)) = inner.split_once('=') {
1294 return match template {
1295 "*" => format!("{{{name}}}"),
1297 "**" => catch_all(name, is_last),
1299 _ => {
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 return format!("{{{inner}}}");
1316 }
1317
1318 match segment {
1320 "**" => catch_all(&format!("wildcard{idx}"), is_last),
1321 "*" => format!("{{wildcard{idx}}}"),
1322 literal => literal.to_string(),
1323 }
1324}
1325
1326fn 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;