pub mod body;
pub mod codec;
pub mod error;
pub(crate) mod httpbody;
pub mod metadata;
pub(crate) mod path;
pub mod request;
pub(crate) mod response;
pub(crate) mod rule;
mod select;
mod table;
pub use path::proto_path_to_axum;
pub use select::RpcSelection;
use axum::body::{Body, Bytes};
use axum::extract::{FromRequest, FromRequestParts, OriginalUri, RawPathParams, Request, State};
use axum::http::header::{ALLOW, CONTENT_TYPE};
use axum::http::{HeaderMap, HeaderName, HeaderValue, Method, StatusCode, Uri};
use axum::response::sse::{Event, KeepAlive, Sse};
use axum::response::{IntoResponse, Response};
use axum::Router;
use futures::{StreamExt, TryStreamExt};
use prost_reflect::{
DescriptorPool, DynamicMessage, FieldDescriptor, MessageDescriptor, MethodDescriptor,
SerializeOptions,
};
use std::borrow::Cow;
use std::collections::HashMap;
use std::sync::Arc;
use std::time::Duration;
use tonic::client::Grpc;
use tonic::metadata::MetadataMap;
use crate::client_address::ClientAddress;
use crate::config::AliasConfig;
use crate::guard::BoxedService;
use crate::received::ReceivedRequest;
use crate::service::ConnectionInfo;
use crate::upstream::Upstream;
use error::{ErrorDetailsPolicy, StatusDetails};
use path::MountedPath;
use response::UpstreamHeaders;
use rule::RouteMethod;
use table::{Choice, PathTable, Routes};
pub const UPSTREAM_DEADLINE: Duration = Duration::from_secs(5);
pub trait TranscodeState: Clone + Send + Sync + 'static {
type Upstream: Upstream;
fn into_upstream(self) -> Self::Upstream;
fn forwarded_headers(&self) -> &[String];
fn forwarded_header_names(&self) -> Option<&[HeaderName]> {
None
}
fn sse_keep_alive_secs(&self) -> u64;
}
impl<U: Upstream> TranscodeState for crate::ProxyState<U> {
type Upstream = U;
fn into_upstream(self) -> U {
self.upstream
}
fn forwarded_headers(&self) -> &[String] {
&self.forwarded_headers
}
fn forwarded_header_names(&self) -> Option<&[HeaderName]> {
Some(&self.forwarded_names)
}
fn sse_keep_alive_secs(&self) -> u64 {
self.sse_keep_alive_secs
}
}
type PathParams<'a> = Vec<(&'a str, Cow<'a, str>)>;
#[derive(Debug, Clone)]
enum RequestBody {
Parsed(request::BodyMapping),
RawRoot,
RawField {
mapping: request::BodyMapping,
field: FieldDescriptor,
http_body: MessageDescriptor,
},
}
#[derive(Debug, Clone)]
enum ResponseShape {
Json(Option<String>),
HttpBody(Vec<FieldDescriptor>),
}
#[derive(Debug, Clone)]
struct RouteEntry {
http_path: String,
http_method: RouteMethod,
grpc_path: axum::http::uri::PathAndQuery,
method: MethodDescriptor,
streaming: bool,
request_body: RequestBody,
response: ResponseShape,
error_details: Option<Arc<StatusDetails>>,
ndjson_envelope: bool,
response_has_required: bool,
denied_headers: Arc<[HeaderName]>,
}
impl RouteEntry {
fn codec(&self) -> codec::DynamicCodec {
codec::DynamicCodec::with_required_check(self.method.output(), self.response_has_required)
}
fn raw_body(&self, message: DynamicMessage) -> Option<httpbody::RawBody> {
match &self.response {
ResponseShape::HttpBody(path) => Some(httpbody::take(message, path)),
ResponseShape::Json(_) => None,
}
}
}
#[derive(Debug, Clone, Default)]
pub struct TranscodeOptions {
pub(crate) error_details: ErrorDetailsPolicy,
pub(crate) ndjson_envelope: bool,
pub(crate) denied_response_headers: Arc<[HeaderName]>,
pub(crate) selection: RpcSelection,
}
impl TranscodeOptions {
pub fn with_selection(mut self, selection: RpcSelection) -> Self {
self.selection = selection;
self
}
pub fn with_error_details(mut self, policy: ErrorDetailsPolicy) -> Self {
self.error_details = policy;
self
}
pub fn with_ndjson_envelope(mut self, enabled: bool) -> Self {
self.ndjson_envelope = enabled;
self
}
pub fn with_denied_response_headers(
mut self,
names: impl IntoIterator<Item = HeaderName>,
) -> Self {
self.denied_response_headers = names.into_iter().collect();
self
}
}
pub fn routes<S: TranscodeState>(pool: &DescriptorPool, aliases: &[AliasConfig]) -> Router<S> {
routes_with_options(pool, aliases, &TranscodeOptions::default())
}
pub fn routes_with_options<S: TranscodeState>(
pool: &DescriptorPool,
aliases: &[AliasConfig],
options: &TranscodeOptions,
) -> Router<S> {
routes_and_choices(pool, aliases, options).0
}
pub(crate) fn routes_and_choices<S: TranscodeState>(
pool: &DescriptorPool,
aliases: &[AliasConfig],
options: &TranscodeOptions,
) -> (Router<S>, Option<Choices>) {
let tables = path_tables(pool, aliases, options);
if tables.is_empty() {
tracing::warn!("No HTTP-annotated RPCs found in proto descriptors");
return (Router::new(), None);
}
let routes = Arc::new(Routes::new(tables));
let choices = Choices {
routes: routes.clone(),
ranked: None,
};
let mut router: Router<S> = Router::new();
for (table, mounted) in routes.tables.iter().enumerate() {
let routes = routes.clone();
let handler =
move |State(state): State<S>, request: Request| dispatch(routes, table, state, request);
let mut route = axum::routing::any(handler.clone());
if mounted.binds_head() {
route = route.head(handler);
}
router = router.route(&mounted.path, route);
}
(router, Some(choices))
}
pub(crate) struct Choices {
routes: Arc<Routes>,
ranked: Option<Arc<table::Ranked>>,
}
impl Choices {
pub(crate) fn among(mut self, others: &[(Option<Method>, String)]) -> Self {
self.ranked = table::Ranked::new(&self.routes, others).map(Arc::new);
self
}
pub(crate) fn with_elsewhere(self, elsewhere: Router) -> Chooser {
Chooser(Arc::new(Choosing {
routes: self.routes,
ranked: self.ranked,
elsewhere,
}))
}
pub(crate) fn direct<S: TranscodeState>(
&self,
state: S,
layered: impl FnOnce(BoxedService) -> BoxedService,
) -> Option<Direct> {
let ranked = self.ranked.clone()?;
let dispatcher = BoxedService::new(Dispatcher {
routes: self.routes.clone(),
state,
});
Some(Direct {
ranking: Arc::new(Ranking {
ranked,
routes: self.routes.clone(),
}),
service: layered(dispatcher),
})
}
}
#[derive(Clone)]
pub(crate) struct Direct {
ranking: Arc<Ranking>,
service: BoxedService,
}
struct Ranking {
ranked: Arc<table::Ranked>,
routes: Arc<Routes>,
}
impl Direct {
pub(crate) fn takes<B>(&self, request: &mut http::Request<B>) -> bool {
let ranking = &*self.ranking;
match ranking.ranked.table(request.method(), request.uri().path()) {
Some(table) => choose(&ranking.routes, Some(&ranking.ranked), table, request),
None => false,
}
}
pub(crate) fn service(&self) -> &BoxedService {
&self.service
}
}
#[derive(Clone)]
struct Dispatcher<S> {
routes: Arc<Routes>,
state: S,
}
impl<S: TranscodeState> tower::Service<Request> for Dispatcher<S> {
type Response = Response;
type Error = std::convert::Infallible;
type Future = std::pin::Pin<
Box<dyn std::future::Future<Output = Result<Response, std::convert::Infallible>> + Send>,
>;
fn poll_ready(
&mut self,
_cx: &mut std::task::Context<'_>,
) -> std::task::Poll<Result<(), Self::Error>> {
std::task::Poll::Ready(Ok(()))
}
fn call(&mut self, request: Request) -> Self::Future {
let routes = self.routes.clone();
let state = self.state.clone();
Box::pin(async move { Ok(serve(routes, state, request).await) })
}
}
fn choose<B>(
routes: &Routes,
ranked: Option<&table::Ranked>,
table: usize,
request: &mut http::Request<B>,
) -> bool {
let (method, path) = (request.method(), request.uri().path());
let choice = match ranked {
Some(ranked) => match routes.choose_ranked(table, method, path, &|other| {
ranked.ranks_first(other, method, path)
}) {
Choice::MethodNotAllowed { table, allow } => Choice::MethodNotAllowed {
table,
allow: ranked.allow(table, allow),
},
choice => choice,
},
None => routes.choose(table, method, path),
};
match choice {
Choice::NotFound => false,
choice => {
request.extensions_mut().insert(Chosen(choice));
true
}
}
}
#[derive(Clone)]
pub(crate) struct Chooser(Arc<Choosing>);
struct Choosing {
routes: Arc<Routes>,
ranked: Option<Arc<table::Ranked>>,
elsewhere: Router,
}
impl Chooser {
pub(crate) fn with_fallback(self, fallback: BoxedService) -> Self {
Self(Arc::new(Choosing {
routes: self.0.routes.clone(),
ranked: self.0.ranked.clone(),
elsewhere: self.0.elsewhere.clone().fallback_service(fallback),
}))
}
pub(crate) fn layer(self, router: Router) -> Router {
router.layer(self)
}
}
impl<S> tower::Layer<S> for Chooser {
type Service = ChooseRouted<S>;
fn layer(&self, route: S) -> ChooseRouted<S> {
ChooseRouted {
chooser: self.clone(),
route,
}
}
}
#[derive(Clone)]
pub(crate) struct ChooseRouted<S> {
chooser: Chooser,
route: S,
}
impl<S> tower::Service<Request> for ChooseRouted<S>
where
S: tower::Service<Request, Response = Response, Error = std::convert::Infallible>,
{
type Response = Response;
type Error = std::convert::Infallible;
type Future = ChooseFuture<S::Future>;
fn poll_ready(
&mut self,
cx: &mut std::task::Context<'_>,
) -> std::task::Poll<Result<(), Self::Error>> {
self.route.poll_ready(cx)
}
fn call(&mut self, mut request: Request) -> Self::Future {
let choosing = &*self.chooser.0;
let extensions = request.extensions();
let matched = extensions
.get::<axum::extract::MatchedPath>()
.and_then(|matched| {
let nested = extensions.get::<axum::extract::NestedPath>();
local_path(matched.as_str(), nested.map(|nested| nested.as_str()))
})
.and_then(|path| choosing.routes.table_at(path));
let ranked = choosing.ranked.as_deref();
if matched.is_none_or(|matched| choose(&choosing.routes, ranked, matched, &mut request)) {
return ChooseFuture::Route {
future: self.route.call(request),
};
}
request
.extensions_mut()
.remove::<axum::extract::MatchedPath>();
ChooseFuture::Elsewhere {
future: tower::ServiceExt::oneshot(choosing.elsewhere.clone(), request),
}
}
}
pin_project_lite::pin_project! {
#[project = ChooseProjection]
pub(crate) enum ChooseFuture<F> {
Route { #[pin] future: F },
Elsewhere { #[pin] future: tower::util::Oneshot<Router, Request> },
}
}
impl<F> std::future::Future for ChooseFuture<F>
where
F: std::future::Future<Output = Result<Response, std::convert::Infallible>>,
{
type Output = Result<Response, std::convert::Infallible>;
fn poll(
self: std::pin::Pin<&mut Self>,
cx: &mut std::task::Context<'_>,
) -> std::task::Poll<Self::Output> {
match self.project() {
ChooseProjection::Route { future } => future.poll(cx),
ChooseProjection::Elsewhere { future } => future.poll(cx),
}
}
}
#[derive(Clone)]
struct Chosen(Choice);
fn local_path<'p>(matched: &'p str, nested: Option<&str>) -> Option<&'p str> {
let Some(nested) = nested else {
return Some(matched);
};
let local = matched.strip_prefix(nested.trim_end_matches('/'))?;
Some(if local.is_empty() { "/" } else { local })
}
fn path_tables(
pool: &DescriptorPool,
aliases: &[AliasConfig],
options: &TranscodeOptions,
) -> Vec<PathTable> {
let bindings = route_bindings(pool, aliases, &options.selection, true);
tracing::info!("Registering {} transcoded REST→gRPC routes", bindings.len());
let mut status_details: [Option<Arc<StatusDetails>>; 2] = [None, None];
let mut tables: Vec<PathTable> = Vec::new();
let mut by_shape: HashMap<String, usize> = HashMap::new();
for mut binding in bindings {
let policy = &options.error_details;
let route = binding.mount.display();
if policy.enabled_for(&route) {
let opaque = policy.opaque_for(&route);
let slot = usize::from(opaque);
if status_details[slot].is_none() {
let base = status_details
.iter()
.flatten()
.next()
.map(|existing| existing.as_ref().clone())
.unwrap_or_else(|| StatusDetails::new(pool));
status_details[slot] = Some(Arc::new(base.with_opaque_details(opaque)));
}
binding.entry.error_details = status_details[slot].clone();
}
binding.entry.ndjson_envelope = options.ndjson_envelope;
binding.entry.denied_headers = options.denied_response_headers.clone();
match by_shape.get(&binding.mount.shape) {
Some(&index) => tables[index].add(binding.mount, binding.entry),
None => {
by_shape.insert(binding.mount.shape.clone(), tables.len());
tables.push(PathTable::new(binding.mount, binding.entry));
}
}
}
retain_mountable(
&mut tables,
|table| table.path.as_str(),
|path, error| {
tracing::error!(
path = %path,
%error,
"transcoded route conflicts with another; skipping its bindings"
);
},
);
tables
}
fn retain_mountable<T>(
items: &mut Vec<T>,
path: impl Fn(&T) -> &str,
mut refused: impl FnMut(&str, matchit::InsertError),
) {
let mut router = matchit::Router::new();
items.retain(|item| match router.insert(path(item), ()) {
Ok(()) => true,
Err(error) => {
refused(path(item), error);
false
}
});
}
pub(crate) fn mounted_shapes(
pool: &DescriptorPool,
aliases: &[AliasConfig],
selection: &RpcSelection,
) -> std::collections::HashSet<String> {
let mut seen = std::collections::HashSet::new();
let mut paths: Vec<MountedPath> = route_bindings(pool, aliases, selection, false)
.into_iter()
.filter_map(|binding| {
seen.insert(binding.mount.shape.clone())
.then_some(binding.mount)
})
.collect();
retain_mountable(&mut paths, |mount| mount.axum.as_str(), |_, _| {});
paths.into_iter().map(|mount| mount.shape).collect()
}
async fn dispatch<S: TranscodeState>(
routes: Arc<Routes>,
matched: usize,
state: S,
request: Request,
) -> Response {
let (mut parts, body) = request.into_parts();
let choice = match parts.extensions.remove::<Chosen>() {
Some(Chosen(choice)) => choice,
None => routes.choose(matched, &parts.method, parts.uri.path()),
};
let (table, index) = match choice {
Choice::Route { table, index } => (table, index),
Choice::MethodNotAllowed { allow, .. } => {
return (StatusCode::METHOD_NOT_ALLOWED, [(ALLOW, allow)]).into_response()
}
Choice::NotFound => return StatusCode::NOT_FOUND.into_response(),
};
let captured = match RawPathParams::from_request_parts(&mut parts, &state).await {
Ok(captured) => captured,
Err(rejection) => return rejection.into_response(),
};
let mut path_params: PathParams<'_> = captured
.iter()
.map(|(name, value)| (name, Cow::Borrowed(value)))
.collect();
let multi_segment = routes.tables[table].multi_segment(index);
let local;
if table != matched || !multi_segment.is_empty() {
local = parts.uri.clone();
let prefixes = path_params
.len()
.checked_sub(routes.tables[matched].captures.len())
.expect("the router captures every variable of the path it matched");
path_params.truncate(prefixes);
if let Err(rejection) = capture(
&routes,
table,
multi_segment,
local.path(),
&mut path_params,
) {
return rejection.into_response(routes.tables[table].entry(index));
}
}
let uri = match parts.extensions.remove::<OriginalUri>() {
Some(OriginalUri(uri)) => uri,
None => parts.uri.clone(),
};
let begun = begin(
&routes,
(table, index),
&state,
(parts, body),
path_params,
uri,
)
.await;
serve_begun(begun, routes, table, index).await
}
async fn serve<S: TranscodeState>(routes: Arc<Routes>, state: S, request: Request) -> Response {
let (mut parts, body) = request.into_parts();
let head = parts.method == Method::HEAD;
let connect = parts.method == Method::CONNECT;
let choice = match parts.extensions.remove::<Chosen>() {
Some(Chosen(choice)) => choice,
None => Choice::NotFound,
};
let mut response = match choice {
Choice::Route { table, index } => {
let local = parts.uri.clone();
let uri = match parts.extensions.remove::<OriginalUri>() {
Some(OriginalUri(uri)) => uri,
None => local.clone(),
};
let multi_segment = routes.tables[table].multi_segment(index);
let mut path_params = Vec::with_capacity(routes.tables[table].captures.len());
match capture(
&routes,
table,
multi_segment,
local.path(),
&mut path_params,
) {
Ok(()) => {
let begun = begin(
&routes,
(table, index),
&state,
(parts, body),
path_params,
uri,
)
.await;
serve_begun(begun, routes, table, index).await
}
Err(rejection) => rejection.into_response(routes.tables[table].entry(index)),
}
}
Choice::MethodNotAllowed { allow, .. } => {
(StatusCode::METHOD_NOT_ALLOWED, [(ALLOW, allow)]).into_response()
}
Choice::NotFound => StatusCode::NOT_FOUND.into_response(),
};
let status = response.status();
if connect && status.is_success() {
response.headers_mut().remove(http::header::CONTENT_LENGTH);
*response.body_mut() = axum::body::Body::empty();
return response;
}
if status != StatusCode::NO_CONTENT
&& status != StatusCode::NOT_MODIFIED
&& !response
.headers()
.contains_key(http::header::CONTENT_LENGTH)
{
if let Some(length) = http_body::Body::size_hint(response.body()).exact() {
response
.headers_mut()
.insert(http::header::CONTENT_LENGTH, HeaderValue::from(length));
}
}
if head {
*response.body_mut() = axum::body::Body::empty();
}
response
}
enum Begun<U> {
Call {
call: Call<U>,
sse: bool,
keep_alive_secs: u64,
},
Answered(Response),
}
async fn begin<S: TranscodeState>(
routes: &Routes,
(table, index): (usize, usize),
state: &S,
(mut parts, body): (http::request::Parts, axum::body::Body),
mut path_params: PathParams<'_>,
uri: Uri,
) -> Begun<S::Upstream> {
routes.tables[table].bind_params(index, &mut path_params);
let client = Client {
headers: std::mem::take(&mut parts.headers),
origin: Origin {
connection: parts.extensions.remove::<ConnectionInfo>(),
address: parts.extensions.remove::<ClientAddress>(),
},
line: RequestLine {
method: std::mem::replace(&mut parts.method, Method::GET),
uri,
},
};
let body = match Bytes::from_request(Request::from_parts(parts, body), state).await {
Ok(body) => body,
Err(rejection) => return Begun::Answered(rejection.into_response()),
};
let entry = routes.tables[table].entry(index);
match start(state.clone(), client, &path_params, body, entry) {
Ok((call, sse, keep_alive_secs)) => Begun::Call {
call,
sse,
keep_alive_secs,
},
Err(rejection) => Begun::Answered(rejection.into_response(entry)),
}
}
async fn serve_begun<U: crate::upstream::Upstream>(
begun: Begun<U>,
routes: Arc<Routes>,
table: usize,
index: usize,
) -> Response {
let (call, sse, keep_alive_secs) = match begun {
Begun::Call {
call,
sse,
keep_alive_secs,
} => (call, sse, keep_alive_secs),
Begun::Answered(response) => return response,
};
let entry = RouteRef {
routes,
table,
index,
};
if entry.streaming {
streaming_call(call, entry, sse, keep_alive_secs).await
} else {
unary_call(call, &entry).await
}
}
fn capture<'p>(
routes: &'p Routes,
table: usize,
multi_segment: &[usize],
path: &'p str,
params: &mut PathParams<'p>,
) -> Result<(), Unmappable> {
let found = routes
.params_of(table, path)
.expect("the chosen table was found by matching this path");
for (position, (name, raw)) in found.iter().enumerate() {
let decoded: Cow<'p, [u8]> = if multi_segment.contains(&position) {
path::decode_multi_segment(raw)
} else {
percent_encoding::percent_decode_str(raw).into()
};
let not_utf8 = || Unmappable(format!("path parameter {name} is not valid UTF-8"));
let value = match decoded {
Cow::Borrowed(bytes) => {
Cow::Borrowed(std::str::from_utf8(bytes).map_err(|_| not_utf8())?)
}
Cow::Owned(bytes) => Cow::Owned(String::from_utf8(bytes).map_err(|_| not_utf8())?),
};
params.push((name, value));
}
Ok(())
}
#[derive(Clone)]
struct RouteRef {
routes: Arc<Routes>,
table: usize,
index: usize,
}
impl std::ops::Deref for RouteRef {
type Target = RouteEntry;
fn deref(&self) -> &RouteEntry {
self.routes.tables[self.table].entry(self.index)
}
}
struct RouteBinding {
entry: RouteEntry,
mount: MountedPath,
}
impl RouteBinding {
fn claim(&self) -> table::Claim<'_> {
table::Claim {
method: &self.entry.http_method,
verb: self.mount.verb.as_deref(),
template: self.mount.last_template.as_deref(),
literals: &self.mount.literals,
}
}
}
fn route_bindings(
pool: &DescriptorPool,
aliases: &[AliasConfig],
selection: &RpcSelection,
report: bool,
) -> Vec<RouteBinding> {
let mut bindings = Vec::new();
let mut push = |mount: MountedPath, entry: RouteEntry| match mount.routable() {
Ok(()) => bindings.push(RouteBinding { entry, mount }),
Err(_) if !report => {}
Err(error) => tracing::error!(
path = %mount.display(),
rpc = %entry.grpc_path,
%error,
"google.api.http path template cannot be routed; skipping this binding"
),
};
for entry in extract_routes(pool, selection) {
for alias in aliases {
if let Some(suffix) = entry.http_path.strip_prefix(&alias.to) {
if alias.from.ends_with("/{path}") {
let prefix = alias.from.trim_end_matches("/{path}");
push(
MountedPath::new(&format!("{prefix}{suffix}")),
entry.clone(),
);
}
}
}
push(MountedPath::new(&entry.http_path), entry);
}
bindings
}
pub fn route_paths(
pool: &DescriptorPool,
aliases: &[AliasConfig],
selection: &RpcSelection,
) -> Vec<(String, String)> {
let mut paths = Vec::new();
for bindings in &shape_bindings(pool, aliases, selection) {
let path = bindings[0].mount.axum.as_str();
let listed = |method: &str| (method.to_owned(), path.to_owned());
if bindings
.iter()
.any(|b| b.entry.http_method == RouteMethod::Any || b.mount.verb.is_some())
{
paths.push(listed("*"));
} else {
let mut methods: Vec<&str> = Vec::new();
for binding in bindings {
let method = binding.entry.http_method.as_str();
if !methods.contains(&method) {
methods.push(method);
paths.push(listed(method));
}
}
}
for (at, binding) in bindings.iter().enumerate() {
let clashes = bindings[..at]
.iter()
.any(|earlier| table::clash(&earlier.claim(), &binding.claim()));
if clashes {
paths.push(listed(binding.entry.http_method.as_str()));
}
}
}
paths
}
fn shape_bindings(
pool: &DescriptorPool,
aliases: &[AliasConfig],
selection: &RpcSelection,
) -> Vec<Vec<RouteBinding>> {
let mut shapes: Vec<Vec<RouteBinding>> = Vec::new();
let mut by_shape: HashMap<String, usize> = HashMap::new();
for binding in route_bindings(pool, aliases, selection, false) {
match by_shape.get(&binding.mount.shape) {
Some(&index) => shapes[index].push(binding),
None => {
by_shape.insert(binding.mount.shape.clone(), shapes.len());
shapes.push(vec![binding]);
}
}
}
retain_mountable(
&mut shapes,
|bindings| bindings[0].mount.axum.as_str(),
|_, _| {},
);
shapes
}
pub(crate) struct BoundMethods {
pub(crate) methods: Vec<Method>,
pub(crate) every: bool,
}
pub(crate) fn bound_methods(
pool: &DescriptorPool,
aliases: &[AliasConfig],
selection: &RpcSelection,
) -> BoundMethods {
let mut bound = BoundMethods {
methods: Vec::new(),
every: false,
};
for binding in shape_bindings(pool, aliases, selection)
.into_iter()
.flatten()
{
match binding.entry.http_method {
RouteMethod::One(method) if !bound.methods.contains(&method) => {
bound.methods.push(method);
}
RouteMethod::One(_) => {}
RouteMethod::Any => bound.every = true,
}
}
bound
}
fn response_serialize_options() -> SerializeOptions {
SerializeOptions::new()
.skip_default_fields(false)
.stringify_64_bit_integers(true)
}
fn message_to_json_bytes(
msg: &DynamicMessage,
opts: &SerializeOptions,
) -> Result<Vec<u8>, serde_json::Error> {
let mut buf = Vec::with_capacity(128);
msg.serialize_with_options(&mut serde_json::Serializer::new(&mut buf), opts)?;
Ok(buf)
}
fn message_to_json_string(msg: &DynamicMessage, opts: &SerializeOptions) -> Result<String, String> {
let buf = message_to_json_bytes(msg, opts).map_err(|e| e.to_string())?;
Ok(unsafe { String::from_utf8_unchecked(buf) })
}
pub(crate) fn json_body(
msg: &DynamicMessage,
response_body: Option<&str>,
) -> Result<Vec<u8>, serde_json::Error> {
let opts = response_serialize_options();
let Some(path) = response_body else {
return message_to_json_bytes(msg, &opts);
};
let mut value = Some(msg.serialize_with_options(serde_json::value::Serializer, &opts)?);
let mut desc = Some(prost_reflect::ReflectMessage::descriptor(msg));
for segment in path.split('.') {
let field = desc.as_ref().and_then(|d| d.get_field_by_name(segment));
desc = match field.as_ref().map(FieldDescriptor::kind) {
Some(prost_reflect::Kind::Message(inner)) => Some(inner),
_ => None,
};
value = match (value, field) {
(Some(serde_json::Value::Object(mut fields)), Some(field)) => {
fields.remove(field.json_name())
}
_ => None,
};
}
let value = value.unwrap_or_else(|| {
tracing::warn!(
response_body = %path,
"configured response_body path not found in response; returning null"
);
serde_json::Value::Null
});
serde_json::to_vec(&value)
}
fn wants_sse(headers: &HeaderMap) -> bool {
headers
.get_all(axum::http::header::ACCEPT)
.iter()
.filter_map(|v| v.to_str().ok())
.flat_map(|accept| accept.split(','))
.any(accept_range_selects_sse)
}
fn accept_range_selects_sse(range: &str) -> bool {
let mut parts = range.split(';');
let media = parts.next().unwrap_or("").trim();
if !media.eq_ignore_ascii_case("text/event-stream") {
return false;
}
for param in parts {
let mut kv = param.splitn(2, '=');
if kv.next().unwrap_or("").trim().eq_ignore_ascii_case("q") {
let q: f32 = kv.next().unwrap_or("").trim().parse().unwrap_or(1.0);
return q > 0.0;
}
}
true
}
struct Client {
headers: HeaderMap,
origin: Origin,
line: RequestLine,
}
struct RequestLine {
method: Method,
uri: Uri,
}
impl RequestLine {
fn received(self, entry: &RouteEntry) -> Result<ReceivedRequest, Unmappable> {
let path_and_query = self
.uri
.into_parts()
.path_and_query
.ok_or_else(|| Unmappable("the request target has no path".to_string()))?;
Ok(ReceivedRequest::new(
self.method,
path_and_query,
entry.grpc_path.clone(),
))
}
}
struct Origin {
connection: Option<ConnectionInfo>,
address: Option<ClientAddress>,
}
fn start<S: TranscodeState>(
proxy_state: S,
client: Client,
path_params: &[(&str, Cow<'_, str>)],
body: Bytes,
entry: &RouteEntry,
) -> Result<(Call<S::Upstream>, bool, u64), Unmappable> {
let keep_alive_secs = proxy_state.sse_keep_alive_secs();
let Client {
headers,
origin,
line,
} = client;
let prepared = prepare(
proxy_state,
&headers,
origin,
path_params,
line.uri.query(),
body,
entry,
)
.and_then(|mut call| {
call.request.extensions_mut().insert(line.received(entry)?);
Ok(call)
});
let call = prepared?;
Ok((
call,
entry.streaming && wants_sse(&headers),
keep_alive_secs,
))
}
struct Unmappable(String);
impl Unmappable {
fn into_response(self, entry: &RouteEntry) -> Response {
let status = tonic::Status::invalid_argument(self.0);
error::status_to_response_with_details(&status, entry.error_details.as_deref())
}
}
struct Call<U> {
upstream: U,
request: tonic::Request<DynamicMessage>,
deadline: Duration,
}
impl<U: Upstream> Call<U> {
async fn open(
self,
entry: &RouteEntry,
) -> Result<tonic::Response<tonic::Streaming<DynamicMessage>>, tonic::Status> {
let Self {
upstream,
request,
deadline,
} = self;
let call = async move {
let mut client = Grpc::new(upstream);
if let Err(e) = client.ready().await {
let e: crate::upstream::BoxError = e.into();
return Err(tonic::Status::unavailable(format!(
"gRPC upstream not ready: {e}"
)));
}
client
.server_streaming(request, entry.grpc_path.clone(), entry.codec())
.await
};
match tokio::time::timeout(deadline, call).await {
Ok(result) => result,
Err(_) => Err(tonic::Status::deadline_exceeded(
"upstream did not answer within the deadline",
)),
}
}
}
fn prepare<S: TranscodeState>(
proxy_state: S,
headers: &HeaderMap,
origin: Origin,
path_params: &[(&str, Cow<'_, str>)],
raw_query: Option<&str>,
body: Bytes,
entry: &RouteEntry,
) -> Result<Call<S::Upstream>, Unmappable> {
let Origin {
connection,
address,
} = origin;
let forwarded = match proxy_state.forwarded_header_names() {
Some(names) => metadata::Forwarded::Named(names),
None => metadata::Forwarded::Listed(proxy_state.forwarded_headers()),
};
let request_metadata = metadata::forwarded_metadata(headers, forwarded, address.is_some())
.map_err(|e| Unmappable(e.to_string()))?;
let message =
decode_request(entry, headers, path_params, raw_query, body).map_err(Unmappable)?;
let mut request = tonic::Request::new(message);
*request.metadata_mut() = request_metadata;
if let Some(connection) = connection {
connection.into_tonic_extensions(request.extensions_mut());
}
if let Some(address) = address {
request.extensions_mut().insert(address);
}
let deadline = metadata::apply_request_deadline(&mut request, headers)
.map_or(UPSTREAM_DEADLINE, |client| client.min(UPSTREAM_DEADLINE));
Ok(Call {
upstream: proxy_state.into_upstream(),
request,
deadline,
})
}
struct UnaryAnswer {
initial: MetadataMap,
message: DynamicMessage,
trailers: Option<MetadataMap>,
}
async fn call_unary<U: Upstream>(
call: Call<U>,
entry: &RouteEntry,
) -> Result<UnaryAnswer, tonic::Status> {
let (initial, mut stream, _) = call.open(entry).await?.into_parts();
let message = match stream.message().await {
Ok(Some(message)) => message,
Ok(None) => {
let status = tonic::Status::internal("Missing response message.");
return Err(with_initial(status, initial));
}
Err(status) => return Err(with_initial(status, initial)),
};
let trailers = match stream.trailers().await {
Ok(trailers) => trailers,
Err(status) => return Err(with_initial(status, initial)),
};
Ok(UnaryAnswer {
initial,
message,
trailers,
})
}
fn with_initial(mut status: tonic::Status, initial: MetadataMap) -> tonic::Status {
let own = std::mem::take(status.metadata_mut()).into_headers();
let mut merged = initial.into_headers();
let mut last: Option<HeaderName> = None;
for (name, value) in own {
if let Some(name) = name {
merged.append(&name, value);
last = Some(name);
} else if let Some(name) = &last {
merged.append(name, value);
}
}
*status.metadata_mut() = MetadataMap::from_headers(merged);
status
}
async fn unary_call<U: Upstream>(call: Call<U>, entry: &RouteEntry) -> Response {
match call_unary(call, entry).await {
Ok(answer) => unary_success(entry, answer),
Err(status) => upstream_error(status, entry),
}
}
fn unary_success(entry: &RouteEntry, answer: UnaryAnswer) -> Response {
let mut upstream = UpstreamHeaders::default();
upstream.absorb(answer.initial, &entry.denied_headers);
if let Some(trailers) = answer.trailers {
upstream.absorb(trailers, &entry.denied_headers);
}
let status = match upstream.status() {
Ok(status) => status.unwrap_or(StatusCode::OK),
Err(response::InvalidHttpCode) => {
tracing::error!(
rpc = %entry.grpc_path,
"upstream set x-http-code to something other than one integer in 200-599"
);
return error::malformed_response(entry.error_details.as_deref());
}
};
let (content_type, body) = match &entry.response {
ResponseShape::HttpBody(path) => {
let raw = httpbody::take(answer.message, path);
match content_type_header(&raw.content_type) {
Ok(content_type) => (content_type, Body::from(raw.data)),
Err(()) => return invalid_content_type(entry),
}
}
ResponseShape::Json(response_body) => {
match json_body(&answer.message, response_body.as_deref()) {
Ok(json) => (
Some(HeaderValue::from_static("application/json")),
Body::from(json),
),
Err(e) => {
tracing::error!("Failed to serialize gRPC response: {e}");
return error::status_to_response_with_details(
&tonic::Status::internal("failed to serialize response"),
entry.error_details.as_deref(),
);
}
}
}
};
response::build(status, upstream.into_headers(), content_type, body)
}
fn content_type_header(content_type: &str) -> Result<Option<HeaderValue>, ()> {
if content_type.is_empty() {
return Ok(None);
}
HeaderValue::from_str(content_type)
.map(Some)
.map_err(|_| ())
}
fn invalid_content_type(entry: &RouteEntry) -> Response {
tracing::error!(
rpc = %entry.grpc_path,
"upstream HttpBody content_type is not a valid header value"
);
error::malformed_response(entry.error_details.as_deref())
}
fn upstream_error(mut status: tonic::Status, entry: &RouteEntry) -> Response {
let (response, faithful) = error::render_response(&status, entry.error_details.as_deref());
let metadata = std::mem::take(status.metadata_mut());
if !faithful || metadata.is_empty() {
return response;
}
let mut upstream = UpstreamHeaders::default();
upstream.absorb(metadata, &entry.denied_headers);
response::with_upstream_headers(response, upstream.into_headers())
}
async fn streaming_call<U: Upstream>(
call: Call<U>,
entry: RouteRef,
use_sse: bool,
keep_alive_secs: u64,
) -> Response {
let response = match call.open(&entry).await {
Ok(response) => response,
Err(status) => return upstream_error(status, &entry),
};
let (initial, stream, _) = response.into_parts();
if matches!(entry.response, ResponseShape::HttpBody(_)) {
return http_body_stream(stream, entry, initial).await;
}
let mut upstream = UpstreamHeaders::default();
upstream.absorb(initial, &entry.denied_headers);
let headers = upstream.into_headers();
let envelope = entry.ndjson_envelope;
let render_error =
move |status: &tonic::Status| error::error_body(status, entry.error_details.as_deref());
let response = if use_sse {
sse_response(stream, render_error, keep_alive_secs)
} else {
ndjson_response(stream, render_error, envelope)
};
response::with_upstream_headers(response, headers)
}
async fn http_body_stream(
mut stream: tonic::Streaming<DynamicMessage>,
entry: RouteRef,
initial: MetadataMap,
) -> Response {
let first = match stream.message().await {
Ok(Some(message)) => entry.raw_body(message).unwrap_or_default(),
Ok(None) => httpbody::RawBody::default(),
Err(status) => return upstream_error(with_initial(status, initial), &entry),
};
let mut upstream = UpstreamHeaders::default();
upstream.absorb(initial, &entry.denied_headers);
let headers = upstream.into_headers();
let content_type = match content_type_header(&first.content_type) {
Ok(content_type) => content_type,
Err(()) => return invalid_content_type(&entry),
};
let rest = stream.map(move |item| match item {
Ok(message) => Ok(entry.raw_body(message).unwrap_or_default().data),
Err(status) => {
tracing::error!(
rpc = %entry.grpc_path,
code = ?status.code(),
"server-streaming HttpBody failed after the response started; aborting the body"
);
Err(std::io::Error::other(status))
}
});
let chunks = futures::stream::once(futures::future::ready(Ok(first.data)))
.chain(rest)
.try_filter(|chunk| futures::future::ready(!chunk.is_empty()));
response::build(
StatusCode::OK,
headers,
content_type,
Body::from_stream(chunks),
)
}
enum StreamFrame {
Data(String),
Error(serde_json::Value),
}
const STATUS_TYPE_URL: &str = "type.googleapis.com/google.rpc.Status";
fn json_frames<St, R>(
stream: St,
render_error: R,
) -> impl futures::Stream<Item = StreamFrame> + Send + 'static
where
St: futures::Stream<Item = Result<DynamicMessage, tonic::Status>> + Send + Unpin + 'static,
R: Fn(&tonic::Status) -> serde_json::Value + Send + 'static,
{
let state = Some((stream, render_error, response_serialize_options()));
futures::stream::unfold(state, |state| async move {
let (mut stream, render_error, opts) = state?;
let error = match stream.next().await? {
Ok(msg) => match message_to_json_string(&msg, &opts) {
Ok(s) => return Some((StreamFrame::Data(s), Some((stream, render_error, opts)))),
Err(e) => tonic::Status::internal(format!("serialization error: {e}")),
},
Err(status) => status,
};
Some((StreamFrame::Error(render_error(&error)), None))
})
}
fn ndjson_response<St, R>(stream: St, render_error: R, envelope: bool) -> Response
where
St: futures::Stream<Item = Result<DynamicMessage, tonic::Status>> + Send + Unpin + 'static,
R: Fn(&tonic::Status) -> serde_json::Value + Send + 'static,
{
let byte_stream = json_frames(stream, render_error).map(move |frame| {
let mut line = match frame {
StreamFrame::Data(s) if envelope => {
let mut wrapped = String::with_capacity(s.len() + 12);
wrapped.push_str("{\"result\":");
wrapped.push_str(&s);
wrapped.push('}');
wrapped
}
StreamFrame::Data(s) => s,
StreamFrame::Error(body) if envelope => {
serde_json::json!({ "error": body }).to_string()
}
StreamFrame::Error(mut body) => {
if let Some(fields) = body.as_object_mut() {
fields.insert("@type".into(), STATUS_TYPE_URL.into());
}
body.to_string()
}
};
line.push('\n');
Ok::<Bytes, std::io::Error>(Bytes::from(line))
});
let body = Body::from_stream(byte_stream);
Response::builder()
.status(StatusCode::OK)
.header(CONTENT_TYPE, "application/x-ndjson")
.body(body)
.unwrap_or_else(|_| StatusCode::INTERNAL_SERVER_ERROR.into_response())
}
fn sse_response<St, R>(stream: St, render_error: R, keep_alive_secs: u64) -> Response
where
St: futures::Stream<Item = Result<DynamicMessage, tonic::Status>> + Send + Unpin + 'static,
R: Fn(&tonic::Status) -> serde_json::Value + Send + 'static,
{
let event_stream = json_frames(stream, render_error).map(|frame| {
let event = match frame {
StreamFrame::Data(s) => Event::default().data(s),
StreamFrame::Error(body) => Event::default()
.event("stream-error")
.data(body.to_string()),
};
Ok::<Event, std::convert::Infallible>(event)
});
Sse::new(event_stream)
.keep_alive(KeepAlive::new().interval(std::time::Duration::from_secs(keep_alive_secs)))
.into_response()
}
static NO_PARSED_BODY: request::BodyMapping = request::BodyMapping::None;
fn decode_request(
entry: &RouteEntry,
headers: &HeaderMap,
path_params: &[(&str, Cow<'_, str>)],
raw_query: Option<&str>,
body_bytes: Bytes,
) -> Result<DynamicMessage, String> {
let (mapping, body, raw_query) = match &entry.request_body {
RequestBody::Parsed(mapping @ request::BodyMapping::None) => {
(mapping, request::Body::Absent, raw_query)
}
RequestBody::Parsed(mapping) => (
mapping,
request::Body::new(body::content_type(headers), &body_bytes),
raw_query,
),
RequestBody::RawRoot => (&NO_PARSED_BODY, request::Body::Absent, None),
RequestBody::RawField { mapping, .. } => (mapping, request::Body::Absent, raw_query),
};
let mut message =
request::build_message(&entry.method.input(), mapping, body, path_params, raw_query)?;
match &entry.request_body {
RequestBody::Parsed(_) => {}
RequestBody::RawRoot => {
httpbody::fill(&mut message, request_content_type(headers)?, body_bytes);
}
RequestBody::RawField {
field, http_body, ..
} => {
let mut inner = DynamicMessage::new(http_body.clone());
httpbody::fill(&mut inner, request_content_type(headers)?, body_bytes);
message.set_field(field, prost_reflect::Value::Message(inner));
}
}
Ok(message)
}
fn request_content_type(headers: &HeaderMap) -> Result<String, String> {
match headers.get(CONTENT_TYPE) {
None => Ok(String::new()),
Some(value) => value
.to_str()
.map(str::to_owned)
.map_err(|_| "request Content-Type is not a visible ASCII string".to_string()),
}
}
fn extract_routes(pool: &DescriptorPool, selection: &RpcSelection) -> Vec<RouteEntry> {
let http_ext = match pool.get_extension_by_name("google.api.http") {
Some(ext) => ext,
None => {
tracing::warn!("google.api.http extension not found in descriptor pool");
return Vec::new();
}
};
let mut entries = Vec::new();
for service in pool.services() {
for method in service.methods() {
if method.is_client_streaming() || !selection.selects(&method) {
continue;
}
let grpc_path = format!("/{}/{}", service.full_name(), method.name());
let grpc_path: axum::http::uri::PathAndQuery = match grpc_path.parse() {
Ok(p) => p,
Err(e) => {
tracing::error!("skipping route with invalid gRPC path '{grpc_path}': {e}");
continue;
}
};
let input = method.input();
let output = method.output();
let streaming = method.is_server_streaming();
let response_has_required = codec::has_required_fields(&output);
for binding in rule::http_bindings(&method, &http_ext) {
if streaming {
tracing::info!(
"Registering streaming route: {} {} → {}",
binding.method.as_str(),
binding.path,
grpc_path
);
}
entries.push(RouteEntry {
http_path: binding.path,
http_method: binding.method,
grpc_path: grpc_path.clone(),
method: method.clone(),
streaming,
request_body: request_body(&input, binding.body),
response: response_shape(&output, binding.response_body),
error_details: None,
ndjson_envelope: false,
response_has_required,
denied_headers: Arc::default(),
});
}
}
}
entries
}
fn request_body(input: &MessageDescriptor, mapping: request::BodyMapping) -> RequestBody {
let raw_field = match &mapping {
request::BodyMapping::Root if httpbody::is_http_body(input) => return RequestBody::RawRoot,
request::BodyMapping::Field(name) => httpbody::http_body_field(input, name),
_ => None,
};
match raw_field {
Some((field, http_body)) => RequestBody::RawField {
mapping,
field,
http_body,
},
None => RequestBody::Parsed(mapping),
}
}
fn response_shape(output: &MessageDescriptor, response_body: Option<String>) -> ResponseShape {
match response_body {
None if httpbody::is_http_body(output) => ResponseShape::HttpBody(Vec::new()),
None => ResponseShape::Json(None),
Some(path) => match httpbody::http_body_path(output, &path) {
Some(fields) => ResponseShape::HttpBody(fields),
None => ResponseShape::Json(Some(path)),
},
}
}
#[cfg(test)]
mod tests;