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::config::AliasConfig;
45use crate::service::ConnectionInfo;
46use crate::upstream::Upstream;
47use error::{ErrorDetailsPolicy, StatusDetails};
48use response::UpstreamHeaders;
49use rule::RouteMethod;
50
51pub const UPSTREAM_DEADLINE: Duration = Duration::from_secs(5);
54
55pub trait TranscodeState: Clone + Send + Sync + 'static {
61 type Upstream: Upstream;
63 fn into_upstream(self) -> Self::Upstream;
65 fn forwarded_headers(&self) -> &[String];
67 fn sse_keep_alive_secs(&self) -> u64;
69}
70
71impl<U: Upstream> TranscodeState for crate::ProxyState<U> {
72 type Upstream = U;
73 fn into_upstream(self) -> U {
74 self.upstream
75 }
76 fn forwarded_headers(&self) -> &[String] {
77 &self.forwarded_headers
78 }
79 fn sse_keep_alive_secs(&self) -> u64 {
80 self.sse_keep_alive_secs
81 }
82}
83
84type PathParams = HashMap<String, String>;
86
87#[derive(Debug, Clone)]
89enum RequestBody {
90 Parsed(request::BodyMapping),
92 RawRoot,
94 RawField {
97 mapping: request::BodyMapping,
99 field: FieldDescriptor,
100 http_body: MessageDescriptor,
102 },
103}
104
105#[derive(Debug, Clone)]
107enum ResponseShape {
108 Json(Option<String>),
110 HttpBody(Vec<FieldDescriptor>),
113}
114
115#[derive(Debug, Clone)]
117struct RouteEntry {
118 http_path: String,
120 http_method: RouteMethod,
122 grpc_path: axum::http::uri::PathAndQuery,
125 method: MethodDescriptor,
127 streaming: bool,
129 request_body: RequestBody,
131 response: ResponseShape,
133 error_details: Option<Arc<StatusDetails>>,
136 ndjson_envelope: bool,
138 response_has_required: bool,
141 denied_headers: Arc<[HeaderName]>,
144}
145
146impl RouteEntry {
147 fn codec(&self) -> codec::DynamicCodec {
148 codec::DynamicCodec::with_required_check(self.method.output(), self.response_has_required)
149 }
150
151 fn raw_body(&self, message: DynamicMessage) -> Option<httpbody::RawBody> {
153 match &self.response {
154 ResponseShape::HttpBody(path) => Some(httpbody::take(message, path)),
155 ResponseShape::Json(_) => None,
156 }
157 }
158}
159
160#[derive(Debug, Clone, Default)]
176pub struct TranscodeOptions {
177 pub(crate) error_details: ErrorDetailsPolicy,
178 pub(crate) ndjson_envelope: bool,
179 pub(crate) denied_response_headers: Arc<[HeaderName]>,
180 pub(crate) selection: RpcSelection,
181}
182
183impl TranscodeOptions {
184 pub fn with_selection(mut self, selection: RpcSelection) -> Self {
187 self.selection = selection;
188 self
189 }
190
191 pub fn with_error_details(mut self, policy: ErrorDetailsPolicy) -> Self {
194 self.error_details = policy;
195 self
196 }
197
198 pub fn with_ndjson_envelope(mut self, enabled: bool) -> Self {
206 self.ndjson_envelope = enabled;
207 self
208 }
209
210 pub fn with_denied_response_headers(
216 mut self,
217 names: impl IntoIterator<Item = HeaderName>,
218 ) -> Self {
219 self.denied_response_headers = names.into_iter().collect();
220 self
221 }
222}
223
224pub fn routes<S: TranscodeState>(pool: &DescriptorPool, aliases: &[AliasConfig]) -> Router<S> {
231 routes_with_options(pool, aliases, &TranscodeOptions::default())
232}
233
234pub fn routes_with_options<S: TranscodeState>(
240 pool: &DescriptorPool,
241 aliases: &[AliasConfig],
242 options: &TranscodeOptions,
243) -> Router<S> {
244 let bindings = route_bindings(pool, aliases, &options.selection);
245 if bindings.is_empty() {
246 tracing::warn!("No HTTP-annotated RPCs found in proto descriptors");
247 return Router::new();
248 }
249
250 tracing::info!("Registering {} transcoded REST→gRPC routes", bindings.len());
251
252 let mut status_details: [Option<Arc<StatusDetails>>; 2] = [None, None];
256 let mut paths: Vec<PathRoutes> = Vec::new();
259 let mut path_index: HashMap<String, usize> = HashMap::new();
260 for mut binding in bindings {
261 let policy = &options.error_details;
262 if policy.enabled_for(&binding.axum_path) {
263 let opaque = policy.opaque_for(&binding.axum_path);
264 let slot = usize::from(opaque);
265 if status_details[slot].is_none() {
266 let base = status_details
267 .iter()
268 .flatten()
269 .next()
270 .map(|existing| existing.as_ref().clone())
271 .unwrap_or_else(|| StatusDetails::new(pool));
272 status_details[slot] = Some(Arc::new(base.with_opaque_details(opaque)));
273 }
274 binding.entry.error_details = status_details[slot].clone();
275 }
276 binding.entry.ndjson_envelope = options.ndjson_envelope;
277 binding.entry.denied_headers = options.denied_response_headers.clone();
278 let index = match path_index.get(&binding.axum_path) {
279 Some(&index) => index,
280 None => {
281 path_index.insert(binding.axum_path.clone(), paths.len());
282 paths.push(PathRoutes {
283 path: binding.axum_path,
284 methods: Vec::new(),
285 });
286 paths.len() - 1
287 }
288 };
289 paths[index].add(Arc::new(binding.entry));
290 }
291
292 let mut router: Router<S> = Router::new();
293 for path in &paths {
294 router = router.route(&path.path, path.method_router());
295 }
296 router
297}
298
299macro_rules! endpoint {
302 ($entry:expr) => {{
303 let entry: Arc<RouteEntry> = $entry;
304 move |state: State<S>,
305 headers: HeaderMap,
306 path_params: Path<PathParams>,
307 raw_query: RawQuery,
308 connection: Option<Extension<ConnectionInfo>>,
309 body: Bytes| {
310 let client = Client {
311 headers,
312 connection: connection.map(|Extension(connection)| connection),
313 };
314 handle(state, client, path_params, raw_query, body, entry)
315 }
316 }};
317}
318
319struct PathRoutes {
321 path: String,
322 methods: Vec<Arc<RouteEntry>>,
323}
324
325impl PathRoutes {
326 fn add(&mut self, entry: Arc<RouteEntry>) {
328 let taken = self.methods.iter().any(|existing| {
329 existing.http_method == entry.http_method
330 || existing.http_method == RouteMethod::Any
331 || entry.http_method == RouteMethod::Any
332 });
333 if taken {
334 tracing::error!(
335 method = entry.http_method.as_str(),
336 path = %self.path,
337 rpc = %entry.grpc_path,
338 "HTTP method and path already bound to another RPC; skipping this binding"
339 );
340 return;
341 }
342 self.methods.push(entry);
343 }
344
345 fn method_router<S: TranscodeState>(&self) -> MethodRouter<S> {
350 let mut router = MethodRouter::new();
351 let mut extension: Vec<(Method, Arc<RouteEntry>)> = Vec::new();
352 let mut allow: Vec<&str> = Vec::new();
353 for entry in &self.methods {
354 match &entry.http_method {
355 RouteMethod::Any => return axum::routing::any(endpoint!(entry.clone())),
357 RouteMethod::One(method) => {
358 allow.push(method.as_str());
359 match MethodFilter::try_from(method.clone()) {
360 Ok(filter) => router = router.on(filter, endpoint!(entry.clone())),
361 Err(_) => extension.push((method.clone(), entry.clone())),
362 }
363 }
364 }
365 }
366 if extension.is_empty() {
367 return router;
368 }
369 if allow.contains(&"GET") && !allow.contains(&"HEAD") {
371 allow.push("HEAD");
372 }
373 let allow = HeaderValue::from_str(&allow.join(", "))
374 .expect("method tokens are valid header value characters");
375 let extension: Arc<[(Method, Arc<RouteEntry>)]> = extension.into();
376 router.fallback(
379 move |State(state): State<S>, request: axum::extract::Request| async move {
380 match extension
381 .iter()
382 .find(|(bound, _)| bound == request.method())
383 {
384 Some((_, entry)) => {
385 axum::handler::Handler::call(endpoint!(entry.clone()), request, state).await
386 }
387 None => (StatusCode::METHOD_NOT_ALLOWED, [(ALLOW, allow)]).into_response(),
388 }
389 },
390 )
391 }
392}
393
394struct RouteBinding {
397 entry: RouteEntry,
398 axum_path: String,
399}
400
401fn route_bindings(
407 pool: &DescriptorPool,
408 aliases: &[AliasConfig],
409 selection: &RpcSelection,
410) -> Vec<RouteBinding> {
411 let mut bindings = Vec::new();
412 for entry in extract_routes(pool, selection) {
413 for alias in aliases {
414 if let Some(suffix) = entry.http_path.strip_prefix(&alias.to) {
415 if alias.from.ends_with("/{path}") {
416 let prefix = alias.from.trim_end_matches("/{path}");
417 bindings.push(RouteBinding {
418 axum_path: proto_path_to_axum(&format!("{prefix}{suffix}")),
419 entry: entry.clone(),
420 });
421 }
422 }
423 }
424 bindings.push(RouteBinding {
425 axum_path: proto_path_to_axum(&entry.http_path),
426 entry,
427 });
428 }
429 bindings
430}
431
432pub fn route_paths(
444 pool: &DescriptorPool,
445 aliases: &[AliasConfig],
446 selection: &RpcSelection,
447) -> Vec<(String, String)> {
448 route_bindings(pool, aliases, selection)
449 .into_iter()
450 .map(|b| (b.entry.http_method.as_str().to_string(), b.axum_path))
451 .collect()
452}
453
454fn response_serialize_options() -> SerializeOptions {
457 SerializeOptions::new()
458 .skip_default_fields(false)
459 .stringify_64_bit_integers(true)
460}
461
462fn message_to_json_bytes(
464 msg: &DynamicMessage,
465 opts: &SerializeOptions,
466) -> Result<Vec<u8>, serde_json::Error> {
467 let mut buf = Vec::with_capacity(128);
468 msg.serialize_with_options(&mut serde_json::Serializer::new(&mut buf), opts)?;
469 Ok(buf)
470}
471
472fn message_to_json_string(msg: &DynamicMessage, opts: &SerializeOptions) -> Result<String, String> {
474 let buf = message_to_json_bytes(msg, opts).map_err(|e| e.to_string())?;
475 Ok(unsafe { String::from_utf8_unchecked(buf) })
478}
479
480fn json_body(
483 msg: &DynamicMessage,
484 response_body: Option<&str>,
485) -> Result<Vec<u8>, serde_json::Error> {
486 let opts = response_serialize_options();
487 let Some(path) = response_body else {
488 return message_to_json_bytes(msg, &opts);
489 };
490 let mut value = Some(msg.serialize_with_options(serde_json::value::Serializer, &opts)?);
494 let mut desc = Some(prost_reflect::ReflectMessage::descriptor(msg));
495 for segment in path.split('.') {
496 let field = desc.as_ref().and_then(|d| d.get_field_by_name(segment));
497 desc = match field.as_ref().map(FieldDescriptor::kind) {
498 Some(prost_reflect::Kind::Message(inner)) => Some(inner),
499 _ => None,
500 };
501 value = match (value, field) {
502 (Some(serde_json::Value::Object(mut fields)), Some(field)) => {
503 fields.remove(field.json_name())
504 }
505 _ => None,
506 };
507 }
508 let value = value.unwrap_or_else(|| {
509 tracing::warn!(
510 response_body = %path,
511 "configured response_body path not found in response; returning null"
512 );
513 serde_json::Value::Null
514 });
515 serde_json::to_vec(&value)
516}
517
518fn wants_sse(headers: &HeaderMap) -> bool {
526 headers
527 .get_all(axum::http::header::ACCEPT)
528 .iter()
529 .filter_map(|v| v.to_str().ok())
530 .flat_map(|accept| accept.split(','))
531 .any(accept_range_selects_sse)
532}
533
534fn accept_range_selects_sse(range: &str) -> bool {
537 let mut parts = range.split(';');
538 let media = parts.next().unwrap_or("").trim();
539 if !media.eq_ignore_ascii_case("text/event-stream") {
540 return false;
541 }
542 for param in parts {
545 let mut kv = param.splitn(2, '=');
546 if kv.next().unwrap_or("").trim().eq_ignore_ascii_case("q") {
547 let q: f32 = kv.next().unwrap_or("").trim().parse().unwrap_or(1.0);
548 return q > 0.0;
549 }
550 }
551 true
552}
553
554struct Client {
557 headers: HeaderMap,
558 connection: Option<ConnectionInfo>,
559}
560
561async fn handle<S: TranscodeState>(
563 State(proxy_state): State<S>,
564 client: Client,
565 Path(path_params): Path<PathParams>,
566 RawQuery(raw_query): RawQuery,
567 body: Bytes,
568 entry: Arc<RouteEntry>,
569) -> Response {
570 let keep_alive_secs = proxy_state.sse_keep_alive_secs();
571 let Client {
572 headers,
573 connection,
574 } = client;
575 let prepared = prepare(
576 proxy_state,
577 &headers,
578 connection,
579 &path_params,
580 raw_query.as_deref(),
581 body,
582 &entry,
583 );
584 let call = match prepared {
585 Ok(call) => call,
586 Err(rejection) => return rejection.into_response(&entry),
587 };
588 if entry.streaming {
589 streaming_call(call, entry, wants_sse(&headers), keep_alive_secs).await
590 } else {
591 unary_call(call, &entry).await
592 }
593}
594
595struct Unmappable(String);
598
599impl Unmappable {
600 fn into_response(self, entry: &RouteEntry) -> Response {
602 let status = tonic::Status::invalid_argument(self.0);
603 error::status_to_response_with_details(&status, entry.error_details.as_deref())
604 }
605}
606
607struct Call<U> {
610 upstream: U,
611 request: tonic::Request<DynamicMessage>,
612 deadline: Duration,
613}
614
615impl<U: Upstream> Call<U> {
616 async fn open(
622 self,
623 entry: &RouteEntry,
624 ) -> Result<tonic::Response<tonic::Streaming<DynamicMessage>>, tonic::Status> {
625 let Self {
626 upstream,
627 request,
628 deadline,
629 } = self;
630 let call = async move {
631 let mut client = Grpc::new(upstream);
632 if let Err(e) = client.ready().await {
633 let e: crate::upstream::BoxError = e.into();
634 return Err(tonic::Status::unavailable(format!(
635 "gRPC upstream not ready: {e}"
636 )));
637 }
638 client
639 .server_streaming(request, entry.grpc_path.clone(), entry.codec())
640 .await
641 };
642 match tokio::time::timeout(deadline, call).await {
643 Ok(result) => result,
644 Err(_) => Err(tonic::Status::deadline_exceeded(
645 "upstream did not answer within the deadline",
646 )),
647 }
648 }
649}
650
651fn prepare<S: TranscodeState>(
653 proxy_state: S,
654 headers: &HeaderMap,
655 connection: Option<ConnectionInfo>,
656 path_params: &PathParams,
657 raw_query: Option<&str>,
658 body: Bytes,
659 entry: &RouteEntry,
660) -> Result<Call<S::Upstream>, Unmappable> {
661 let request_metadata =
662 metadata::try_http_headers_to_grpc_metadata(headers, proxy_state.forwarded_headers())
663 .map_err(|e| Unmappable(e.to_string()))?;
664 let message =
665 decode_request(entry, headers, path_params, raw_query, body).map_err(Unmappable)?;
666 let mut request = tonic::Request::new(message);
667 *request.metadata_mut() = request_metadata;
668 if let Some(connection) = connection {
672 connection.into_tonic_extensions(request.extensions_mut());
673 }
674 let deadline = metadata::apply_request_deadline(&mut request, headers)
678 .map_or(UPSTREAM_DEADLINE, |client| client.min(UPSTREAM_DEADLINE));
679
680 Ok(Call {
681 upstream: proxy_state.into_upstream(),
682 request,
683 deadline,
684 })
685}
686
687struct UnaryAnswer {
690 initial: MetadataMap,
691 message: DynamicMessage,
692 trailers: Option<MetadataMap>,
693}
694
695async fn call_unary<U: Upstream>(
700 call: Call<U>,
701 entry: &RouteEntry,
702) -> Result<UnaryAnswer, tonic::Status> {
703 let (initial, mut stream, _) = call.open(entry).await?.into_parts();
704 let message = match stream.message().await {
705 Ok(Some(message)) => message,
706 Ok(None) => {
707 let status = tonic::Status::internal("Missing response message.");
708 return Err(with_initial(status, initial));
709 }
710 Err(status) => return Err(with_initial(status, initial)),
711 };
712 let trailers = match stream.trailers().await {
713 Ok(trailers) => trailers,
714 Err(status) => return Err(with_initial(status, initial)),
715 };
716 Ok(UnaryAnswer {
717 initial,
718 message,
719 trailers,
720 })
721}
722
723fn with_initial(mut status: tonic::Status, initial: MetadataMap) -> tonic::Status {
727 let own = std::mem::take(status.metadata_mut()).into_headers();
728 let mut merged = initial.into_headers();
729 let mut last: Option<HeaderName> = None;
730 for (name, value) in own {
732 if let Some(name) = name {
733 merged.append(&name, value);
734 last = Some(name);
735 } else if let Some(name) = &last {
736 merged.append(name, value);
737 }
738 }
739 *status.metadata_mut() = MetadataMap::from_headers(merged);
740 status
741}
742
743async fn unary_call<U: Upstream>(call: Call<U>, entry: &RouteEntry) -> Response {
745 match call_unary(call, entry).await {
746 Ok(answer) => unary_success(entry, answer),
747 Err(status) => upstream_error(status, entry),
748 }
749}
750
751fn unary_success(entry: &RouteEntry, answer: UnaryAnswer) -> Response {
754 let mut upstream = UpstreamHeaders::default();
755 upstream.absorb(answer.initial, &entry.denied_headers);
756 if let Some(trailers) = answer.trailers {
757 upstream.absorb(trailers, &entry.denied_headers);
758 }
759 let status = match upstream.status() {
760 Ok(status) => status.unwrap_or(StatusCode::OK),
761 Err(response::InvalidHttpCode) => {
762 tracing::error!(
763 rpc = %entry.grpc_path,
764 "upstream set x-http-code to something other than one integer in 200-599"
765 );
766 return error::malformed_response(entry.error_details.as_deref());
767 }
768 };
769 let (content_type, body) = match &entry.response {
770 ResponseShape::HttpBody(path) => {
771 let raw = httpbody::take(answer.message, path);
772 match content_type_header(&raw.content_type) {
773 Ok(content_type) => (content_type, Body::from(raw.data)),
774 Err(()) => return invalid_content_type(entry),
775 }
776 }
777 ResponseShape::Json(response_body) => {
778 match json_body(&answer.message, response_body.as_deref()) {
779 Ok(json) => (
780 Some(HeaderValue::from_static("application/json")),
781 Body::from(json),
782 ),
783 Err(e) => {
784 tracing::error!("Failed to serialize gRPC response: {e}");
785 return error::status_to_response_with_details(
786 &tonic::Status::internal("failed to serialize response"),
787 entry.error_details.as_deref(),
788 );
789 }
790 }
791 }
792 };
793 response::build(status, upstream.into_headers(), content_type, body)
794}
795
796fn content_type_header(content_type: &str) -> Result<Option<HeaderValue>, ()> {
798 if content_type.is_empty() {
799 return Ok(None);
800 }
801 HeaderValue::from_str(content_type)
802 .map(Some)
803 .map_err(|_| ())
804}
805
806fn invalid_content_type(entry: &RouteEntry) -> Response {
808 tracing::error!(
809 rpc = %entry.grpc_path,
810 "upstream HttpBody content_type is not a valid header value"
811 );
812 error::malformed_response(entry.error_details.as_deref())
813}
814
815fn upstream_error(mut status: tonic::Status, entry: &RouteEntry) -> Response {
821 let (response, faithful) = error::render_response(&status, entry.error_details.as_deref());
822 let metadata = std::mem::take(status.metadata_mut());
823 if !faithful || metadata.is_empty() {
824 return response;
825 }
826 let mut upstream = UpstreamHeaders::default();
827 upstream.absorb(metadata, &entry.denied_headers);
828 response::with_upstream_headers(response, upstream.into_headers())
829}
830
831async fn streaming_call<U: Upstream>(
841 call: Call<U>,
842 entry: Arc<RouteEntry>,
843 use_sse: bool,
844 keep_alive_secs: u64,
845) -> Response {
846 let response = match call.open(&entry).await {
847 Ok(response) => response,
848 Err(status) => return upstream_error(status, &entry),
850 };
851 let (initial, stream, _) = response.into_parts();
852 if matches!(entry.response, ResponseShape::HttpBody(_)) {
853 return http_body_stream(stream, entry, initial).await;
854 }
855 let mut upstream = UpstreamHeaders::default();
856 upstream.absorb(initial, &entry.denied_headers);
857 let headers = upstream.into_headers();
858 let envelope = entry.ndjson_envelope;
865 let render_error =
866 move |status: &tonic::Status| error::error_body(status, entry.error_details.as_deref());
867 let response = if use_sse {
868 sse_response(stream, render_error, keep_alive_secs)
869 } else {
870 ndjson_response(stream, render_error, envelope)
871 };
872 response::with_upstream_headers(response, headers)
873}
874
875async fn http_body_stream(
882 mut stream: tonic::Streaming<DynamicMessage>,
883 entry: Arc<RouteEntry>,
884 initial: MetadataMap,
885) -> Response {
886 let first = match stream.message().await {
887 Ok(Some(message)) => entry.raw_body(message).unwrap_or_default(),
888 Ok(None) => httpbody::RawBody::default(),
889 Err(status) => return upstream_error(with_initial(status, initial), &entry),
890 };
891 let mut upstream = UpstreamHeaders::default();
892 upstream.absorb(initial, &entry.denied_headers);
893 let headers = upstream.into_headers();
894 let content_type = match content_type_header(&first.content_type) {
895 Ok(content_type) => content_type,
896 Err(()) => return invalid_content_type(&entry),
897 };
898 let rest = stream.map(move |item| match item {
899 Ok(message) => Ok(entry.raw_body(message).unwrap_or_default().data),
900 Err(status) => {
901 tracing::error!(
902 rpc = %entry.grpc_path,
903 code = ?status.code(),
904 "server-streaming HttpBody failed after the response started; aborting the body"
905 );
906 Err(std::io::Error::other(status))
907 }
908 });
909 let chunks = futures::stream::once(futures::future::ready(Ok(first.data)))
910 .chain(rest)
911 .try_filter(|chunk| futures::future::ready(!chunk.is_empty()));
912 response::build(
913 StatusCode::OK,
914 headers,
915 content_type,
916 Body::from_stream(chunks),
917 )
918}
919
920enum StreamFrame {
928 Data(String),
929 Error(serde_json::Value),
930}
931
932const STATUS_TYPE_URL: &str = "type.googleapis.com/google.rpc.Status";
940
941fn json_frames<St, R>(
951 stream: St,
952 render_error: R,
953) -> impl futures::Stream<Item = StreamFrame> + Send + 'static
954where
955 St: futures::Stream<Item = Result<DynamicMessage, tonic::Status>> + Send + Unpin + 'static,
956 R: Fn(&tonic::Status) -> serde_json::Value + Send + 'static,
957{
958 let state = Some((stream, render_error, response_serialize_options()));
959 futures::stream::unfold(state, |state| async move {
960 let (mut stream, render_error, opts) = state?;
961 let error = match stream.next().await? {
962 Ok(msg) => match message_to_json_string(&msg, &opts) {
963 Ok(s) => return Some((StreamFrame::Data(s), Some((stream, render_error, opts)))),
964 Err(e) => tonic::Status::internal(format!("serialization error: {e}")),
965 },
966 Err(status) => status,
967 };
968 Some((StreamFrame::Error(render_error(&error)), None))
969 })
970}
971
972fn ndjson_response<St, R>(stream: St, render_error: R, envelope: bool) -> Response
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 byte_stream = json_frames(stream, render_error).map(move |frame| {
984 let mut line = match frame {
985 StreamFrame::Data(s) if envelope => {
988 let mut wrapped = String::with_capacity(s.len() + 12);
989 wrapped.push_str("{\"result\":");
990 wrapped.push_str(&s);
991 wrapped.push('}');
992 wrapped
993 }
994 StreamFrame::Data(s) => s,
995 StreamFrame::Error(body) if envelope => {
996 serde_json::json!({ "error": body }).to_string()
997 }
998 StreamFrame::Error(mut body) => {
999 if let Some(fields) = body.as_object_mut() {
1000 fields.insert("@type".into(), STATUS_TYPE_URL.into());
1001 }
1002 body.to_string()
1003 }
1004 };
1005 line.push('\n');
1006 Ok::<Bytes, std::io::Error>(Bytes::from(line))
1007 });
1008
1009 let body = Body::from_stream(byte_stream);
1010 Response::builder()
1014 .status(StatusCode::OK)
1015 .header(CONTENT_TYPE, "application/x-ndjson")
1016 .body(body)
1017 .unwrap_or_else(|_| StatusCode::INTERNAL_SERVER_ERROR.into_response())
1018}
1019
1020fn sse_response<St, R>(stream: St, render_error: R, keep_alive_secs: u64) -> Response
1022where
1023 St: futures::Stream<Item = Result<DynamicMessage, tonic::Status>> + Send + Unpin + 'static,
1024 R: Fn(&tonic::Status) -> serde_json::Value + Send + 'static,
1025{
1026 let event_stream = json_frames(stream, render_error).map(|frame| {
1030 let event = match frame {
1031 StreamFrame::Data(s) => Event::default().data(s),
1032 StreamFrame::Error(body) => Event::default()
1033 .event("stream-error")
1034 .data(body.to_string()),
1035 };
1036 Ok::<Event, std::convert::Infallible>(event)
1037 });
1038
1039 Sse::new(event_stream)
1040 .keep_alive(KeepAlive::new().interval(std::time::Duration::from_secs(keep_alive_secs)))
1041 .into_response()
1042}
1043
1044static NO_PARSED_BODY: request::BodyMapping = request::BodyMapping::None;
1046
1047fn decode_request(
1053 entry: &RouteEntry,
1054 headers: &HeaderMap,
1055 path_params: &PathParams,
1056 raw_query: Option<&str>,
1057 body_bytes: Bytes,
1058) -> Result<DynamicMessage, String> {
1059 let (mapping, body, raw_query) = match &entry.request_body {
1064 RequestBody::Parsed(mapping @ request::BodyMapping::None) => {
1065 (mapping, request::Body::Absent, raw_query)
1066 }
1067 RequestBody::Parsed(mapping) => (
1068 mapping,
1069 request::Body::new(body::content_type(headers), &body_bytes),
1070 raw_query,
1071 ),
1072 RequestBody::RawRoot => (&NO_PARSED_BODY, request::Body::Absent, None),
1073 RequestBody::RawField { mapping, .. } => (mapping, request::Body::Absent, raw_query),
1074 };
1075
1076 let mut message = request::build_request_message(
1077 &entry.method.input(),
1078 mapping,
1079 body,
1080 path_params,
1081 raw_query,
1082 )?;
1083 match &entry.request_body {
1084 RequestBody::Parsed(_) => {}
1085 RequestBody::RawRoot => {
1086 httpbody::fill(&mut message, request_content_type(headers)?, body_bytes);
1087 }
1088 RequestBody::RawField {
1089 field, http_body, ..
1090 } => {
1091 let mut inner = DynamicMessage::new(http_body.clone());
1092 httpbody::fill(&mut inner, request_content_type(headers)?, body_bytes);
1093 message.set_field(field, prost_reflect::Value::Message(inner));
1094 }
1095 }
1096 Ok(message)
1097}
1098
1099fn request_content_type(headers: &HeaderMap) -> Result<String, String> {
1103 match headers.get(CONTENT_TYPE) {
1104 None => Ok(String::new()),
1105 Some(value) => value
1106 .to_str()
1107 .map(str::to_owned)
1108 .map_err(|_| "request Content-Type is not a visible ASCII string".to_string()),
1109 }
1110}
1111
1112fn extract_routes(pool: &DescriptorPool, selection: &RpcSelection) -> Vec<RouteEntry> {
1115 let http_ext = match pool.get_extension_by_name("google.api.http") {
1116 Some(ext) => ext,
1117 None => {
1118 tracing::warn!("google.api.http extension not found in descriptor pool");
1119 return Vec::new();
1120 }
1121 };
1122
1123 let mut entries = Vec::new();
1124
1125 for service in pool.services() {
1126 for method in service.methods() {
1127 if method.is_client_streaming() || !selection.selects(&method) {
1128 continue;
1129 }
1130
1131 let grpc_path = format!("/{}/{}", service.full_name(), method.name());
1132 let grpc_path: axum::http::uri::PathAndQuery = match grpc_path.parse() {
1133 Ok(p) => p,
1134 Err(e) => {
1135 tracing::error!("skipping route with invalid gRPC path '{grpc_path}': {e}");
1136 continue;
1137 }
1138 };
1139
1140 let input = method.input();
1141 let output = method.output();
1142 let streaming = method.is_server_streaming();
1143 let response_has_required = codec::has_required_fields(&output);
1144 for binding in rule::http_bindings(&method, &http_ext) {
1145 if streaming {
1146 tracing::info!(
1147 "Registering streaming route: {} {} → {}",
1148 binding.method.as_str(),
1149 binding.path,
1150 grpc_path
1151 );
1152 }
1153 entries.push(RouteEntry {
1154 http_path: binding.path,
1155 http_method: binding.method,
1156 grpc_path: grpc_path.clone(),
1157 method: method.clone(),
1158 streaming,
1159 request_body: request_body(&input, binding.body),
1160 response: response_shape(&output, binding.response_body),
1161 error_details: None,
1163 ndjson_envelope: false,
1164 response_has_required,
1165 denied_headers: Arc::default(),
1166 });
1167 }
1168 }
1169 }
1170
1171 entries
1172}
1173
1174fn request_body(input: &MessageDescriptor, mapping: request::BodyMapping) -> RequestBody {
1180 let raw_field = match &mapping {
1181 request::BodyMapping::Root if httpbody::is_http_body(input) => return RequestBody::RawRoot,
1182 request::BodyMapping::Field(name) => httpbody::http_body_field(input, name),
1183 _ => None,
1184 };
1185 match raw_field {
1186 Some((field, http_body)) => RequestBody::RawField {
1187 mapping,
1188 field,
1189 http_body,
1190 },
1191 None => RequestBody::Parsed(mapping),
1192 }
1193}
1194
1195fn response_shape(output: &MessageDescriptor, response_body: Option<String>) -> ResponseShape {
1198 match response_body {
1199 None if httpbody::is_http_body(output) => ResponseShape::HttpBody(Vec::new()),
1200 None => ResponseShape::Json(None),
1201 Some(path) => match httpbody::http_body_path(output, &path) {
1202 Some(fields) => ResponseShape::HttpBody(fields),
1203 None => ResponseShape::Json(Some(path)),
1204 },
1205 }
1206}
1207
1208pub fn proto_path_to_axum(path: &str) -> String {
1219 let mut out = String::with_capacity(path.len());
1220
1221 let segments = split_top_level(path);
1222 let last = segments.len().saturating_sub(1);
1223 for (idx, segment) in segments.iter().enumerate() {
1224 if idx > 0 {
1225 out.push('/');
1226 }
1227 out.push_str(&convert_segment(segment, idx, idx == last));
1228 }
1229
1230 out
1231}
1232
1233fn split_top_level(path: &str) -> Vec<&str> {
1240 let mut segments = Vec::new();
1241 let mut depth = 0usize;
1242 let mut start = 0usize;
1243
1244 for (i, ch) in path.char_indices() {
1245 match ch {
1246 '{' => depth += 1,
1247 '}' if depth > 0 => depth -= 1,
1250 '/' if depth == 0 => {
1251 segments.push(&path[start..i]);
1252 start = i + 1;
1253 }
1254 _ => {}
1255 }
1256 }
1257 segments.push(&path[start..]);
1258 segments
1259}
1260
1261fn convert_segment(segment: &str, idx: usize, is_last: bool) -> String {
1266 if let Some(inner) = segment.strip_prefix('{').and_then(|s| s.strip_suffix('}')) {
1267 if let Some((name, template)) = inner.split_once('=') {
1269 return match template {
1270 "*" => format!("{{{name}}}"),
1272 "**" => catch_all(name, is_last),
1274 _ => {
1280 tracing::warn!(
1281 template = %inner,
1282 "google.api.http multi-segment field template is not fully \
1283 supported; routing it as a catch-all capture"
1284 );
1285 catch_all(name, is_last)
1286 }
1287 };
1288 }
1289 return format!("{{{inner}}}");
1291 }
1292
1293 match segment {
1295 "**" => catch_all(&format!("wildcard{idx}"), is_last),
1296 "*" => format!("{{wildcard{idx}}}"),
1297 literal => literal.to_string(),
1298 }
1299}
1300
1301fn catch_all(name: &str, is_last: bool) -> String {
1309 if is_last {
1310 format!("{{*{name}}}")
1311 } else {
1312 tracing::warn!(
1313 capture = %name,
1314 "catch-all in a non-terminal path segment is unrepresentable in axum; \
1315 degrading to a single-segment capture"
1316 );
1317 format!("{{{name}}}")
1318 }
1319}
1320
1321#[cfg(test)]
1322mod tests;