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 request_metadata =
581 metadata::try_http_headers_to_grpc_metadata(headers, proxy_state.forwarded_headers())
582 .map_err(|e| Rejection::Unmappable(e.to_string()))?;
583 let message = decode_request(entry, headers, path_params, raw_query, body)
584 .map_err(Rejection::Unmappable)?;
585 let mut request = tonic::Request::new(message);
586 *request.metadata_mut() = request_metadata;
587 metadata::apply_request_deadline(&mut request, headers);
588
589 let mut client = Grpc::new(proxy_state.grpc_channel());
590 if let Err(e) = client.ready().await {
591 return Err(Rejection::NotReady(format!("gRPC upstream not ready: {e}")));
592 }
593 Ok((client, request))
594}
595
596struct UnaryAnswer {
599 initial: MetadataMap,
600 message: DynamicMessage,
601 trailers: Option<MetadataMap>,
602}
603
604async fn call_unary(
609 client: &mut Grpc<tonic::transport::Channel>,
610 request: tonic::Request<DynamicMessage>,
611 entry: &RouteEntry,
612) -> Result<UnaryAnswer, tonic::Status> {
613 let response = client
614 .server_streaming(request, entry.grpc_path.clone(), entry.codec())
615 .await?;
616 let (initial, mut stream, _) = response.into_parts();
617 let message = match stream.message().await {
618 Ok(Some(message)) => message,
619 Ok(None) => {
620 let status = tonic::Status::internal("Missing response message.");
621 return Err(with_initial(status, initial));
622 }
623 Err(status) => return Err(with_initial(status, initial)),
624 };
625 let trailers = match stream.trailers().await {
626 Ok(trailers) => trailers,
627 Err(status) => return Err(with_initial(status, initial)),
628 };
629 Ok(UnaryAnswer {
630 initial,
631 message,
632 trailers,
633 })
634}
635
636fn with_initial(mut status: tonic::Status, initial: MetadataMap) -> tonic::Status {
640 let own = std::mem::take(status.metadata_mut()).into_headers();
641 let mut merged = initial.into_headers();
642 let mut last: Option<HeaderName> = None;
643 for (name, value) in own {
645 if let Some(name) = name {
646 merged.append(&name, value);
647 last = Some(name);
648 } else if let Some(name) = &last {
649 merged.append(name, value);
650 }
651 }
652 *status.metadata_mut() = MetadataMap::from_headers(merged);
653 status
654}
655
656async fn unary_call(
658 mut client: Grpc<tonic::transport::Channel>,
659 request: tonic::Request<DynamicMessage>,
660 entry: &RouteEntry,
661) -> Response {
662 match call_unary(&mut client, request, entry).await {
663 Ok(answer) => unary_success(entry, answer),
664 Err(status) => upstream_error(status, entry),
665 }
666}
667
668fn unary_success(entry: &RouteEntry, answer: UnaryAnswer) -> Response {
671 let mut upstream = UpstreamHeaders::default();
672 upstream.absorb(answer.initial, &entry.denied_headers);
673 if let Some(trailers) = answer.trailers {
674 upstream.absorb(trailers, &entry.denied_headers);
675 }
676 let status = match upstream.status() {
677 Ok(status) => status.unwrap_or(StatusCode::OK),
678 Err(response::InvalidHttpCode) => {
679 tracing::error!(
680 rpc = %entry.grpc_path,
681 "upstream set x-http-code to something other than one integer in 200-599"
682 );
683 return error::malformed_response(entry.error_details.as_deref());
684 }
685 };
686 let (content_type, body) = match &entry.response {
687 ResponseShape::HttpBody(path) => {
688 let raw = httpbody::take(answer.message, path);
689 match content_type_header(&raw.content_type) {
690 Ok(content_type) => (content_type, Body::from(raw.data)),
691 Err(()) => return invalid_content_type(entry),
692 }
693 }
694 ResponseShape::Json(response_body) => {
695 match json_body(&answer.message, response_body.as_deref()) {
696 Ok(json) => (
697 Some(HeaderValue::from_static("application/json")),
698 Body::from(json),
699 ),
700 Err(e) => {
701 tracing::error!("Failed to serialize gRPC response: {e}");
702 return error::status_to_response_with_details(
703 &tonic::Status::internal("failed to serialize response"),
704 entry.error_details.as_deref(),
705 );
706 }
707 }
708 }
709 };
710 response::build(status, upstream.into_headers(), content_type, body)
711}
712
713fn content_type_header(content_type: &str) -> Result<Option<HeaderValue>, ()> {
715 if content_type.is_empty() {
716 return Ok(None);
717 }
718 HeaderValue::from_str(content_type)
719 .map(Some)
720 .map_err(|_| ())
721}
722
723fn invalid_content_type(entry: &RouteEntry) -> Response {
725 tracing::error!(
726 rpc = %entry.grpc_path,
727 "upstream HttpBody content_type is not a valid header value"
728 );
729 error::malformed_response(entry.error_details.as_deref())
730}
731
732fn upstream_error(mut status: tonic::Status, entry: &RouteEntry) -> Response {
738 let (response, faithful) = error::render_response(&status, entry.error_details.as_deref());
739 let metadata = std::mem::take(status.metadata_mut());
740 if !faithful || metadata.is_empty() {
741 return response;
742 }
743 let mut upstream = UpstreamHeaders::default();
744 upstream.absorb(metadata, &entry.denied_headers);
745 response::with_upstream_headers(response, upstream.into_headers())
746}
747
748async fn streaming_call(
758 mut client: Grpc<tonic::transport::Channel>,
759 request: tonic::Request<DynamicMessage>,
760 entry: Arc<RouteEntry>,
761 use_sse: bool,
762 keep_alive_secs: u64,
763) -> Response {
764 let response = match client
765 .server_streaming(request, entry.grpc_path.clone(), entry.codec())
766 .await
767 {
768 Ok(response) => response,
769 Err(status) => return upstream_error(status, &entry),
771 };
772 let (initial, stream, _) = response.into_parts();
773 if matches!(entry.response, ResponseShape::HttpBody(_)) {
774 return http_body_stream(stream, entry, initial).await;
775 }
776 let mut upstream = UpstreamHeaders::default();
777 upstream.absorb(initial, &entry.denied_headers);
778 let headers = upstream.into_headers();
779 let envelope = entry.ndjson_envelope;
786 let render_error =
787 move |status: &tonic::Status| error::error_body(status, entry.error_details.as_deref());
788 let response = if use_sse {
789 sse_response(stream, render_error, keep_alive_secs)
790 } else {
791 ndjson_response(stream, render_error, envelope)
792 };
793 response::with_upstream_headers(response, headers)
794}
795
796async fn http_body_stream(
803 mut stream: tonic::Streaming<DynamicMessage>,
804 entry: Arc<RouteEntry>,
805 initial: MetadataMap,
806) -> Response {
807 let first = match stream.message().await {
808 Ok(Some(message)) => entry.raw_body(message).unwrap_or_default(),
809 Ok(None) => httpbody::RawBody::default(),
810 Err(status) => return upstream_error(with_initial(status, initial), &entry),
811 };
812 let mut upstream = UpstreamHeaders::default();
813 upstream.absorb(initial, &entry.denied_headers);
814 let headers = upstream.into_headers();
815 let content_type = match content_type_header(&first.content_type) {
816 Ok(content_type) => content_type,
817 Err(()) => return invalid_content_type(&entry),
818 };
819 let rest = stream.map(move |item| match item {
820 Ok(message) => Ok(entry.raw_body(message).unwrap_or_default().data),
821 Err(status) => {
822 tracing::error!(
823 rpc = %entry.grpc_path,
824 code = ?status.code(),
825 "server-streaming HttpBody failed after the response started; aborting the body"
826 );
827 Err(std::io::Error::other(status))
828 }
829 });
830 let chunks = futures::stream::once(futures::future::ready(Ok(first.data)))
831 .chain(rest)
832 .try_filter(|chunk| futures::future::ready(!chunk.is_empty()));
833 response::build(
834 StatusCode::OK,
835 headers,
836 content_type,
837 Body::from_stream(chunks),
838 )
839}
840
841enum StreamFrame {
849 Data(String),
850 Error(serde_json::Value),
851}
852
853const STATUS_TYPE_URL: &str = "type.googleapis.com/google.rpc.Status";
861
862fn json_frames<St, R>(
872 stream: St,
873 render_error: R,
874) -> impl futures::Stream<Item = StreamFrame> + Send + 'static
875where
876 St: futures::Stream<Item = Result<DynamicMessage, tonic::Status>> + Send + Unpin + 'static,
877 R: Fn(&tonic::Status) -> serde_json::Value + Send + 'static,
878{
879 let state = Some((stream, render_error, response_serialize_options()));
880 futures::stream::unfold(state, |state| async move {
881 let (mut stream, render_error, opts) = state?;
882 let error = match stream.next().await? {
883 Ok(msg) => match message_to_json_string(&msg, &opts) {
884 Ok(s) => return Some((StreamFrame::Data(s), Some((stream, render_error, opts)))),
885 Err(e) => tonic::Status::internal(format!("serialization error: {e}")),
886 },
887 Err(status) => status,
888 };
889 Some((StreamFrame::Error(render_error(&error)), None))
890 })
891}
892
893fn ndjson_response<St, R>(stream: St, render_error: R, envelope: bool) -> Response
900where
901 St: futures::Stream<Item = Result<DynamicMessage, tonic::Status>> + Send + Unpin + 'static,
902 R: Fn(&tonic::Status) -> serde_json::Value + Send + 'static,
903{
904 let byte_stream = json_frames(stream, render_error).map(move |frame| {
905 let mut line = match frame {
906 StreamFrame::Data(s) if envelope => {
909 let mut wrapped = String::with_capacity(s.len() + 12);
910 wrapped.push_str("{\"result\":");
911 wrapped.push_str(&s);
912 wrapped.push('}');
913 wrapped
914 }
915 StreamFrame::Data(s) => s,
916 StreamFrame::Error(body) if envelope => {
917 serde_json::json!({ "error": body }).to_string()
918 }
919 StreamFrame::Error(mut body) => {
920 if let Some(fields) = body.as_object_mut() {
921 fields.insert("@type".into(), STATUS_TYPE_URL.into());
922 }
923 body.to_string()
924 }
925 };
926 line.push('\n');
927 Ok::<Bytes, std::io::Error>(Bytes::from(line))
928 });
929
930 let body = Body::from_stream(byte_stream);
931 Response::builder()
935 .status(StatusCode::OK)
936 .header(CONTENT_TYPE, "application/x-ndjson")
937 .body(body)
938 .unwrap_or_else(|_| StatusCode::INTERNAL_SERVER_ERROR.into_response())
939}
940
941fn sse_response<St, R>(stream: St, render_error: R, keep_alive_secs: u64) -> Response
943where
944 St: futures::Stream<Item = Result<DynamicMessage, tonic::Status>> + Send + Unpin + 'static,
945 R: Fn(&tonic::Status) -> serde_json::Value + Send + 'static,
946{
947 let event_stream = json_frames(stream, render_error).map(|frame| {
951 let event = match frame {
952 StreamFrame::Data(s) => Event::default().data(s),
953 StreamFrame::Error(body) => Event::default()
954 .event("stream-error")
955 .data(body.to_string()),
956 };
957 Ok::<Event, std::convert::Infallible>(event)
958 });
959
960 Sse::new(event_stream)
961 .keep_alive(KeepAlive::new().interval(std::time::Duration::from_secs(keep_alive_secs)))
962 .into_response()
963}
964
965static NO_PARSED_BODY: request::BodyMapping = request::BodyMapping::None;
967
968fn decode_request(
974 entry: &RouteEntry,
975 headers: &HeaderMap,
976 path_params: &PathParams,
977 raw_query: Option<&str>,
978 body_bytes: Bytes,
979) -> Result<DynamicMessage, String> {
980 let (mapping, body, raw_query) = match &entry.request_body {
985 RequestBody::Parsed(mapping @ request::BodyMapping::None) => {
986 (mapping, request::Body::Absent, raw_query)
987 }
988 RequestBody::Parsed(mapping) => (
989 mapping,
990 request::Body::new(body::content_type(headers), &body_bytes),
991 raw_query,
992 ),
993 RequestBody::RawRoot => (&NO_PARSED_BODY, request::Body::Absent, None),
994 RequestBody::RawField { mapping, .. } => (mapping, request::Body::Absent, raw_query),
995 };
996
997 let mut message = request::build_request_message(
998 &entry.method.input(),
999 mapping,
1000 body,
1001 path_params,
1002 raw_query,
1003 )?;
1004 match &entry.request_body {
1005 RequestBody::Parsed(_) => {}
1006 RequestBody::RawRoot => {
1007 httpbody::fill(&mut message, request_content_type(headers)?, body_bytes);
1008 }
1009 RequestBody::RawField {
1010 field, http_body, ..
1011 } => {
1012 let mut inner = DynamicMessage::new(http_body.clone());
1013 httpbody::fill(&mut inner, request_content_type(headers)?, body_bytes);
1014 message.set_field(field, prost_reflect::Value::Message(inner));
1015 }
1016 }
1017 Ok(message)
1018}
1019
1020fn request_content_type(headers: &HeaderMap) -> Result<String, String> {
1024 match headers.get(CONTENT_TYPE) {
1025 None => Ok(String::new()),
1026 Some(value) => value
1027 .to_str()
1028 .map(str::to_owned)
1029 .map_err(|_| "request Content-Type is not a visible ASCII string".to_string()),
1030 }
1031}
1032
1033fn extract_routes(pool: &DescriptorPool) -> Vec<RouteEntry> {
1036 let http_ext = match pool.get_extension_by_name("google.api.http") {
1037 Some(ext) => ext,
1038 None => {
1039 tracing::warn!("google.api.http extension not found in descriptor pool");
1040 return Vec::new();
1041 }
1042 };
1043
1044 let mut entries = Vec::new();
1045
1046 for service in pool.services() {
1047 for method in service.methods() {
1048 if method.is_client_streaming() {
1049 continue;
1050 }
1051
1052 let grpc_path = format!("/{}/{}", service.full_name(), method.name());
1053 let grpc_path: axum::http::uri::PathAndQuery = match grpc_path.parse() {
1054 Ok(p) => p,
1055 Err(e) => {
1056 tracing::error!("skipping route with invalid gRPC path '{grpc_path}': {e}");
1057 continue;
1058 }
1059 };
1060
1061 let input = method.input();
1062 let output = method.output();
1063 let streaming = method.is_server_streaming();
1064 let response_has_required = codec::has_required_fields(&output);
1065 for binding in rule::http_bindings(&method, &http_ext) {
1066 if streaming {
1067 tracing::info!(
1068 "Registering streaming route: {} {} → {}",
1069 binding.method.as_str(),
1070 binding.path,
1071 grpc_path
1072 );
1073 }
1074 entries.push(RouteEntry {
1075 http_path: binding.path,
1076 http_method: binding.method,
1077 grpc_path: grpc_path.clone(),
1078 method: method.clone(),
1079 streaming,
1080 request_body: request_body(&input, binding.body),
1081 response: response_shape(&output, binding.response_body),
1082 error_details: None,
1084 ndjson_envelope: false,
1085 response_has_required,
1086 denied_headers: Arc::default(),
1087 });
1088 }
1089 }
1090 }
1091
1092 entries
1093}
1094
1095fn request_body(input: &MessageDescriptor, mapping: request::BodyMapping) -> RequestBody {
1101 let raw_field = match &mapping {
1102 request::BodyMapping::Root if httpbody::is_http_body(input) => return RequestBody::RawRoot,
1103 request::BodyMapping::Field(name) => httpbody::http_body_field(input, name),
1104 _ => None,
1105 };
1106 match raw_field {
1107 Some((field, http_body)) => RequestBody::RawField {
1108 mapping,
1109 field,
1110 http_body,
1111 },
1112 None => RequestBody::Parsed(mapping),
1113 }
1114}
1115
1116fn response_shape(output: &MessageDescriptor, response_body: Option<String>) -> ResponseShape {
1119 match response_body {
1120 None if httpbody::is_http_body(output) => ResponseShape::HttpBody(Vec::new()),
1121 None => ResponseShape::Json(None),
1122 Some(path) => match httpbody::http_body_path(output, &path) {
1123 Some(fields) => ResponseShape::HttpBody(fields),
1124 None => ResponseShape::Json(Some(path)),
1125 },
1126 }
1127}
1128
1129pub fn proto_path_to_axum(path: &str) -> String {
1140 let mut out = String::with_capacity(path.len());
1141
1142 let segments = split_top_level(path);
1143 let last = segments.len().saturating_sub(1);
1144 for (idx, segment) in segments.iter().enumerate() {
1145 if idx > 0 {
1146 out.push('/');
1147 }
1148 out.push_str(&convert_segment(segment, idx, idx == last));
1149 }
1150
1151 out
1152}
1153
1154fn split_top_level(path: &str) -> Vec<&str> {
1161 let mut segments = Vec::new();
1162 let mut depth = 0usize;
1163 let mut start = 0usize;
1164
1165 for (i, ch) in path.char_indices() {
1166 match ch {
1167 '{' => depth += 1,
1168 '}' if depth > 0 => depth -= 1,
1171 '/' if depth == 0 => {
1172 segments.push(&path[start..i]);
1173 start = i + 1;
1174 }
1175 _ => {}
1176 }
1177 }
1178 segments.push(&path[start..]);
1179 segments
1180}
1181
1182fn convert_segment(segment: &str, idx: usize, is_last: bool) -> String {
1187 if let Some(inner) = segment.strip_prefix('{').and_then(|s| s.strip_suffix('}')) {
1188 if let Some((name, template)) = inner.split_once('=') {
1190 return match template {
1191 "*" => format!("{{{name}}}"),
1193 "**" => catch_all(name, is_last),
1195 _ => {
1201 tracing::warn!(
1202 template = %inner,
1203 "google.api.http multi-segment field template is not fully \
1204 supported; routing it as a catch-all capture"
1205 );
1206 catch_all(name, is_last)
1207 }
1208 };
1209 }
1210 return format!("{{{inner}}}");
1212 }
1213
1214 match segment {
1216 "**" => catch_all(&format!("wildcard{idx}"), is_last),
1217 "*" => format!("{{wildcard{idx}}}"),
1218 literal => literal.to_string(),
1219 }
1220}
1221
1222fn catch_all(name: &str, is_last: bool) -> String {
1230 if is_last {
1231 format!("{{*{name}}}")
1232 } else {
1233 tracing::warn!(
1234 capture = %name,
1235 "catch-all in a non-terminal path segment is unrepresentable in axum; \
1236 degrading to a single-segment capture"
1237 );
1238 format!("{{{name}}}")
1239 }
1240}
1241
1242#[cfg(test)]
1243mod tests;