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::{OriginalUri, Path, RawQuery, State};
26use axum::http::header::{ALLOW, CONTENT_TYPE};
27use axum::http::{HeaderMap, HeaderName, HeaderValue, Method, StatusCode, Uri};
28use axum::response::sse::{Event, KeepAlive, Sse};
29use axum::response::{IntoResponse, Response};
30use axum::routing::{MethodFilter, MethodRouter};
31use axum::Extension;
32use axum::Router;
33use futures::{StreamExt, TryStreamExt};
34use prost_reflect::{
35 DescriptorPool, DynamicMessage, FieldDescriptor, MessageDescriptor, MethodDescriptor,
36 SerializeOptions,
37};
38use std::collections::HashMap;
39use std::sync::Arc;
40use std::time::Duration;
41use tonic::client::Grpc;
42use tonic::metadata::MetadataMap;
43
44use crate::client_address::ClientAddress;
45use crate::config::AliasConfig;
46use crate::received::ReceivedRequest;
47use crate::service::ConnectionInfo;
48use crate::upstream::Upstream;
49use error::{ErrorDetailsPolicy, StatusDetails};
50use response::UpstreamHeaders;
51use rule::RouteMethod;
52
53pub const UPSTREAM_DEADLINE: Duration = Duration::from_secs(5);
56
57pub trait TranscodeState: Clone + Send + Sync + 'static {
63 type Upstream: Upstream;
65 fn into_upstream(self) -> Self::Upstream;
67 fn forwarded_headers(&self) -> &[String];
69 fn sse_keep_alive_secs(&self) -> u64;
71}
72
73impl<U: Upstream> TranscodeState for crate::ProxyState<U> {
74 type Upstream = U;
75 fn into_upstream(self) -> U {
76 self.upstream
77 }
78 fn forwarded_headers(&self) -> &[String] {
79 &self.forwarded_headers
80 }
81 fn sse_keep_alive_secs(&self) -> u64 {
82 self.sse_keep_alive_secs
83 }
84}
85
86type PathParams = HashMap<String, String>;
88
89#[derive(Debug, Clone)]
91enum RequestBody {
92 Parsed(request::BodyMapping),
94 RawRoot,
96 RawField {
99 mapping: request::BodyMapping,
101 field: FieldDescriptor,
102 http_body: MessageDescriptor,
104 },
105}
106
107#[derive(Debug, Clone)]
109enum ResponseShape {
110 Json(Option<String>),
112 HttpBody(Vec<FieldDescriptor>),
115}
116
117#[derive(Debug, Clone)]
119struct RouteEntry {
120 http_path: String,
122 http_method: RouteMethod,
124 grpc_path: axum::http::uri::PathAndQuery,
127 method: MethodDescriptor,
129 streaming: bool,
131 request_body: RequestBody,
133 response: ResponseShape,
135 error_details: Option<Arc<StatusDetails>>,
138 ndjson_envelope: bool,
140 response_has_required: bool,
143 denied_headers: Arc<[HeaderName]>,
146}
147
148impl RouteEntry {
149 fn codec(&self) -> codec::DynamicCodec {
150 codec::DynamicCodec::with_required_check(self.method.output(), self.response_has_required)
151 }
152
153 fn raw_body(&self, message: DynamicMessage) -> Option<httpbody::RawBody> {
155 match &self.response {
156 ResponseShape::HttpBody(path) => Some(httpbody::take(message, path)),
157 ResponseShape::Json(_) => None,
158 }
159 }
160}
161
162#[derive(Debug, Clone, Default)]
178pub struct TranscodeOptions {
179 pub(crate) error_details: ErrorDetailsPolicy,
180 pub(crate) ndjson_envelope: bool,
181 pub(crate) denied_response_headers: Arc<[HeaderName]>,
182 pub(crate) selection: RpcSelection,
183}
184
185impl TranscodeOptions {
186 pub fn with_selection(mut self, selection: RpcSelection) -> Self {
189 self.selection = selection;
190 self
191 }
192
193 pub fn with_error_details(mut self, policy: ErrorDetailsPolicy) -> Self {
196 self.error_details = policy;
197 self
198 }
199
200 pub fn with_ndjson_envelope(mut self, enabled: bool) -> Self {
208 self.ndjson_envelope = enabled;
209 self
210 }
211
212 pub fn with_denied_response_headers(
218 mut self,
219 names: impl IntoIterator<Item = HeaderName>,
220 ) -> Self {
221 self.denied_response_headers = names.into_iter().collect();
222 self
223 }
224}
225
226pub fn routes<S: TranscodeState>(pool: &DescriptorPool, aliases: &[AliasConfig]) -> Router<S> {
233 routes_with_options(pool, aliases, &TranscodeOptions::default())
234}
235
236pub fn routes_with_options<S: TranscodeState>(
242 pool: &DescriptorPool,
243 aliases: &[AliasConfig],
244 options: &TranscodeOptions,
245) -> Router<S> {
246 let bindings = route_bindings(pool, aliases, &options.selection);
247 if bindings.is_empty() {
248 tracing::warn!("No HTTP-annotated RPCs found in proto descriptors");
249 return Router::new();
250 }
251
252 tracing::info!("Registering {} transcoded REST→gRPC routes", bindings.len());
253
254 let mut status_details: [Option<Arc<StatusDetails>>; 2] = [None, None];
258 let mut paths: Vec<PathRoutes> = Vec::new();
261 let mut path_index: HashMap<String, usize> = HashMap::new();
262 for mut binding in bindings {
263 let policy = &options.error_details;
264 if policy.enabled_for(&binding.axum_path) {
265 let opaque = policy.opaque_for(&binding.axum_path);
266 let slot = usize::from(opaque);
267 if status_details[slot].is_none() {
268 let base = status_details
269 .iter()
270 .flatten()
271 .next()
272 .map(|existing| existing.as_ref().clone())
273 .unwrap_or_else(|| StatusDetails::new(pool));
274 status_details[slot] = Some(Arc::new(base.with_opaque_details(opaque)));
275 }
276 binding.entry.error_details = status_details[slot].clone();
277 }
278 binding.entry.ndjson_envelope = options.ndjson_envelope;
279 binding.entry.denied_headers = options.denied_response_headers.clone();
280 let index = match path_index.get(&binding.axum_path) {
281 Some(&index) => index,
282 None => {
283 path_index.insert(binding.axum_path.clone(), paths.len());
284 paths.push(PathRoutes {
285 path: binding.axum_path,
286 methods: Vec::new(),
287 });
288 paths.len() - 1
289 }
290 };
291 paths[index].add(Arc::new(binding.entry));
292 }
293
294 let mut router: Router<S> = Router::new();
295 for path in &paths {
296 router = router.route(&path.path, path.method_router());
297 }
298 router
299}
300
301macro_rules! endpoint {
304 ($entry:expr) => {{
305 let entry: Arc<RouteEntry> = $entry;
306 move |state: State<S>,
307 method: Method,
308 OriginalUri(uri): OriginalUri,
309 headers: HeaderMap,
310 path_params: Path<PathParams>,
311 raw_query: RawQuery,
312 connection: Option<Extension<ConnectionInfo>>,
313 address: Option<Extension<ClientAddress>>,
314 body: Bytes| {
315 let client = Client {
316 headers,
317 origin: Origin {
318 connection: connection.map(|Extension(connection)| connection),
319 address: address.map(|Extension(address)| address),
320 },
321 line: RequestLine { method, uri },
322 };
323 handle(state, client, path_params, raw_query, body, entry)
324 }
325 }};
326}
327
328struct PathRoutes {
330 path: String,
331 methods: Vec<Arc<RouteEntry>>,
332}
333
334impl PathRoutes {
335 fn add(&mut self, entry: Arc<RouteEntry>) {
337 let taken = self.methods.iter().any(|existing| {
338 existing.http_method == entry.http_method
339 || existing.http_method == RouteMethod::Any
340 || entry.http_method == RouteMethod::Any
341 });
342 if taken {
343 tracing::error!(
344 method = entry.http_method.as_str(),
345 path = %self.path,
346 rpc = %entry.grpc_path,
347 "HTTP method and path already bound to another RPC; skipping this binding"
348 );
349 return;
350 }
351 self.methods.push(entry);
352 }
353
354 fn method_router<S: TranscodeState>(&self) -> MethodRouter<S> {
359 let mut router = MethodRouter::new();
360 let mut extension: Vec<(Method, Arc<RouteEntry>)> = Vec::new();
361 let mut allow: Vec<&str> = Vec::new();
362 for entry in &self.methods {
363 match &entry.http_method {
364 RouteMethod::Any => return axum::routing::any(endpoint!(entry.clone())),
366 RouteMethod::One(method) => {
367 allow.push(method.as_str());
368 match MethodFilter::try_from(method.clone()) {
369 Ok(filter) => router = router.on(filter, endpoint!(entry.clone())),
370 Err(_) => extension.push((method.clone(), entry.clone())),
371 }
372 }
373 }
374 }
375 if extension.is_empty() {
376 return router;
377 }
378 if allow.contains(&"GET") && !allow.contains(&"HEAD") {
380 allow.push("HEAD");
381 }
382 let allow = HeaderValue::from_str(&allow.join(", "))
383 .expect("method tokens are valid header value characters");
384 let extension: Arc<[(Method, Arc<RouteEntry>)]> = extension.into();
385 router.fallback(
388 move |State(state): State<S>, request: axum::extract::Request| async move {
389 match extension
390 .iter()
391 .find(|(bound, _)| bound == request.method())
392 {
393 Some((_, entry)) => {
394 axum::handler::Handler::call(endpoint!(entry.clone()), request, state).await
395 }
396 None => (StatusCode::METHOD_NOT_ALLOWED, [(ALLOW, allow)]).into_response(),
397 }
398 },
399 )
400 }
401}
402
403struct RouteBinding {
406 entry: RouteEntry,
407 axum_path: String,
408}
409
410fn route_bindings(
416 pool: &DescriptorPool,
417 aliases: &[AliasConfig],
418 selection: &RpcSelection,
419) -> Vec<RouteBinding> {
420 let mut bindings = Vec::new();
421 for entry in extract_routes(pool, selection) {
422 for alias in aliases {
423 if let Some(suffix) = entry.http_path.strip_prefix(&alias.to) {
424 if alias.from.ends_with("/{path}") {
425 let prefix = alias.from.trim_end_matches("/{path}");
426 bindings.push(RouteBinding {
427 axum_path: proto_path_to_axum(&format!("{prefix}{suffix}")),
428 entry: entry.clone(),
429 });
430 }
431 }
432 }
433 bindings.push(RouteBinding {
434 axum_path: proto_path_to_axum(&entry.http_path),
435 entry,
436 });
437 }
438 bindings
439}
440
441pub fn route_paths(
453 pool: &DescriptorPool,
454 aliases: &[AliasConfig],
455 selection: &RpcSelection,
456) -> Vec<(String, String)> {
457 route_bindings(pool, aliases, selection)
458 .into_iter()
459 .map(|b| (b.entry.http_method.as_str().to_string(), b.axum_path))
460 .collect()
461}
462
463fn response_serialize_options() -> SerializeOptions {
466 SerializeOptions::new()
467 .skip_default_fields(false)
468 .stringify_64_bit_integers(true)
469}
470
471fn message_to_json_bytes(
473 msg: &DynamicMessage,
474 opts: &SerializeOptions,
475) -> Result<Vec<u8>, serde_json::Error> {
476 let mut buf = Vec::with_capacity(128);
477 msg.serialize_with_options(&mut serde_json::Serializer::new(&mut buf), opts)?;
478 Ok(buf)
479}
480
481fn message_to_json_string(msg: &DynamicMessage, opts: &SerializeOptions) -> Result<String, String> {
483 let buf = message_to_json_bytes(msg, opts).map_err(|e| e.to_string())?;
484 Ok(unsafe { String::from_utf8_unchecked(buf) })
487}
488
489fn json_body(
492 msg: &DynamicMessage,
493 response_body: Option<&str>,
494) -> Result<Vec<u8>, serde_json::Error> {
495 let opts = response_serialize_options();
496 let Some(path) = response_body else {
497 return message_to_json_bytes(msg, &opts);
498 };
499 let mut value = Some(msg.serialize_with_options(serde_json::value::Serializer, &opts)?);
503 let mut desc = Some(prost_reflect::ReflectMessage::descriptor(msg));
504 for segment in path.split('.') {
505 let field = desc.as_ref().and_then(|d| d.get_field_by_name(segment));
506 desc = match field.as_ref().map(FieldDescriptor::kind) {
507 Some(prost_reflect::Kind::Message(inner)) => Some(inner),
508 _ => None,
509 };
510 value = match (value, field) {
511 (Some(serde_json::Value::Object(mut fields)), Some(field)) => {
512 fields.remove(field.json_name())
513 }
514 _ => None,
515 };
516 }
517 let value = value.unwrap_or_else(|| {
518 tracing::warn!(
519 response_body = %path,
520 "configured response_body path not found in response; returning null"
521 );
522 serde_json::Value::Null
523 });
524 serde_json::to_vec(&value)
525}
526
527fn wants_sse(headers: &HeaderMap) -> bool {
535 headers
536 .get_all(axum::http::header::ACCEPT)
537 .iter()
538 .filter_map(|v| v.to_str().ok())
539 .flat_map(|accept| accept.split(','))
540 .any(accept_range_selects_sse)
541}
542
543fn accept_range_selects_sse(range: &str) -> bool {
546 let mut parts = range.split(';');
547 let media = parts.next().unwrap_or("").trim();
548 if !media.eq_ignore_ascii_case("text/event-stream") {
549 return false;
550 }
551 for param in parts {
554 let mut kv = param.splitn(2, '=');
555 if kv.next().unwrap_or("").trim().eq_ignore_ascii_case("q") {
556 let q: f32 = kv.next().unwrap_or("").trim().parse().unwrap_or(1.0);
557 return q > 0.0;
558 }
559 }
560 true
561}
562
563struct Client {
566 headers: HeaderMap,
567 origin: Origin,
568 line: RequestLine,
569}
570
571struct RequestLine {
574 method: Method,
575 uri: Uri,
576}
577
578impl RequestLine {
579 fn received(self, entry: &RouteEntry) -> Result<ReceivedRequest, Unmappable> {
584 let path_and_query = self
585 .uri
586 .into_parts()
587 .path_and_query
588 .ok_or_else(|| Unmappable("the request target has no path".to_string()))?;
589 Ok(ReceivedRequest::new(
590 self.method,
591 path_and_query,
592 entry.grpc_path.clone(),
593 ))
594 }
595}
596
597struct Origin {
600 connection: Option<ConnectionInfo>,
601 address: Option<ClientAddress>,
602}
603
604async fn handle<S: TranscodeState>(
606 State(proxy_state): State<S>,
607 client: Client,
608 Path(path_params): Path<PathParams>,
609 RawQuery(raw_query): RawQuery,
610 body: Bytes,
611 entry: Arc<RouteEntry>,
612) -> Response {
613 let keep_alive_secs = proxy_state.sse_keep_alive_secs();
614 let Client {
615 headers,
616 origin,
617 line,
618 } = client;
619 let prepared = line.received(&entry).and_then(|received| {
620 let mut call = prepare(
621 proxy_state,
622 &headers,
623 origin,
624 &path_params,
625 raw_query.as_deref(),
626 body,
627 &entry,
628 )?;
629 call.request.extensions_mut().insert(received);
631 Ok(call)
632 });
633 let call = match prepared {
634 Ok(call) => call,
635 Err(rejection) => return rejection.into_response(&entry),
636 };
637 if entry.streaming {
638 streaming_call(call, entry, wants_sse(&headers), keep_alive_secs).await
639 } else {
640 unary_call(call, &entry).await
641 }
642}
643
644struct Unmappable(String);
647
648impl Unmappable {
649 fn into_response(self, entry: &RouteEntry) -> Response {
651 let status = tonic::Status::invalid_argument(self.0);
652 error::status_to_response_with_details(&status, entry.error_details.as_deref())
653 }
654}
655
656struct Call<U> {
659 upstream: U,
660 request: tonic::Request<DynamicMessage>,
661 deadline: Duration,
662}
663
664impl<U: Upstream> Call<U> {
665 async fn open(
671 self,
672 entry: &RouteEntry,
673 ) -> Result<tonic::Response<tonic::Streaming<DynamicMessage>>, tonic::Status> {
674 let Self {
675 upstream,
676 request,
677 deadline,
678 } = self;
679 let call = async move {
680 let mut client = Grpc::new(upstream);
681 if let Err(e) = client.ready().await {
682 let e: crate::upstream::BoxError = e.into();
683 return Err(tonic::Status::unavailable(format!(
684 "gRPC upstream not ready: {e}"
685 )));
686 }
687 client
688 .server_streaming(request, entry.grpc_path.clone(), entry.codec())
689 .await
690 };
691 match tokio::time::timeout(deadline, call).await {
692 Ok(result) => result,
693 Err(_) => Err(tonic::Status::deadline_exceeded(
694 "upstream did not answer within the deadline",
695 )),
696 }
697 }
698}
699
700fn prepare<S: TranscodeState>(
702 proxy_state: S,
703 headers: &HeaderMap,
704 origin: Origin,
705 path_params: &PathParams,
706 raw_query: Option<&str>,
707 body: Bytes,
708 entry: &RouteEntry,
709) -> Result<Call<S::Upstream>, Unmappable> {
710 let Origin {
711 connection,
712 address,
713 } = origin;
714 let request_metadata = match address {
718 Some(_) => {
719 metadata::rewritten_headers_to_grpc_metadata(headers, proxy_state.forwarded_headers())
720 }
721 None => {
722 metadata::try_http_headers_to_grpc_metadata(headers, proxy_state.forwarded_headers())
723 }
724 }
725 .map_err(|e| Unmappable(e.to_string()))?;
726 let message =
727 decode_request(entry, headers, path_params, raw_query, body).map_err(Unmappable)?;
728 let mut request = tonic::Request::new(message);
729 *request.metadata_mut() = request_metadata;
730 if let Some(connection) = connection {
735 connection.into_tonic_extensions(request.extensions_mut());
736 }
737 if let Some(address) = address {
738 request.extensions_mut().insert(address);
739 }
740 let deadline = metadata::apply_request_deadline(&mut request, headers)
744 .map_or(UPSTREAM_DEADLINE, |client| client.min(UPSTREAM_DEADLINE));
745
746 Ok(Call {
747 upstream: proxy_state.into_upstream(),
748 request,
749 deadline,
750 })
751}
752
753struct UnaryAnswer {
756 initial: MetadataMap,
757 message: DynamicMessage,
758 trailers: Option<MetadataMap>,
759}
760
761async fn call_unary<U: Upstream>(
766 call: Call<U>,
767 entry: &RouteEntry,
768) -> Result<UnaryAnswer, tonic::Status> {
769 let (initial, mut stream, _) = call.open(entry).await?.into_parts();
770 let message = match stream.message().await {
771 Ok(Some(message)) => message,
772 Ok(None) => {
773 let status = tonic::Status::internal("Missing response message.");
774 return Err(with_initial(status, initial));
775 }
776 Err(status) => return Err(with_initial(status, initial)),
777 };
778 let trailers = match stream.trailers().await {
779 Ok(trailers) => trailers,
780 Err(status) => return Err(with_initial(status, initial)),
781 };
782 Ok(UnaryAnswer {
783 initial,
784 message,
785 trailers,
786 })
787}
788
789fn with_initial(mut status: tonic::Status, initial: MetadataMap) -> tonic::Status {
793 let own = std::mem::take(status.metadata_mut()).into_headers();
794 let mut merged = initial.into_headers();
795 let mut last: Option<HeaderName> = None;
796 for (name, value) in own {
798 if let Some(name) = name {
799 merged.append(&name, value);
800 last = Some(name);
801 } else if let Some(name) = &last {
802 merged.append(name, value);
803 }
804 }
805 *status.metadata_mut() = MetadataMap::from_headers(merged);
806 status
807}
808
809async fn unary_call<U: Upstream>(call: Call<U>, entry: &RouteEntry) -> Response {
811 match call_unary(call, entry).await {
812 Ok(answer) => unary_success(entry, answer),
813 Err(status) => upstream_error(status, entry),
814 }
815}
816
817fn unary_success(entry: &RouteEntry, answer: UnaryAnswer) -> Response {
820 let mut upstream = UpstreamHeaders::default();
821 upstream.absorb(answer.initial, &entry.denied_headers);
822 if let Some(trailers) = answer.trailers {
823 upstream.absorb(trailers, &entry.denied_headers);
824 }
825 let status = match upstream.status() {
826 Ok(status) => status.unwrap_or(StatusCode::OK),
827 Err(response::InvalidHttpCode) => {
828 tracing::error!(
829 rpc = %entry.grpc_path,
830 "upstream set x-http-code to something other than one integer in 200-599"
831 );
832 return error::malformed_response(entry.error_details.as_deref());
833 }
834 };
835 let (content_type, body) = match &entry.response {
836 ResponseShape::HttpBody(path) => {
837 let raw = httpbody::take(answer.message, path);
838 match content_type_header(&raw.content_type) {
839 Ok(content_type) => (content_type, Body::from(raw.data)),
840 Err(()) => return invalid_content_type(entry),
841 }
842 }
843 ResponseShape::Json(response_body) => {
844 match json_body(&answer.message, response_body.as_deref()) {
845 Ok(json) => (
846 Some(HeaderValue::from_static("application/json")),
847 Body::from(json),
848 ),
849 Err(e) => {
850 tracing::error!("Failed to serialize gRPC response: {e}");
851 return error::status_to_response_with_details(
852 &tonic::Status::internal("failed to serialize response"),
853 entry.error_details.as_deref(),
854 );
855 }
856 }
857 }
858 };
859 response::build(status, upstream.into_headers(), content_type, body)
860}
861
862fn content_type_header(content_type: &str) -> Result<Option<HeaderValue>, ()> {
864 if content_type.is_empty() {
865 return Ok(None);
866 }
867 HeaderValue::from_str(content_type)
868 .map(Some)
869 .map_err(|_| ())
870}
871
872fn invalid_content_type(entry: &RouteEntry) -> Response {
874 tracing::error!(
875 rpc = %entry.grpc_path,
876 "upstream HttpBody content_type is not a valid header value"
877 );
878 error::malformed_response(entry.error_details.as_deref())
879}
880
881fn upstream_error(mut status: tonic::Status, entry: &RouteEntry) -> Response {
887 let (response, faithful) = error::render_response(&status, entry.error_details.as_deref());
888 let metadata = std::mem::take(status.metadata_mut());
889 if !faithful || metadata.is_empty() {
890 return response;
891 }
892 let mut upstream = UpstreamHeaders::default();
893 upstream.absorb(metadata, &entry.denied_headers);
894 response::with_upstream_headers(response, upstream.into_headers())
895}
896
897async fn streaming_call<U: Upstream>(
907 call: Call<U>,
908 entry: Arc<RouteEntry>,
909 use_sse: bool,
910 keep_alive_secs: u64,
911) -> Response {
912 let response = match call.open(&entry).await {
913 Ok(response) => response,
914 Err(status) => return upstream_error(status, &entry),
916 };
917 let (initial, stream, _) = response.into_parts();
918 if matches!(entry.response, ResponseShape::HttpBody(_)) {
919 return http_body_stream(stream, entry, initial).await;
920 }
921 let mut upstream = UpstreamHeaders::default();
922 upstream.absorb(initial, &entry.denied_headers);
923 let headers = upstream.into_headers();
924 let envelope = entry.ndjson_envelope;
931 let render_error =
932 move |status: &tonic::Status| error::error_body(status, entry.error_details.as_deref());
933 let response = if use_sse {
934 sse_response(stream, render_error, keep_alive_secs)
935 } else {
936 ndjson_response(stream, render_error, envelope)
937 };
938 response::with_upstream_headers(response, headers)
939}
940
941async fn http_body_stream(
948 mut stream: tonic::Streaming<DynamicMessage>,
949 entry: Arc<RouteEntry>,
950 initial: MetadataMap,
951) -> Response {
952 let first = match stream.message().await {
953 Ok(Some(message)) => entry.raw_body(message).unwrap_or_default(),
954 Ok(None) => httpbody::RawBody::default(),
955 Err(status) => return upstream_error(with_initial(status, initial), &entry),
956 };
957 let mut upstream = UpstreamHeaders::default();
958 upstream.absorb(initial, &entry.denied_headers);
959 let headers = upstream.into_headers();
960 let content_type = match content_type_header(&first.content_type) {
961 Ok(content_type) => content_type,
962 Err(()) => return invalid_content_type(&entry),
963 };
964 let rest = stream.map(move |item| match item {
965 Ok(message) => Ok(entry.raw_body(message).unwrap_or_default().data),
966 Err(status) => {
967 tracing::error!(
968 rpc = %entry.grpc_path,
969 code = ?status.code(),
970 "server-streaming HttpBody failed after the response started; aborting the body"
971 );
972 Err(std::io::Error::other(status))
973 }
974 });
975 let chunks = futures::stream::once(futures::future::ready(Ok(first.data)))
976 .chain(rest)
977 .try_filter(|chunk| futures::future::ready(!chunk.is_empty()));
978 response::build(
979 StatusCode::OK,
980 headers,
981 content_type,
982 Body::from_stream(chunks),
983 )
984}
985
986enum StreamFrame {
994 Data(String),
995 Error(serde_json::Value),
996}
997
998const STATUS_TYPE_URL: &str = "type.googleapis.com/google.rpc.Status";
1006
1007fn json_frames<St, R>(
1017 stream: St,
1018 render_error: R,
1019) -> impl futures::Stream<Item = StreamFrame> + Send + 'static
1020where
1021 St: futures::Stream<Item = Result<DynamicMessage, tonic::Status>> + Send + Unpin + 'static,
1022 R: Fn(&tonic::Status) -> serde_json::Value + Send + 'static,
1023{
1024 let state = Some((stream, render_error, response_serialize_options()));
1025 futures::stream::unfold(state, |state| async move {
1026 let (mut stream, render_error, opts) = state?;
1027 let error = match stream.next().await? {
1028 Ok(msg) => match message_to_json_string(&msg, &opts) {
1029 Ok(s) => return Some((StreamFrame::Data(s), Some((stream, render_error, opts)))),
1030 Err(e) => tonic::Status::internal(format!("serialization error: {e}")),
1031 },
1032 Err(status) => status,
1033 };
1034 Some((StreamFrame::Error(render_error(&error)), None))
1035 })
1036}
1037
1038fn ndjson_response<St, R>(stream: St, render_error: R, envelope: bool) -> Response
1045where
1046 St: futures::Stream<Item = Result<DynamicMessage, tonic::Status>> + Send + Unpin + 'static,
1047 R: Fn(&tonic::Status) -> serde_json::Value + Send + 'static,
1048{
1049 let byte_stream = json_frames(stream, render_error).map(move |frame| {
1050 let mut line = match frame {
1051 StreamFrame::Data(s) if envelope => {
1054 let mut wrapped = String::with_capacity(s.len() + 12);
1055 wrapped.push_str("{\"result\":");
1056 wrapped.push_str(&s);
1057 wrapped.push('}');
1058 wrapped
1059 }
1060 StreamFrame::Data(s) => s,
1061 StreamFrame::Error(body) if envelope => {
1062 serde_json::json!({ "error": body }).to_string()
1063 }
1064 StreamFrame::Error(mut body) => {
1065 if let Some(fields) = body.as_object_mut() {
1066 fields.insert("@type".into(), STATUS_TYPE_URL.into());
1067 }
1068 body.to_string()
1069 }
1070 };
1071 line.push('\n');
1072 Ok::<Bytes, std::io::Error>(Bytes::from(line))
1073 });
1074
1075 let body = Body::from_stream(byte_stream);
1076 Response::builder()
1080 .status(StatusCode::OK)
1081 .header(CONTENT_TYPE, "application/x-ndjson")
1082 .body(body)
1083 .unwrap_or_else(|_| StatusCode::INTERNAL_SERVER_ERROR.into_response())
1084}
1085
1086fn sse_response<St, R>(stream: St, render_error: R, keep_alive_secs: u64) -> Response
1088where
1089 St: futures::Stream<Item = Result<DynamicMessage, tonic::Status>> + Send + Unpin + 'static,
1090 R: Fn(&tonic::Status) -> serde_json::Value + Send + 'static,
1091{
1092 let event_stream = json_frames(stream, render_error).map(|frame| {
1096 let event = match frame {
1097 StreamFrame::Data(s) => Event::default().data(s),
1098 StreamFrame::Error(body) => Event::default()
1099 .event("stream-error")
1100 .data(body.to_string()),
1101 };
1102 Ok::<Event, std::convert::Infallible>(event)
1103 });
1104
1105 Sse::new(event_stream)
1106 .keep_alive(KeepAlive::new().interval(std::time::Duration::from_secs(keep_alive_secs)))
1107 .into_response()
1108}
1109
1110static NO_PARSED_BODY: request::BodyMapping = request::BodyMapping::None;
1112
1113fn decode_request(
1119 entry: &RouteEntry,
1120 headers: &HeaderMap,
1121 path_params: &PathParams,
1122 raw_query: Option<&str>,
1123 body_bytes: Bytes,
1124) -> Result<DynamicMessage, String> {
1125 let (mapping, body, raw_query) = match &entry.request_body {
1130 RequestBody::Parsed(mapping @ request::BodyMapping::None) => {
1131 (mapping, request::Body::Absent, raw_query)
1132 }
1133 RequestBody::Parsed(mapping) => (
1134 mapping,
1135 request::Body::new(body::content_type(headers), &body_bytes),
1136 raw_query,
1137 ),
1138 RequestBody::RawRoot => (&NO_PARSED_BODY, request::Body::Absent, None),
1139 RequestBody::RawField { mapping, .. } => (mapping, request::Body::Absent, raw_query),
1140 };
1141
1142 let mut message = request::build_request_message(
1143 &entry.method.input(),
1144 mapping,
1145 body,
1146 path_params,
1147 raw_query,
1148 )?;
1149 match &entry.request_body {
1150 RequestBody::Parsed(_) => {}
1151 RequestBody::RawRoot => {
1152 httpbody::fill(&mut message, request_content_type(headers)?, body_bytes);
1153 }
1154 RequestBody::RawField {
1155 field, http_body, ..
1156 } => {
1157 let mut inner = DynamicMessage::new(http_body.clone());
1158 httpbody::fill(&mut inner, request_content_type(headers)?, body_bytes);
1159 message.set_field(field, prost_reflect::Value::Message(inner));
1160 }
1161 }
1162 Ok(message)
1163}
1164
1165fn request_content_type(headers: &HeaderMap) -> Result<String, String> {
1169 match headers.get(CONTENT_TYPE) {
1170 None => Ok(String::new()),
1171 Some(value) => value
1172 .to_str()
1173 .map(str::to_owned)
1174 .map_err(|_| "request Content-Type is not a visible ASCII string".to_string()),
1175 }
1176}
1177
1178fn extract_routes(pool: &DescriptorPool, selection: &RpcSelection) -> Vec<RouteEntry> {
1181 let http_ext = match pool.get_extension_by_name("google.api.http") {
1182 Some(ext) => ext,
1183 None => {
1184 tracing::warn!("google.api.http extension not found in descriptor pool");
1185 return Vec::new();
1186 }
1187 };
1188
1189 let mut entries = Vec::new();
1190
1191 for service in pool.services() {
1192 for method in service.methods() {
1193 if method.is_client_streaming() || !selection.selects(&method) {
1194 continue;
1195 }
1196
1197 let grpc_path = format!("/{}/{}", service.full_name(), method.name());
1198 let grpc_path: axum::http::uri::PathAndQuery = match grpc_path.parse() {
1199 Ok(p) => p,
1200 Err(e) => {
1201 tracing::error!("skipping route with invalid gRPC path '{grpc_path}': {e}");
1202 continue;
1203 }
1204 };
1205
1206 let input = method.input();
1207 let output = method.output();
1208 let streaming = method.is_server_streaming();
1209 let response_has_required = codec::has_required_fields(&output);
1210 for binding in rule::http_bindings(&method, &http_ext) {
1211 if streaming {
1212 tracing::info!(
1213 "Registering streaming route: {} {} → {}",
1214 binding.method.as_str(),
1215 binding.path,
1216 grpc_path
1217 );
1218 }
1219 entries.push(RouteEntry {
1220 http_path: binding.path,
1221 http_method: binding.method,
1222 grpc_path: grpc_path.clone(),
1223 method: method.clone(),
1224 streaming,
1225 request_body: request_body(&input, binding.body),
1226 response: response_shape(&output, binding.response_body),
1227 error_details: None,
1229 ndjson_envelope: false,
1230 response_has_required,
1231 denied_headers: Arc::default(),
1232 });
1233 }
1234 }
1235 }
1236
1237 entries
1238}
1239
1240fn request_body(input: &MessageDescriptor, mapping: request::BodyMapping) -> RequestBody {
1246 let raw_field = match &mapping {
1247 request::BodyMapping::Root if httpbody::is_http_body(input) => return RequestBody::RawRoot,
1248 request::BodyMapping::Field(name) => httpbody::http_body_field(input, name),
1249 _ => None,
1250 };
1251 match raw_field {
1252 Some((field, http_body)) => RequestBody::RawField {
1253 mapping,
1254 field,
1255 http_body,
1256 },
1257 None => RequestBody::Parsed(mapping),
1258 }
1259}
1260
1261fn response_shape(output: &MessageDescriptor, response_body: Option<String>) -> ResponseShape {
1264 match response_body {
1265 None if httpbody::is_http_body(output) => ResponseShape::HttpBody(Vec::new()),
1266 None => ResponseShape::Json(None),
1267 Some(path) => match httpbody::http_body_path(output, &path) {
1268 Some(fields) => ResponseShape::HttpBody(fields),
1269 None => ResponseShape::Json(Some(path)),
1270 },
1271 }
1272}
1273
1274pub fn proto_path_to_axum(path: &str) -> String {
1285 let mut out = String::with_capacity(path.len());
1286
1287 let segments = split_top_level(path);
1288 let last = segments.len().saturating_sub(1);
1289 for (idx, segment) in segments.iter().enumerate() {
1290 if idx > 0 {
1291 out.push('/');
1292 }
1293 out.push_str(&convert_segment(segment, idx, idx == last));
1294 }
1295
1296 out
1297}
1298
1299fn split_top_level(path: &str) -> Vec<&str> {
1306 let mut segments = Vec::new();
1307 let mut depth = 0usize;
1308 let mut start = 0usize;
1309
1310 for (i, ch) in path.char_indices() {
1311 match ch {
1312 '{' => depth += 1,
1313 '}' if depth > 0 => depth -= 1,
1316 '/' if depth == 0 => {
1317 segments.push(&path[start..i]);
1318 start = i + 1;
1319 }
1320 _ => {}
1321 }
1322 }
1323 segments.push(&path[start..]);
1324 segments
1325}
1326
1327fn convert_segment(segment: &str, idx: usize, is_last: bool) -> String {
1332 if let Some(inner) = segment.strip_prefix('{').and_then(|s| s.strip_suffix('}')) {
1333 if let Some((name, template)) = inner.split_once('=') {
1335 return match template {
1336 "*" => format!("{{{name}}}"),
1338 "**" => catch_all(name, is_last),
1340 _ => {
1346 tracing::warn!(
1347 template = %inner,
1348 "google.api.http multi-segment field template is not fully \
1349 supported; routing it as a catch-all capture"
1350 );
1351 catch_all(name, is_last)
1352 }
1353 };
1354 }
1355 return format!("{{{inner}}}");
1357 }
1358
1359 match segment {
1361 "**" => catch_all(&format!("wildcard{idx}"), is_last),
1362 "*" => format!("{{wildcard{idx}}}"),
1363 literal => literal.to_string(),
1364 }
1365}
1366
1367fn catch_all(name: &str, is_last: bool) -> String {
1375 if is_last {
1376 format!("{{*{name}}}")
1377 } else {
1378 tracing::warn!(
1379 capture = %name,
1380 "catch-all in a non-terminal path segment is unrepresentable in axum; \
1381 degrading to a single-segment capture"
1382 );
1383 format!("{{{name}}}")
1384 }
1385}
1386
1387#[cfg(test)]
1388mod tests;