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;
20
21use axum::body::{Body, Bytes};
22use axum::extract::{Path, RawQuery, State};
23use axum::http::header::{ALLOW, CONTENT_TYPE};
24use axum::http::{HeaderMap, HeaderName, HeaderValue, Method, StatusCode};
25use axum::response::sse::{Event, KeepAlive, Sse};
26use axum::response::{IntoResponse, Response};
27use axum::routing::{MethodFilter, MethodRouter};
28use axum::Router;
29use futures::{StreamExt, TryStreamExt};
30use prost_reflect::{
31 DescriptorPool, DynamicMessage, FieldDescriptor, MessageDescriptor, MethodDescriptor,
32 SerializeOptions,
33};
34use std::collections::HashMap;
35use std::sync::Arc;
36use tonic::client::Grpc;
37use tonic::metadata::MetadataMap;
38
39use crate::config::AliasConfig;
40use error::{ErrorDetailsPolicy, StatusDetails};
41use response::UpstreamHeaders;
42use rule::RouteMethod;
43
44pub trait TranscodeState: Clone + Send + Sync + 'static {
49 fn grpc_channel(&self) -> tonic::transport::Channel;
51 fn forwarded_headers(&self) -> &[String];
53 fn sse_keep_alive_secs(&self) -> u64;
55}
56
57impl TranscodeState for crate::ProxyState {
58 fn grpc_channel(&self) -> tonic::transport::Channel {
59 self.grpc_channel.clone()
60 }
61 fn forwarded_headers(&self) -> &[String] {
62 &self.forwarded_headers
63 }
64 fn sse_keep_alive_secs(&self) -> u64 {
65 self.sse_keep_alive_secs
66 }
67}
68
69type PathParams = HashMap<String, String>;
71
72#[derive(Debug, Clone)]
74enum RequestBody {
75 Parsed(request::BodyMapping),
77 RawRoot,
79 RawField {
82 mapping: request::BodyMapping,
84 field: FieldDescriptor,
85 http_body: MessageDescriptor,
87 },
88}
89
90#[derive(Debug, Clone)]
92enum ResponseShape {
93 Json(Option<String>),
95 HttpBody(Vec<FieldDescriptor>),
98}
99
100#[derive(Debug, Clone)]
102struct RouteEntry {
103 http_path: String,
105 http_method: RouteMethod,
107 grpc_path: axum::http::uri::PathAndQuery,
110 method: MethodDescriptor,
112 streaming: bool,
114 request_body: RequestBody,
116 response: ResponseShape,
118 error_details: Option<Arc<StatusDetails>>,
121 ndjson_envelope: bool,
123 response_has_required: bool,
126 denied_headers: Arc<[HeaderName]>,
129}
130
131impl RouteEntry {
132 fn codec(&self) -> codec::DynamicCodec {
133 codec::DynamicCodec::with_required_check(self.method.output(), self.response_has_required)
134 }
135
136 fn raw_body(&self, message: DynamicMessage) -> Option<httpbody::RawBody> {
138 match &self.response {
139 ResponseShape::HttpBody(path) => Some(httpbody::take(message, path)),
140 ResponseShape::Json(_) => None,
141 }
142 }
143}
144
145#[derive(Debug, Clone, Default)]
161pub struct TranscodeOptions {
162 pub(crate) error_details: ErrorDetailsPolicy,
163 pub(crate) ndjson_envelope: bool,
164 pub(crate) denied_response_headers: Arc<[HeaderName]>,
165}
166
167impl TranscodeOptions {
168 pub fn with_error_details(mut self, policy: ErrorDetailsPolicy) -> Self {
171 self.error_details = policy;
172 self
173 }
174
175 pub fn with_ndjson_envelope(mut self, enabled: bool) -> Self {
183 self.ndjson_envelope = enabled;
184 self
185 }
186
187 pub fn with_denied_response_headers(
193 mut self,
194 names: impl IntoIterator<Item = HeaderName>,
195 ) -> Self {
196 self.denied_response_headers = names.into_iter().collect();
197 self
198 }
199}
200
201pub fn routes<S: TranscodeState>(pool: &DescriptorPool, aliases: &[AliasConfig]) -> Router<S> {
208 routes_with_options(pool, aliases, &TranscodeOptions::default())
209}
210
211pub fn routes_with_options<S: TranscodeState>(
217 pool: &DescriptorPool,
218 aliases: &[AliasConfig],
219 options: &TranscodeOptions,
220) -> Router<S> {
221 let bindings = route_bindings(pool, aliases);
222 if bindings.is_empty() {
223 tracing::warn!("No HTTP-annotated RPCs found in proto descriptors");
224 return Router::new();
225 }
226
227 tracing::info!("Registering {} transcoded REST→gRPC routes", bindings.len());
228
229 let mut status_details: [Option<Arc<StatusDetails>>; 2] = [None, None];
233 let mut paths: Vec<PathRoutes> = Vec::new();
236 let mut path_index: HashMap<String, usize> = HashMap::new();
237 for mut binding in bindings {
238 let policy = &options.error_details;
239 if policy.enabled_for(&binding.axum_path) {
240 let opaque = policy.opaque_for(&binding.axum_path);
241 let slot = usize::from(opaque);
242 if status_details[slot].is_none() {
243 let base = status_details
244 .iter()
245 .flatten()
246 .next()
247 .map(|existing| existing.as_ref().clone())
248 .unwrap_or_else(|| StatusDetails::new(pool));
249 status_details[slot] = Some(Arc::new(base.with_opaque_details(opaque)));
250 }
251 binding.entry.error_details = status_details[slot].clone();
252 }
253 binding.entry.ndjson_envelope = options.ndjson_envelope;
254 binding.entry.denied_headers = options.denied_response_headers.clone();
255 let index = match path_index.get(&binding.axum_path) {
256 Some(&index) => index,
257 None => {
258 path_index.insert(binding.axum_path.clone(), paths.len());
259 paths.push(PathRoutes {
260 path: binding.axum_path,
261 methods: Vec::new(),
262 });
263 paths.len() - 1
264 }
265 };
266 paths[index].add(Arc::new(binding.entry));
267 }
268
269 let mut router: Router<S> = Router::new();
270 for path in &paths {
271 router = router.route(&path.path, path.method_router());
272 }
273 router
274}
275
276macro_rules! endpoint {
279 ($entry:expr) => {{
280 let entry: Arc<RouteEntry> = $entry;
281 move |state: State<S>,
282 headers: HeaderMap,
283 path_params: Path<PathParams>,
284 raw_query: RawQuery,
285 body: Bytes| handle(state, headers, path_params, raw_query, body, entry)
286 }};
287}
288
289struct PathRoutes {
291 path: String,
292 methods: Vec<Arc<RouteEntry>>,
293}
294
295impl PathRoutes {
296 fn add(&mut self, entry: Arc<RouteEntry>) {
298 let taken = self.methods.iter().any(|existing| {
299 existing.http_method == entry.http_method
300 || existing.http_method == RouteMethod::Any
301 || entry.http_method == RouteMethod::Any
302 });
303 if taken {
304 tracing::error!(
305 method = entry.http_method.as_str(),
306 path = %self.path,
307 rpc = %entry.grpc_path,
308 "HTTP method and path already bound to another RPC; skipping this binding"
309 );
310 return;
311 }
312 self.methods.push(entry);
313 }
314
315 fn method_router<S: TranscodeState>(&self) -> MethodRouter<S> {
320 let mut router = MethodRouter::new();
321 let mut extension: Vec<(Method, Arc<RouteEntry>)> = Vec::new();
322 let mut allow: Vec<&str> = Vec::new();
323 for entry in &self.methods {
324 match &entry.http_method {
325 RouteMethod::Any => return axum::routing::any(endpoint!(entry.clone())),
327 RouteMethod::One(method) => {
328 allow.push(method.as_str());
329 match MethodFilter::try_from(method.clone()) {
330 Ok(filter) => router = router.on(filter, endpoint!(entry.clone())),
331 Err(_) => extension.push((method.clone(), entry.clone())),
332 }
333 }
334 }
335 }
336 if extension.is_empty() {
337 return router;
338 }
339 if allow.contains(&"GET") && !allow.contains(&"HEAD") {
341 allow.push("HEAD");
342 }
343 let allow = HeaderValue::from_str(&allow.join(", "))
344 .expect("method tokens are valid header value characters");
345 let extension: Arc<[(Method, Arc<RouteEntry>)]> = extension.into();
346 router.fallback(
349 move |State(state): State<S>, request: axum::extract::Request| async move {
350 match extension
351 .iter()
352 .find(|(bound, _)| bound == request.method())
353 {
354 Some((_, entry)) => {
355 axum::handler::Handler::call(endpoint!(entry.clone()), request, state).await
356 }
357 None => (StatusCode::METHOD_NOT_ALLOWED, [(ALLOW, allow)]).into_response(),
358 }
359 },
360 )
361 }
362}
363
364struct RouteBinding {
367 entry: RouteEntry,
368 axum_path: String,
369}
370
371fn route_bindings(pool: &DescriptorPool, aliases: &[AliasConfig]) -> Vec<RouteBinding> {
377 let mut bindings = Vec::new();
378 for entry in extract_routes(pool) {
379 for alias in aliases {
380 if let Some(suffix) = entry.http_path.strip_prefix(&alias.to) {
381 if alias.from.ends_with("/{path}") {
382 let prefix = alias.from.trim_end_matches("/{path}");
383 bindings.push(RouteBinding {
384 axum_path: proto_path_to_axum(&format!("{prefix}{suffix}")),
385 entry: entry.clone(),
386 });
387 }
388 }
389 }
390 bindings.push(RouteBinding {
391 axum_path: proto_path_to_axum(&entry.http_path),
392 entry,
393 });
394 }
395 bindings
396}
397
398pub fn route_paths(pool: &DescriptorPool, aliases: &[AliasConfig]) -> Vec<(String, String)> {
409 route_bindings(pool, aliases)
410 .into_iter()
411 .map(|b| (b.entry.http_method.as_str().to_string(), b.axum_path))
412 .collect()
413}
414
415fn response_serialize_options() -> SerializeOptions {
418 SerializeOptions::new()
419 .skip_default_fields(false)
420 .stringify_64_bit_integers(true)
421}
422
423fn message_to_json_bytes(
425 msg: &DynamicMessage,
426 opts: &SerializeOptions,
427) -> Result<Vec<u8>, serde_json::Error> {
428 let mut buf = Vec::with_capacity(128);
429 msg.serialize_with_options(&mut serde_json::Serializer::new(&mut buf), opts)?;
430 Ok(buf)
431}
432
433fn message_to_json_string(msg: &DynamicMessage, opts: &SerializeOptions) -> Result<String, String> {
435 let buf = message_to_json_bytes(msg, opts).map_err(|e| e.to_string())?;
436 Ok(unsafe { String::from_utf8_unchecked(buf) })
439}
440
441fn json_body(
444 msg: &DynamicMessage,
445 response_body: Option<&str>,
446) -> Result<Vec<u8>, serde_json::Error> {
447 let opts = response_serialize_options();
448 let Some(path) = response_body else {
449 return message_to_json_bytes(msg, &opts);
450 };
451 let mut value = Some(msg.serialize_with_options(serde_json::value::Serializer, &opts)?);
455 let mut desc = Some(prost_reflect::ReflectMessage::descriptor(msg));
456 for segment in path.split('.') {
457 let field = desc.as_ref().and_then(|d| d.get_field_by_name(segment));
458 desc = match field.as_ref().map(FieldDescriptor::kind) {
459 Some(prost_reflect::Kind::Message(inner)) => Some(inner),
460 _ => None,
461 };
462 value = match (value, field) {
463 (Some(serde_json::Value::Object(mut fields)), Some(field)) => {
464 fields.remove(field.json_name())
465 }
466 _ => None,
467 };
468 }
469 let value = value.unwrap_or_else(|| {
470 tracing::warn!(
471 response_body = %path,
472 "configured response_body path not found in response; returning null"
473 );
474 serde_json::Value::Null
475 });
476 serde_json::to_vec(&value)
477}
478
479fn wants_sse(headers: &HeaderMap) -> bool {
487 headers
488 .get_all(axum::http::header::ACCEPT)
489 .iter()
490 .filter_map(|v| v.to_str().ok())
491 .flat_map(|accept| accept.split(','))
492 .any(accept_range_selects_sse)
493}
494
495fn accept_range_selects_sse(range: &str) -> bool {
498 let mut parts = range.split(';');
499 let media = parts.next().unwrap_or("").trim();
500 if !media.eq_ignore_ascii_case("text/event-stream") {
501 return false;
502 }
503 for param in parts {
506 let mut kv = param.splitn(2, '=');
507 if kv.next().unwrap_or("").trim().eq_ignore_ascii_case("q") {
508 let q: f32 = kv.next().unwrap_or("").trim().parse().unwrap_or(1.0);
509 return q > 0.0;
510 }
511 }
512 true
513}
514
515async fn handle<S: TranscodeState>(
517 State(proxy_state): State<S>,
518 headers: HeaderMap,
519 Path(path_params): Path<PathParams>,
520 RawQuery(raw_query): RawQuery,
521 body: Bytes,
522 entry: Arc<RouteEntry>,
523) -> Response {
524 let prepared = prepare(
525 &proxy_state,
526 &headers,
527 &path_params,
528 raw_query.as_deref(),
529 body,
530 &entry,
531 )
532 .await;
533 let (client, request) = match prepared {
534 Ok(prepared) => prepared,
535 Err(rejection) => return rejection.into_response(&entry),
536 };
537 if entry.streaming {
538 let keep_alive_secs = proxy_state.sse_keep_alive_secs();
539 streaming_call(client, request, entry, wants_sse(&headers), keep_alive_secs).await
540 } else {
541 unary_call(client, request, &entry).await
542 }
543}
544
545enum Rejection {
547 Unmappable(String),
549 NotReady(String),
551}
552
553impl Rejection {
554 fn into_response(self, entry: &RouteEntry) -> Response {
556 let status = match self {
557 Self::Unmappable(message) => tonic::Status::invalid_argument(message),
558 Self::NotReady(message) => tonic::Status::unavailable(message),
559 };
560 error::status_to_response_with_details(&status, entry.error_details.as_deref())
561 }
562}
563
564async fn prepare<S: TranscodeState>(
567 proxy_state: &S,
568 headers: &HeaderMap,
569 path_params: &PathParams,
570 raw_query: Option<&str>,
571 body: Bytes,
572 entry: &RouteEntry,
573) -> Result<
574 (
575 Grpc<tonic::transport::Channel>,
576 tonic::Request<DynamicMessage>,
577 ),
578 Rejection,
579> {
580 let message = decode_request(entry, headers, path_params, raw_query, body)
581 .map_err(Rejection::Unmappable)?;
582 let mut request = tonic::Request::new(message);
583 *request.metadata_mut() =
584 metadata::http_headers_to_grpc_metadata(headers, proxy_state.forwarded_headers());
585 metadata::apply_request_deadline(&mut request, headers);
586
587 let mut client = Grpc::new(proxy_state.grpc_channel());
588 if let Err(e) = client.ready().await {
589 return Err(Rejection::NotReady(format!("gRPC upstream not ready: {e}")));
590 }
591 Ok((client, request))
592}
593
594struct UnaryAnswer {
597 initial: MetadataMap,
598 message: DynamicMessage,
599 trailers: Option<MetadataMap>,
600}
601
602async fn call_unary(
607 client: &mut Grpc<tonic::transport::Channel>,
608 request: tonic::Request<DynamicMessage>,
609 entry: &RouteEntry,
610) -> Result<UnaryAnswer, tonic::Status> {
611 let response = client
612 .server_streaming(request, entry.grpc_path.clone(), entry.codec())
613 .await?;
614 let (initial, mut stream, _) = response.into_parts();
615 let message = match stream.message().await {
616 Ok(Some(message)) => message,
617 Ok(None) => {
618 let status = tonic::Status::internal("Missing response message.");
619 return Err(with_initial(status, initial));
620 }
621 Err(status) => return Err(with_initial(status, initial)),
622 };
623 let trailers = match stream.trailers().await {
624 Ok(trailers) => trailers,
625 Err(status) => return Err(with_initial(status, initial)),
626 };
627 Ok(UnaryAnswer {
628 initial,
629 message,
630 trailers,
631 })
632}
633
634fn with_initial(mut status: tonic::Status, initial: MetadataMap) -> tonic::Status {
638 let own = std::mem::take(status.metadata_mut()).into_headers();
639 let mut merged = initial.into_headers();
640 let mut last: Option<HeaderName> = None;
641 for (name, value) in own {
643 if let Some(name) = name {
644 merged.append(&name, value);
645 last = Some(name);
646 } else if let Some(name) = &last {
647 merged.append(name, value);
648 }
649 }
650 *status.metadata_mut() = MetadataMap::from_headers(merged);
651 status
652}
653
654async fn unary_call(
656 mut client: Grpc<tonic::transport::Channel>,
657 request: tonic::Request<DynamicMessage>,
658 entry: &RouteEntry,
659) -> Response {
660 match call_unary(&mut client, request, entry).await {
661 Ok(answer) => unary_success(entry, answer),
662 Err(status) => upstream_error(status, entry),
663 }
664}
665
666fn unary_success(entry: &RouteEntry, answer: UnaryAnswer) -> Response {
669 let mut upstream = UpstreamHeaders::default();
670 upstream.absorb(answer.initial, &entry.denied_headers);
671 if let Some(trailers) = answer.trailers {
672 upstream.absorb(trailers, &entry.denied_headers);
673 }
674 let status = match upstream.status() {
675 Ok(status) => status.unwrap_or(StatusCode::OK),
676 Err(response::InvalidHttpCode) => {
677 tracing::error!(
678 rpc = %entry.grpc_path,
679 "upstream set x-http-code to something other than one integer in 200-599"
680 );
681 return error::malformed_response(entry.error_details.as_deref());
682 }
683 };
684 let (content_type, body) = match &entry.response {
685 ResponseShape::HttpBody(path) => {
686 let raw = httpbody::take(answer.message, path);
687 match content_type_header(&raw.content_type) {
688 Ok(content_type) => (content_type, Body::from(raw.data)),
689 Err(()) => return invalid_content_type(entry),
690 }
691 }
692 ResponseShape::Json(response_body) => {
693 match json_body(&answer.message, response_body.as_deref()) {
694 Ok(json) => (
695 Some(HeaderValue::from_static("application/json")),
696 Body::from(json),
697 ),
698 Err(e) => {
699 tracing::error!("Failed to serialize gRPC response: {e}");
700 return error::status_to_response_with_details(
701 &tonic::Status::internal("failed to serialize response"),
702 entry.error_details.as_deref(),
703 );
704 }
705 }
706 }
707 };
708 response::build(status, upstream.into_headers(), content_type, body)
709}
710
711fn content_type_header(content_type: &str) -> Result<Option<HeaderValue>, ()> {
713 if content_type.is_empty() {
714 return Ok(None);
715 }
716 HeaderValue::from_str(content_type)
717 .map(Some)
718 .map_err(|_| ())
719}
720
721fn invalid_content_type(entry: &RouteEntry) -> Response {
723 tracing::error!(
724 rpc = %entry.grpc_path,
725 "upstream HttpBody content_type is not a valid header value"
726 );
727 error::malformed_response(entry.error_details.as_deref())
728}
729
730fn upstream_error(mut status: tonic::Status, entry: &RouteEntry) -> Response {
736 let (response, faithful) = error::render_response(&status, entry.error_details.as_deref());
737 let metadata = std::mem::take(status.metadata_mut());
738 if !faithful || metadata.is_empty() {
739 return response;
740 }
741 let mut upstream = UpstreamHeaders::default();
742 upstream.absorb(metadata, &entry.denied_headers);
743 response::with_upstream_headers(response, upstream.into_headers())
744}
745
746async fn streaming_call(
756 mut client: Grpc<tonic::transport::Channel>,
757 request: tonic::Request<DynamicMessage>,
758 entry: Arc<RouteEntry>,
759 use_sse: bool,
760 keep_alive_secs: u64,
761) -> Response {
762 let response = match client
763 .server_streaming(request, entry.grpc_path.clone(), entry.codec())
764 .await
765 {
766 Ok(response) => response,
767 Err(status) => return upstream_error(status, &entry),
769 };
770 let (initial, stream, _) = response.into_parts();
771 if matches!(entry.response, ResponseShape::HttpBody(_)) {
772 return http_body_stream(stream, entry, initial).await;
773 }
774 let mut upstream = UpstreamHeaders::default();
775 upstream.absorb(initial, &entry.denied_headers);
776 let headers = upstream.into_headers();
777 let envelope = entry.ndjson_envelope;
784 let render_error =
785 move |status: &tonic::Status| error::error_body(status, entry.error_details.as_deref());
786 let response = if use_sse {
787 sse_response(stream, render_error, keep_alive_secs)
788 } else {
789 ndjson_response(stream, render_error, envelope)
790 };
791 response::with_upstream_headers(response, headers)
792}
793
794async fn http_body_stream(
801 mut stream: tonic::Streaming<DynamicMessage>,
802 entry: Arc<RouteEntry>,
803 initial: MetadataMap,
804) -> Response {
805 let first = match stream.message().await {
806 Ok(Some(message)) => entry.raw_body(message).unwrap_or_default(),
807 Ok(None) => httpbody::RawBody::default(),
808 Err(status) => return upstream_error(with_initial(status, initial), &entry),
809 };
810 let mut upstream = UpstreamHeaders::default();
811 upstream.absorb(initial, &entry.denied_headers);
812 let headers = upstream.into_headers();
813 let content_type = match content_type_header(&first.content_type) {
814 Ok(content_type) => content_type,
815 Err(()) => return invalid_content_type(&entry),
816 };
817 let rest = stream.map(move |item| match item {
818 Ok(message) => Ok(entry.raw_body(message).unwrap_or_default().data),
819 Err(status) => {
820 tracing::error!(
821 rpc = %entry.grpc_path,
822 code = ?status.code(),
823 "server-streaming HttpBody failed after the response started; aborting the body"
824 );
825 Err(std::io::Error::other(status))
826 }
827 });
828 let chunks = futures::stream::once(futures::future::ready(Ok(first.data)))
829 .chain(rest)
830 .try_filter(|chunk| futures::future::ready(!chunk.is_empty()));
831 response::build(
832 StatusCode::OK,
833 headers,
834 content_type,
835 Body::from_stream(chunks),
836 )
837}
838
839enum StreamFrame {
847 Data(String),
848 Error(serde_json::Value),
849}
850
851const STATUS_TYPE_URL: &str = "type.googleapis.com/google.rpc.Status";
859
860fn json_frames<St, R>(
870 stream: St,
871 render_error: R,
872) -> impl futures::Stream<Item = StreamFrame> + Send + 'static
873where
874 St: futures::Stream<Item = Result<DynamicMessage, tonic::Status>> + Send + Unpin + 'static,
875 R: Fn(&tonic::Status) -> serde_json::Value + Send + 'static,
876{
877 let state = Some((stream, render_error, response_serialize_options()));
878 futures::stream::unfold(state, |state| async move {
879 let (mut stream, render_error, opts) = state?;
880 let error = match stream.next().await? {
881 Ok(msg) => match message_to_json_string(&msg, &opts) {
882 Ok(s) => return Some((StreamFrame::Data(s), Some((stream, render_error, opts)))),
883 Err(e) => tonic::Status::internal(format!("serialization error: {e}")),
884 },
885 Err(status) => status,
886 };
887 Some((StreamFrame::Error(render_error(&error)), None))
888 })
889}
890
891fn ndjson_response<St, R>(stream: St, render_error: R, envelope: bool) -> Response
898where
899 St: futures::Stream<Item = Result<DynamicMessage, tonic::Status>> + Send + Unpin + 'static,
900 R: Fn(&tonic::Status) -> serde_json::Value + Send + 'static,
901{
902 let byte_stream = json_frames(stream, render_error).map(move |frame| {
903 let mut line = match frame {
904 StreamFrame::Data(s) if envelope => {
907 let mut wrapped = String::with_capacity(s.len() + 12);
908 wrapped.push_str("{\"result\":");
909 wrapped.push_str(&s);
910 wrapped.push('}');
911 wrapped
912 }
913 StreamFrame::Data(s) => s,
914 StreamFrame::Error(body) if envelope => {
915 serde_json::json!({ "error": body }).to_string()
916 }
917 StreamFrame::Error(mut body) => {
918 if let Some(fields) = body.as_object_mut() {
919 fields.insert("@type".into(), STATUS_TYPE_URL.into());
920 }
921 body.to_string()
922 }
923 };
924 line.push('\n');
925 Ok::<Bytes, std::io::Error>(Bytes::from(line))
926 });
927
928 let body = Body::from_stream(byte_stream);
929 Response::builder()
933 .status(StatusCode::OK)
934 .header(CONTENT_TYPE, "application/x-ndjson")
935 .body(body)
936 .unwrap_or_else(|_| StatusCode::INTERNAL_SERVER_ERROR.into_response())
937}
938
939fn sse_response<St, R>(stream: St, render_error: R, keep_alive_secs: u64) -> Response
941where
942 St: futures::Stream<Item = Result<DynamicMessage, tonic::Status>> + Send + Unpin + 'static,
943 R: Fn(&tonic::Status) -> serde_json::Value + Send + 'static,
944{
945 let event_stream = json_frames(stream, render_error).map(|frame| {
949 let event = match frame {
950 StreamFrame::Data(s) => Event::default().data(s),
951 StreamFrame::Error(body) => Event::default()
952 .event("stream-error")
953 .data(body.to_string()),
954 };
955 Ok::<Event, std::convert::Infallible>(event)
956 });
957
958 Sse::new(event_stream)
959 .keep_alive(KeepAlive::new().interval(std::time::Duration::from_secs(keep_alive_secs)))
960 .into_response()
961}
962
963static NO_PARSED_BODY: request::BodyMapping = request::BodyMapping::None;
965
966fn decode_request(
972 entry: &RouteEntry,
973 headers: &HeaderMap,
974 path_params: &PathParams,
975 raw_query: Option<&str>,
976 body_bytes: Bytes,
977) -> Result<DynamicMessage, String> {
978 let (mapping, json_body) = match &entry.request_body {
981 RequestBody::Parsed(request::BodyMapping::None) => {
982 (&NO_PARSED_BODY, serde_json::Value::Null)
983 }
984 RequestBody::Parsed(mapping) => (
985 mapping,
986 body::parse_body(body::content_type(headers), &body_bytes)
987 .map_err(|e| format!("failed to parse request body: {e}"))?,
988 ),
989 RequestBody::RawRoot => (&NO_PARSED_BODY, serde_json::Value::Null),
990 RequestBody::RawField { mapping, .. } => (mapping, serde_json::Value::Null),
991 };
992
993 let query_pairs = match entry.request_body {
999 RequestBody::RawRoot => Vec::new(),
1000 _ => request::parse_query(raw_query)?,
1001 };
1002
1003 let input_desc = entry.method.input();
1004 let request_json =
1005 request::build_request_json(&input_desc, mapping, json_body, path_params, &query_pairs)?;
1006
1007 let mut message = DynamicMessage::deserialize(input_desc, request_json)
1008 .map_err(|e| format!("failed to decode request: {e}"))?;
1009 match &entry.request_body {
1010 RequestBody::Parsed(_) => {}
1011 RequestBody::RawRoot => {
1012 httpbody::fill(&mut message, request_content_type(headers)?, body_bytes);
1013 }
1014 RequestBody::RawField {
1015 field, http_body, ..
1016 } => {
1017 let mut inner = DynamicMessage::new(http_body.clone());
1018 httpbody::fill(&mut inner, request_content_type(headers)?, body_bytes);
1019 message.set_field(field, prost_reflect::Value::Message(inner));
1020 }
1021 }
1022 Ok(message)
1023}
1024
1025fn request_content_type(headers: &HeaderMap) -> Result<String, String> {
1029 match headers.get(CONTENT_TYPE) {
1030 None => Ok(String::new()),
1031 Some(value) => value
1032 .to_str()
1033 .map(str::to_owned)
1034 .map_err(|_| "request Content-Type is not a visible ASCII string".to_string()),
1035 }
1036}
1037
1038fn extract_routes(pool: &DescriptorPool) -> Vec<RouteEntry> {
1041 let http_ext = match pool.get_extension_by_name("google.api.http") {
1042 Some(ext) => ext,
1043 None => {
1044 tracing::warn!("google.api.http extension not found in descriptor pool");
1045 return Vec::new();
1046 }
1047 };
1048
1049 let mut entries = Vec::new();
1050
1051 for service in pool.services() {
1052 for method in service.methods() {
1053 if method.is_client_streaming() {
1054 continue;
1055 }
1056
1057 let grpc_path = format!("/{}/{}", service.full_name(), method.name());
1058 let grpc_path: axum::http::uri::PathAndQuery = match grpc_path.parse() {
1059 Ok(p) => p,
1060 Err(e) => {
1061 tracing::error!("skipping route with invalid gRPC path '{grpc_path}': {e}");
1062 continue;
1063 }
1064 };
1065
1066 let input = method.input();
1067 let output = method.output();
1068 let streaming = method.is_server_streaming();
1069 let response_has_required = codec::has_required_fields(&output);
1070 for binding in rule::http_bindings(&method, &http_ext) {
1071 if streaming {
1072 tracing::info!(
1073 "Registering streaming route: {} {} → {}",
1074 binding.method.as_str(),
1075 binding.path,
1076 grpc_path
1077 );
1078 }
1079 entries.push(RouteEntry {
1080 http_path: binding.path,
1081 http_method: binding.method,
1082 grpc_path: grpc_path.clone(),
1083 method: method.clone(),
1084 streaming,
1085 request_body: request_body(&input, binding.body),
1086 response: response_shape(&output, binding.response_body),
1087 error_details: None,
1089 ndjson_envelope: false,
1090 response_has_required,
1091 denied_headers: Arc::default(),
1092 });
1093 }
1094 }
1095 }
1096
1097 entries
1098}
1099
1100fn request_body(input: &MessageDescriptor, mapping: request::BodyMapping) -> RequestBody {
1106 let raw_field = match &mapping {
1107 request::BodyMapping::Root if httpbody::is_http_body(input) => return RequestBody::RawRoot,
1108 request::BodyMapping::Field(name) => httpbody::http_body_field(input, name),
1109 _ => None,
1110 };
1111 match raw_field {
1112 Some((field, http_body)) => RequestBody::RawField {
1113 mapping,
1114 field,
1115 http_body,
1116 },
1117 None => RequestBody::Parsed(mapping),
1118 }
1119}
1120
1121fn response_shape(output: &MessageDescriptor, response_body: Option<String>) -> ResponseShape {
1124 match response_body {
1125 None if httpbody::is_http_body(output) => ResponseShape::HttpBody(Vec::new()),
1126 None => ResponseShape::Json(None),
1127 Some(path) => match httpbody::http_body_path(output, &path) {
1128 Some(fields) => ResponseShape::HttpBody(fields),
1129 None => ResponseShape::Json(Some(path)),
1130 },
1131 }
1132}
1133
1134pub fn proto_path_to_axum(path: &str) -> String {
1145 let mut out = String::with_capacity(path.len());
1146
1147 let segments = split_top_level(path);
1148 let last = segments.len().saturating_sub(1);
1149 for (idx, segment) in segments.iter().enumerate() {
1150 if idx > 0 {
1151 out.push('/');
1152 }
1153 out.push_str(&convert_segment(segment, idx, idx == last));
1154 }
1155
1156 out
1157}
1158
1159fn split_top_level(path: &str) -> Vec<&str> {
1166 let mut segments = Vec::new();
1167 let mut depth = 0usize;
1168 let mut start = 0usize;
1169
1170 for (i, ch) in path.char_indices() {
1171 match ch {
1172 '{' => depth += 1,
1173 '}' if depth > 0 => depth -= 1,
1176 '/' if depth == 0 => {
1177 segments.push(&path[start..i]);
1178 start = i + 1;
1179 }
1180 _ => {}
1181 }
1182 }
1183 segments.push(&path[start..]);
1184 segments
1185}
1186
1187fn convert_segment(segment: &str, idx: usize, is_last: bool) -> String {
1192 if let Some(inner) = segment.strip_prefix('{').and_then(|s| s.strip_suffix('}')) {
1193 if let Some((name, template)) = inner.split_once('=') {
1195 return match template {
1196 "*" => format!("{{{name}}}"),
1198 "**" => catch_all(name, is_last),
1200 _ => {
1206 tracing::warn!(
1207 template = %inner,
1208 "google.api.http multi-segment field template is not fully \
1209 supported; routing it as a catch-all capture"
1210 );
1211 catch_all(name, is_last)
1212 }
1213 };
1214 }
1215 return format!("{{{inner}}}");
1217 }
1218
1219 match segment {
1221 "**" => catch_all(&format!("wildcard{idx}"), is_last),
1222 "*" => format!("{{wildcard{idx}}}"),
1223 literal => literal.to_string(),
1224 }
1225}
1226
1227fn catch_all(name: &str, is_last: bool) -> String {
1235 if is_last {
1236 format!("{{*{name}}}")
1237 } else {
1238 tracing::warn!(
1239 capture = %name,
1240 "catch-all in a non-terminal path segment is unrepresentable in axum; \
1241 degrading to a single-segment capture"
1242 );
1243 format!("{{{name}}}")
1244 }
1245}
1246
1247#[cfg(test)]
1248mod tests;