use std::sync::Arc;
use std::time::{Duration, Instant};
use bytes::Bytes;
use foundation_core::url::Uri;
use foundation_core::valtron;
use foundation_errstacks::ErrorTrace;
use foundation_netio::shared::http::{
Proto, RequestDescriptor, SimpleHeader, SimpleHeaders, SimpleMethod, SimpleUrl, Status,
};
use futures::{Stream, StreamExt};
use crate::shared::codec::{CodecFor, ProcedureCodecs};
use crate::shared::compression::{CompressionRegistry, Compressor};
use crate::shared::context::{CancelSignal, Ctx, IdempotencyLevel, Peer, RequestContext, Spec, StreamType};
use crate::shared::envelope::encode_grpc_timeout;
use crate::shared::error::{Code, ConnectError, ConnectResult};
use crate::shared::interceptor::{Interceptor, InterceptorChain, UnaryCall, UnaryFunc, UnaryReply};
use crate::shared::message::{Request, Response};
use crate::shared::protocol::connect::constants as connect_consts;
use crate::shared::protocol::grpc_web::constants as grpc_web_consts;
use crate::shared::protocol::grpc::parse_grpc_error_trailer;
use crate::shared::protocol::{ClientExchange, ClientExchangeConfig, ProtocolClient};
use crate::ConnectClient;
use crate::GrpcClient;
use crate::GrpcWebClient;
use crate::shared::transport::{
capabilities::{check_compatible, requirements, ProtocolKind},
conn::{ClientReceiver, ClientSender},
frame::BoxFuture,
Transport, TransportStream, DEFAULT_PIPE_DEPTH,
};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ProtocolSelection {
Connect,
Grpc,
GrpcWeb,
}
impl ProtocolSelection {
#[must_use]
pub fn to_kind(self) -> ProtocolKind {
match self {
ProtocolSelection::Connect => ProtocolKind::Connect,
ProtocolSelection::Grpc => ProtocolKind::Grpc,
ProtocolSelection::GrpcWeb => ProtocolKind::GrpcWeb,
}
}
fn peer_protocol_name(&self) -> &'static str {
match self {
ProtocolSelection::Connect => "connect",
ProtocolSelection::Grpc => "grpc",
ProtocolSelection::GrpcWeb => "grpc-web",
}
}
}
#[derive(Debug, Clone)]
pub struct SizeLimits {
pub read_max_bytes: usize,
pub send_max_bytes: usize,
pub compress_min_bytes: usize,
}
impl Default for SizeLimits {
fn default() -> Self {
Self {
read_max_bytes: 4 * 1024 * 1024,
send_max_bytes: 0,
compress_min_bytes: 0,
}
}
}
#[derive(Clone)]
pub struct ClientConfig {
pub url: String,
pub protocol: ProtocolSelection,
pub codec_name: String,
pub compression: CompressionRegistry,
pub send_compression: Option<String>,
pub limits: SizeLimits,
pub interceptors: Vec<Arc<dyn Interceptor>>,
pub default_timeout: Option<Duration>,
pub get_url_max_bytes: usize,
pub get_use_fallback: bool,
pub preferred_http_version: Option<Proto>,
pub pipe_depth: usize,
pub extra_headers: SimpleHeaders,
}
impl Default for ClientConfig {
fn default() -> Self {
Self {
url: String::new(),
protocol: ProtocolSelection::Connect,
codec_name: "proto".to_string(),
compression: CompressionRegistry::new(),
send_compression: None,
limits: SizeLimits::default(),
interceptors: Vec::new(),
default_timeout: None,
get_url_max_bytes: 8192,
get_use_fallback: true,
preferred_http_version: None,
pipe_depth: DEFAULT_PIPE_DEPTH,
extra_headers: SimpleHeaders::new(),
}
}
}
impl core::fmt::Debug for ClientConfig {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.debug_struct("ClientConfig")
.field("url", &self.url)
.field("protocol", &self.protocol)
.field("codec_name", &self.codec_name)
.field("send_compression", &self.send_compression)
.field("limits", &self.limits)
.field("interceptors", &self.interceptors.len())
.field("default_timeout", &self.default_timeout)
.field("get_url_max_bytes", &self.get_url_max_bytes)
.field("get_use_fallback", &self.get_use_fallback)
.field("preferred_http_version", &self.preferred_http_version)
.field("pipe_depth", &self.pipe_depth)
.finish_non_exhaustive()
}
}
#[derive(Clone)]
pub struct ClientOptions {
config: ClientConfig,
idempotency: IdempotencyLevel,
enable_http_get: bool,
}
impl core::fmt::Debug for ClientOptions {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.debug_struct("ClientOptions")
.field("config", &self.config)
.field("idempotency", &self.idempotency)
.field("enable_http_get", &self.enable_http_get)
.finish()
}
}
impl ClientOptions {
#[must_use]
pub fn new() -> Self {
Self {
config: ClientConfig::default(),
idempotency: IdempotencyLevel::Unknown,
enable_http_get: false,
}
}
#[must_use]
pub fn with_header(mut self, key: impl Into<SimpleHeader>, value: impl Into<String>) -> Self {
self.config
.extra_headers
.entry(key.into())
.or_default()
.push(value.into());
self
}
#[must_use]
pub fn with_connect(mut self) -> Self {
self.config.protocol = ProtocolSelection::Connect;
self
}
#[must_use]
pub fn with_grpc(mut self) -> Self {
self.config.protocol = ProtocolSelection::Grpc;
self
}
#[must_use]
pub fn with_grpc_web(mut self) -> Self {
self.config.protocol = ProtocolSelection::GrpcWeb;
self
}
#[must_use]
pub fn with_codec(mut self, name: &str) -> Self {
self.config.codec_name = name.to_string();
self
}
#[must_use]
pub fn with_send_gzip(self) -> Self {
self.with_send_compression("gzip")
}
#[must_use]
pub fn with_send_compression(mut self, name: &str) -> Self {
self.config.send_compression = Some(name.to_string());
self
}
#[must_use]
pub fn with_accept_compression(mut self, _name: &str, compressor: Arc<dyn Compressor>) -> Self {
self.config.compression.register(compressor);
self
}
#[must_use]
pub fn with_read_max_bytes(mut self, n: usize) -> Self {
self.config.limits.read_max_bytes = n;
self
}
#[must_use]
pub fn with_send_max_bytes(mut self, n: usize) -> Self {
self.config.limits.send_max_bytes = n;
self
}
#[must_use]
pub fn with_compress_min_bytes(mut self, n: usize) -> Self {
self.config.limits.compress_min_bytes = n;
self
}
#[must_use]
pub fn with_interceptor(mut self, interceptor: Arc<dyn Interceptor>) -> Self {
self.config.interceptors.push(interceptor);
self
}
#[must_use]
pub fn with_timeout(mut self, timeout: Duration) -> Self {
self.config.default_timeout = Some(timeout);
self
}
#[must_use]
pub fn with_idempotency(mut self, level: IdempotencyLevel) -> Self {
self.idempotency = level;
self
}
#[must_use]
pub fn with_http_get(mut self) -> Self {
self.enable_http_get = true;
self
}
#[must_use]
pub fn with_get_url_max_bytes(mut self, n: usize) -> Self {
self.config.get_url_max_bytes = n;
self
}
#[must_use]
pub fn with_get_fallback(mut self, enabled: bool) -> Self {
self.config.get_use_fallback = enabled;
self
}
#[must_use]
pub fn with_preferred_http_version(mut self, v: Proto) -> Self {
self.config.preferred_http_version = Some(v);
self
}
#[must_use]
pub fn with_pipe_depth(mut self, n: usize) -> Self {
self.config.pipe_depth = n;
self
}
}
impl Default for ClientOptions {
fn default() -> Self {
Self::new()
}
}
pub struct Client<Req, Res> {
transport: Arc<dyn Transport>,
config: ClientConfig,
protocol: Box<dyn ProtocolClient>,
codecs: ProcedureCodecs<Req, Res>,
idempotency: IdempotencyLevel,
enable_http_get: bool,
}
impl<Req: Send + 'static, Res: Send + 'static> Client<Req, Res> {
pub fn new(
transport: Arc<dyn Transport>,
url: &str,
codecs: ProcedureCodecs<Req, Res>,
options: ClientOptions,
) -> ConnectResult<Self> {
let config = options.config;
let config = if url.is_empty() {
config
} else {
ClientConfig { url: url.to_string(), ..config }
};
codecs.for_request(&config.codec_name)?;
let kind = config.protocol.to_kind();
let reqs = requirements(kind, StreamType::Unary);
check_compatible(&reqs, &transport.capabilities())?;
let protocol: Box<dyn ProtocolClient> = match config.protocol {
ProtocolSelection::Connect => Box::new(ConnectClient),
ProtocolSelection::Grpc => Box::new(GrpcClient),
ProtocolSelection::GrpcWeb => Box::new(GrpcWebClient { text: false }),
};
let idempotency = options.idempotency;
let enable_http_get = options.enable_http_get;
Ok(Self {
transport,
config,
protocol,
codecs,
idempotency,
enable_http_get,
})
}
pub async fn unary(&self, ctx: Ctx, request: Request<Req>) -> ConnectResult<Response<Res>> {
let idempotency = self.idempotency;
let spec = Spec {
stream_type: StreamType::Unary,
procedure: String::new(),
is_client: true,
idempotency,
};
let call_ctx = self.derive_call_ctx(&ctx, spec);
let spec = call_ctx.spec().clone();
let url = build_url(&self.config.url, &spec.procedure);
if self.enable_http_get && idempotency == IdempotencyLevel::NoSideEffects {
return self.call_unary_get_or_fallback(call_ctx, request, url).await;
}
self.call_unary_post(call_ctx, request, url).await
}
pub async fn server_stream(
&self,
ctx: Ctx,
request: Request<Req>,
) -> ConnectResult<ServerStream<Res>> {
let spec = Spec {
stream_type: StreamType::ServerStream,
procedure: String::new(),
is_client: true,
idempotency: self.idempotency,
};
let call_ctx = self.derive_call_ctx(&ctx, spec);
let spec = call_ctx.spec().clone();
let peer = call_ctx.peer().clone();
let url = build_url(&self.config.url, &spec.procedure);
let reqs = requirements(self.config.protocol.to_kind(), StreamType::ServerStream);
check_compatible(&reqs, &self.transport.capabilities())?;
let req_codec = self.codecs.for_request(&self.config.codec_name)?;
let encoded = req_codec.marshal(&request.msg)?;
let (body, content_encoding) =
compress_if(&self.config.compression, &self.config.send_compression, &encoded)?;
let mut headers = request.headers().clone();
self.protocol.write_request_headers(
StreamType::ServerStream,
&mut headers,
&self.config.codec_name,
content_encoding.as_deref(),
);
add_timeout_header(
&self.config.protocol,
&mut headers,
call_ctx.remaining_timeout(),
);
for (key, values) in self.config.extra_headers.iter() {
for value in values {
headers.entry(key.clone()).or_default().push(value.clone());
}
}
let desc = build_request_descriptor(&url, &headers, SimpleMethod::POST);
let transport_stream = self
.transport
.open(desc)
.map_err(ConnectError::from)?;
let exchange = self.build_streaming_exchange(&spec, headers, transport_stream, &call_ctx)?;
spawn_reader_writer(exchange.reader_task, exchange.writer_task)?;
let (mut sender, mut receiver) = exchange.conn.split();
sender.send(Bytes::from(body)).await?;
sender.close_send().await?;
let response_headers = receiver.response_headers().await?.clone();
let res_codec = self.codecs.for_response(&self.config.codec_name)?;
Ok(ServerStream {
recv: receiver,
codec: res_codec,
spec,
peer,
response_headers,
trailers: SimpleHeaders::new(),
got_trailers: false,
})
}
pub async fn client_stream(
&self,
ctx: Ctx,
reqs: impl Stream<Item = Req>,
) -> ConnectResult<Response<Res>> {
let spec = Spec {
stream_type: StreamType::ClientStream,
procedure: String::new(),
is_client: true,
idempotency: self.idempotency,
};
let call_ctx = self.derive_call_ctx(&ctx, spec);
let spec = call_ctx.spec().clone();
let url = build_url(&self.config.url, &spec.procedure);
let reqs_needed = requirements(self.config.protocol.to_kind(), StreamType::ClientStream);
check_compatible(&reqs_needed, &self.transport.capabilities())?;
let mut headers = SimpleHeaders::new();
self.protocol.write_request_headers(
StreamType::ClientStream,
&mut headers,
&self.config.codec_name,
self.config.send_compression.as_deref(),
);
add_timeout_header(
&self.config.protocol,
&mut headers,
call_ctx.remaining_timeout(),
);
for (key, values) in self.config.extra_headers.iter() {
for value in values {
headers.entry(key.clone()).or_default().push(value.clone());
}
}
let desc = build_request_descriptor(&url, &headers, SimpleMethod::POST);
let transport_stream = self
.transport
.open(desc)
.map_err(ConnectError::from)?;
let exchange = self.build_streaming_exchange(&spec, headers, transport_stream, &call_ctx)?;
spawn_reader_writer(exchange.reader_task, exchange.writer_task)?;
let (mut sender, mut receiver) = exchange.conn.split();
let req_codec = self.codecs.for_request(&self.config.codec_name)?;
let res_codec = self.codecs.for_response(&self.config.codec_name)?;
let mut pinned_reqs = Box::pin(reqs);
while let Some(msg) = pinned_reqs.next().await {
let frame = req_codec.marshal(&msg)?;
sender.send(frame).await?;
}
sender.close_send().await?;
let frame = receiver
.receive()
.await?
.ok_or_else(|| ConnectError::internal("client stream: expected a response frame"))?;
let decoded = res_codec.unmarshal(frame)?;
let trailers = receiver.response_trailers().await?.clone();
let resp_headers = receiver.response_headers().await?.clone();
let mut response = Response::new(decoded);
for (h, vals) in resp_headers {
response.headers_mut().insert(h, vals);
}
for (h, vals) in trailers {
response.trailers_mut().insert(h, vals);
}
Ok(response)
}
pub async fn bidi_stream(
&self,
ctx: Ctx,
_reqs: impl Stream<Item = Req>,
) -> ConnectResult<BidiStream<Req, Res>> {
let spec = Spec {
stream_type: StreamType::BidiStream,
procedure: String::new(),
is_client: true,
idempotency: self.idempotency,
};
let call_ctx = self.derive_call_ctx(&ctx, spec);
let spec = call_ctx.spec().clone();
let peer = call_ctx.peer().clone();
let url = build_url(&self.config.url, &spec.procedure);
let reqs_needed = requirements(self.config.protocol.to_kind(), StreamType::BidiStream);
check_compatible(&reqs_needed, &self.transport.capabilities())?;
let mut headers = SimpleHeaders::new();
self.protocol.write_request_headers(
StreamType::BidiStream,
&mut headers,
&self.config.codec_name,
self.config.send_compression.as_deref(),
);
add_timeout_header(
&self.config.protocol,
&mut headers,
call_ctx.remaining_timeout(),
);
for (key, values) in self.config.extra_headers.iter() {
for value in values {
headers.entry(key.clone()).or_default().push(value.clone());
}
}
let desc = build_request_descriptor(&url, &headers, SimpleMethod::POST);
let transport_stream = self
.transport
.open(desc)
.map_err(ConnectError::from)?;
let exchange = self.build_streaming_exchange(&spec, headers, transport_stream, &call_ctx)?;
spawn_reader_writer(exchange.reader_task, exchange.writer_task)?;
let (sender, receiver) = exchange.conn.split();
let req_codec = self.codecs.for_request(&self.config.codec_name)?;
let res_codec = self.codecs.for_response(&self.config.codec_name)?;
Ok(BidiStream {
send: sender,
recv: receiver,
req_codec,
res_codec,
spec,
peer,
})
}
fn derive_call_ctx(&self, caller: &Ctx, spec: Spec) -> Ctx {
let deadline = match (caller.request.deadline, self.config.default_timeout) {
(Some(caller_deadline), Some(default)) => {
let earliest = Instant::now() + default;
Some(caller_deadline.min(earliest))
}
(Some(caller_deadline), None) => Some(caller_deadline),
(None, Some(default)) => Some(Instant::now() + default),
(None, None) => None,
};
let peer = Peer {
addr: self.config.url.clone(),
protocol: self.config.protocol.peer_protocol_name().to_string(),
};
Ctx {
bag: caller.bag.clone(),
request: RequestContext::for_dispatch(
spec,
peer,
SimpleHeaders::new(),
deadline,
caller.request.extensions.clone(),
caller.request.connection.clone(),
CancelSignal::linked(caller.cancel_signal()),
),
}
}
fn build_streaming_exchange(
&self,
spec: &Spec,
headers: SimpleHeaders,
stream: TransportStream,
ctx: &Ctx,
) -> ConnectResult<ClientExchange> {
self.protocol.new_conn(
spec,
headers,
stream,
ClientExchangeConfig {
cancel: CancelSignal::linked(ctx.cancel_signal()),
read_max_bytes: self.config.limits.read_max_bytes,
},
)
}
async fn call_unary_post(
&self,
ctx: Ctx,
request: Request<Req>,
url: String,
) -> ConnectResult<Response<Res>> {
let req_codec = self.codecs.for_request(&self.config.codec_name)?;
let encoded = req_codec.marshal(&request.msg)?;
let (body, content_encoding) =
compress_if(&self.config.compression, &self.config.send_compression, &encoded)?;
let mut headers = request.headers().clone();
self.protocol.write_request_headers(
StreamType::Unary,
&mut headers,
&self.config.codec_name,
content_encoding.as_deref(),
);
add_timeout_header(&self.config.protocol, &mut headers, ctx.remaining_timeout());
for (key, values) in self.config.extra_headers.iter() {
for value in values {
headers.entry(key.clone()).or_default().push(value.clone());
}
}
let wire_body = self
.protocol
.encode_unary_request(&body, content_encoding.is_some());
let proto = match self.config.protocol {
ProtocolSelection::Grpc => Proto::HTTP20,
_ => Proto::HTTP11,
};
let call = UnaryCall {
headers: headers.clone(),
codec_name: self.config.codec_name.clone(),
frame: wire_body,
};
let transport = Arc::clone(&self.transport);
let read_max_bytes = self.config.limits.read_max_bytes;
let url_clone = url.clone();
let proto_for_inner = proto.clone();
let inner: UnaryFunc = Arc::new(move |_ctx: Ctx, call: UnaryCall| {
let transport = Arc::clone(&transport);
let url = url_clone.clone();
let proto = proto_for_inner.clone();
Box::pin(async move {
let uri = Uri::parse(&url)
.map_err(|e| ConnectError::internal(format!("invalid URL: {e}")))?;
let desc = RequestDescriptor {
proto,
request_url: SimpleUrl::url_only(&url),
request_uri: uri,
headers: call.headers,
method: SimpleMethod::POST,
};
do_unary_round_trip(&*transport, desc, call.frame, read_max_bytes).await
})
});
let chain = InterceptorChain::new(self.config.interceptors.clone());
let wrapped = chain.wrap_unary(inner);
let reply = wrapped(ctx.clone(), call).await?;
if let Some(grpc_err) = parse_grpc_error_trailer(&reply.trailers)
.or_else(|| parse_grpc_error_trailer(&reply.headers))
{
return Err(grpc_err);
}
if reply.frame.is_empty()
&& matches!(
self.config.protocol,
ProtocolSelection::Grpc | ProtocolSelection::GrpcWeb
)
{
return Err(ConnectError::internal(
"gRPC unary response carried no message and no grpc-status",
)
.into());
}
let wire = self.protocol.decode_unary_response(reply.frame)?;
let res_codec = self.codecs.for_response(&self.config.codec_name)?;
let msg = res_codec.unmarshal(wire)?;
let mut response = Response::new(msg);
for (h, vals) in &reply.headers {
response.headers_mut().insert(h.clone(), vals.clone());
}
for (h, vals) in &reply.trailers {
response.trailers_mut().insert(h.clone(), vals.clone());
}
Ok(response)
}
async fn call_unary_get_or_fallback(
&self,
ctx: Ctx,
request: Request<Req>,
url: String,
) -> ConnectResult<Response<Res>> {
let request_url = self.build_get_url(&url, &request)?;
if self.config.get_url_max_bytes > 0 && request_url.len() > self.config.get_url_max_bytes {
return self.call_unary_post(ctx, request, url).await;
}
let uri = Uri::parse(&request_url)
.map_err(|e| ConnectError::internal(format!("invalid GET URL: {e}")))?;
let desc = RequestDescriptor {
proto: Proto::HTTP11,
request_url: SimpleUrl::url_only(&request_url),
request_uri: uri,
headers: request.headers().clone(),
method: SimpleMethod::GET,
};
let result = do_unary_round_trip(
&*self.transport,
desc,
Bytes::new(),
self.config.limits.read_max_bytes,
)
.await;
match result {
Ok(reply) => {
let res_codec = self.codecs.for_response(&self.config.codec_name)?;
let msg = res_codec.unmarshal(reply.frame)?;
let mut response = Response::new(msg);
for (h, vals) in &reply.headers {
response.headers_mut().insert(h.clone(), vals.clone());
}
for (h, vals) in &reply.trailers {
response.trailers_mut().insert(h.clone(), vals.clone());
}
Ok(response)
}
Err(e) => {
if self.config.get_use_fallback && is_method_not_allowed(&e) {
self.call_unary_post(ctx, request, url).await
} else {
Err(e)
}
}
}
}
fn build_get_url(&self, base_url: &str, request: &Request<Req>) -> ConnectResult<String> {
use crate::shared::protocol::connect::encode_get_query;
let procedure = if request.spec().procedure.is_empty() {
"Method"
} else {
&request.spec().procedure
};
let req_codec = self.codecs.for_request(&self.config.codec_name)?;
let stable = req_codec.marshal_stable(&request.msg)?;
let compressor = self.config.send_compression.as_ref().and_then(|name| {
self.config.compression.get(name).cloned()
});
let query = encode_get_query(
&self.config.codec_name,
&stable,
req_codec.is_binary(),
compressor.as_ref(),
)?;
Ok(format!("{base_url}/{procedure}?{query}"))
}
}
pub struct ServerStream<Res> {
recv: Box<dyn ClientReceiver>,
codec: Arc<dyn CodecFor<Res>>,
spec: Spec,
peer: Peer,
response_headers: SimpleHeaders,
trailers: SimpleHeaders,
got_trailers: bool,
}
impl<Res: Send + 'static> ServerStream<Res> {
pub async fn receive(&mut self) -> ConnectResult<Option<Res>> {
match self.recv.receive().await? {
Some(frame) => Ok(Some(self.codec.unmarshal(frame)?)),
None => Ok(None),
}
}
#[must_use]
pub fn response_headers(&self) -> &SimpleHeaders {
&self.response_headers
}
pub async fn response_trailers(&mut self) -> ConnectResult<&SimpleHeaders> {
if !self.got_trailers {
self.trailers = self.recv.response_trailers().await?.clone();
self.got_trailers = true;
}
Ok(&self.trailers)
}
#[must_use]
pub fn spec(&self) -> &Spec {
&self.spec
}
#[must_use]
pub fn peer(&self) -> &Peer {
&self.peer
}
pub fn close(self) {
}
}
pub struct ClientStream<Req, Res> {
send: Box<dyn ClientSender>,
recv: Box<dyn ClientReceiver>,
req_codec: Arc<dyn CodecFor<Req>>,
res_codec: Arc<dyn CodecFor<Res>>,
}
impl<Req: Send + 'static, Res: Send + 'static> ClientStream<Req, Res> {
pub fn request_headers_mut(&mut self) -> &mut SimpleHeaders {
self.send.request_headers_mut()
}
pub async fn send(&mut self, msg: &Req) -> ConnectResult<()> {
let frame = self.req_codec.marshal(msg)?;
self.send.send(frame).await
}
pub async fn close_and_receive(self) -> ConnectResult<Response<Res>> {
let send = Box::new(self.send);
send.close_send().await?;
let mut recv = self.recv;
match recv.receive().await? {
Some(frame) => {
let msg = self.res_codec.unmarshal(frame)?;
let mut response = Response::new(msg);
let hdrs = recv.response_headers().await?.clone();
for (h, vals) in hdrs {
response.headers_mut().insert(h, vals);
}
let trls = recv.response_trailers().await?.clone();
for (h, vals) in trls {
response.trailers_mut().insert(h, vals);
}
Ok(response)
}
None => Err(ConnectError::internal("client stream: no response").into()),
}
}
}
pub struct BidiStream<Req, Res> {
send: Box<dyn ClientSender>,
recv: Box<dyn ClientReceiver>,
req_codec: Arc<dyn CodecFor<Req>>,
res_codec: Arc<dyn CodecFor<Res>>,
spec: Spec,
peer: Peer,
}
impl<Req: Send + 'static, Res: Send + 'static> BidiStream<Req, Res> {
pub fn request_headers_mut(&mut self) -> &mut SimpleHeaders {
self.send.request_headers_mut()
}
pub async fn send(&mut self, msg: &Req) -> ConnectResult<()> {
let frame = self.req_codec.marshal(msg)?;
self.send.send(frame).await
}
pub async fn send_headers(&mut self) -> ConnectResult<()> {
self.send.flush_headers().await
}
pub async fn close_request(&mut self) -> ConnectResult<()> {
let old = std::mem::replace(&mut self.send, Box::new(NoopClientSender));
old.close_send().await
}
pub async fn receive(&mut self) -> ConnectResult<Option<Res>> {
match self.recv.receive().await? {
Some(frame) => Ok(Some(self.res_codec.unmarshal(frame)?)),
None => Ok(None),
}
}
pub async fn response_headers(&mut self) -> ConnectResult<&SimpleHeaders> {
self.recv.response_headers().await
}
pub async fn response_trailers(&mut self) -> ConnectResult<&SimpleHeaders> {
self.recv.response_trailers().await
}
pub fn split(self) -> (BidiSender<Req>, BidiReceiver<Res>) {
let sender = BidiSender {
send: self.send,
codec: self.req_codec,
};
let receiver = BidiReceiver {
recv: self.recv,
codec: self.res_codec,
spec: self.spec,
peer: self.peer,
};
(sender, receiver)
}
#[must_use]
pub fn spec(&self) -> &Spec {
&self.spec
}
#[must_use]
pub fn peer(&self) -> &Peer {
&self.peer
}
}
pub struct BidiSender<Req> {
send: Box<dyn ClientSender>,
codec: Arc<dyn CodecFor<Req>>,
}
impl<Req: Send + 'static> BidiSender<Req> {
pub async fn send(&mut self, msg: &Req) -> ConnectResult<()> {
let frame = self.codec.marshal(msg)?;
self.send.send(frame).await
}
pub async fn send_headers(&mut self) -> ConnectResult<()> {
self.send.flush_headers().await
}
pub async fn close_request(self) -> ConnectResult<()> {
Box::new(self.send).close_send().await
}
}
pub struct BidiReceiver<Res> {
recv: Box<dyn ClientReceiver>,
codec: Arc<dyn CodecFor<Res>>,
spec: Spec,
peer: Peer,
}
impl<Res: Send + 'static> BidiReceiver<Res> {
pub async fn receive(&mut self) -> ConnectResult<Option<Res>> {
match self.recv.receive().await? {
Some(frame) => Ok(Some(self.codec.unmarshal(frame)?)),
None => Ok(None),
}
}
pub async fn response_headers(&mut self) -> ConnectResult<&SimpleHeaders> {
self.recv.response_headers().await
}
pub async fn response_trailers(&mut self) -> ConnectResult<&SimpleHeaders> {
self.recv.response_trailers().await
}
#[must_use]
pub fn spec(&self) -> &Spec {
&self.spec
}
#[must_use]
pub fn peer(&self) -> &Peer {
&self.peer
}
}
struct NoopClientSender;
impl ClientSender for NoopClientSender {
fn request_headers_mut(&mut self) -> &mut SimpleHeaders {
unreachable!("NoopClientSender has no headers")
}
fn flush_headers(&mut self) -> BoxFuture<'_, ConnectResult<()>> {
Box::pin(async { Ok(()) })
}
fn send(&mut self, _frame: Bytes) -> BoxFuture<'_, ConnectResult<()>> {
Box::pin(async { Ok(()) })
}
fn close_send(self: Box<Self>) -> BoxFuture<'static, ConnectResult<()>> {
Box::pin(async { Ok(()) })
}
}
fn build_url(base_url: &str, procedure: &str) -> String {
if procedure.is_empty() {
return base_url.trim_end_matches('/').to_string();
}
let base = base_url.trim_end_matches('/');
let proc_path = procedure.trim_start_matches('/');
format!("{base}/{proc_path}")
}
fn build_request_descriptor(url: &str, headers: &SimpleHeaders, method: SimpleMethod) -> RequestDescriptor {
let uri = Uri::parse(url).unwrap_or_else(|_| {
Uri::parse(&format!("http://localhost{url}")).expect("fallback URI")
});
RequestDescriptor {
proto: Proto::HTTP11,
request_url: SimpleUrl::url_only(url),
request_uri: uri,
headers: headers.clone(),
method,
}
}
async fn do_unary_round_trip(
transport: &dyn Transport,
desc: RequestDescriptor,
body: Bytes,
read_max_bytes: usize,
) -> ConnectResult<UnaryReply> {
let mut stream = transport.open(desc).map_err(ConnectError::from)?;
if !body.is_empty() {
stream.send_body.try_send(body).map_err(|_| {
ConnectError::internal("failed to push request body into transport pipe")
})?;
}
stream.send_body.close();
let (status, resp_headers) = stream
.head
.next()
.await
.ok_or_else(|| ConnectError::unavailable("transport closed without response head"))?
.map_err(ConnectError::from)?;
let mut body_buf = Vec::new();
while let Some(chunk) = stream.recv_body.next().await {
body_buf.extend_from_slice(&chunk.map_err(ConnectError::from)?);
if read_max_bytes != 0 && body_buf.len() > read_max_bytes {
return Err(ConnectError::resource_exhausted(format!(
"response body exceeds read_max_bytes {read_max_bytes}"
))
.into());
}
}
let trailers = stream.trailers.receive().await.unwrap_or_default();
if status == Status::OK {
Ok(UnaryReply {
headers: resp_headers,
trailers,
frame: Bytes::from(body_buf),
})
} else {
let error_body = if body_buf.is_empty() {
format!("HTTP {status}")
} else {
String::from_utf8_lossy(&body_buf).to_string()
};
Err(ConnectError::new(code_from_http_status(&status), error_body).into())
}
}
fn spawn_reader_writer(
reader_task: BoxFuture<'static, ConnectResult<()>>,
writer_task: BoxFuture<'static, ConnectResult<()>>,
) -> ConnectResult<()> {
valtron::send(valtron::from_future(reader_task))
.map_err(|e| ConnectError::internal(format!("failed to spawn reader task: {e}")))?;
valtron::send(valtron::from_future(writer_task))
.map_err(|e| ConnectError::internal(format!("failed to spawn writer task: {e}")))?;
Ok(())
}
fn compress_if(
registry: &CompressionRegistry,
algo: &Option<String>,
data: &[u8],
) -> ConnectResult<(Vec<u8>, Option<String>)> {
match algo {
Some(name) if name != "identity" => {
let compressor = registry.get(name).ok_or_else(|| {
ConnectError::internal(format!("unknown compression algorithm: {name}"))
})?;
let compressed = compressor.compress(data)?;
Ok((compressed, Some(name.clone())))
}
_ => Ok((data.to_vec(), None)),
}
}
fn add_timeout_header(
protocol: &ProtocolSelection,
headers: &mut SimpleHeaders,
timeout: Option<Duration>,
) {
let Some(d) = timeout else {
return;
};
match protocol {
ProtocolSelection::Connect => {
headers.insert(
SimpleHeader::from(connect_consts::HEADER_TIMEOUT.to_string()),
vec![d.as_millis().to_string()],
);
}
ProtocolSelection::Grpc | ProtocolSelection::GrpcWeb => {
headers.insert(
SimpleHeader::from(grpc_web_consts::HEADER_TIMEOUT.to_string()),
vec![encode_grpc_timeout(d)],
);
}
}
}
fn is_method_not_allowed(err: &ErrorTrace<ConnectError>) -> bool {
let msg = err.to_string().to_lowercase();
msg.contains("405") || msg.contains("method not allowed")
|| msg.contains("415") || msg.contains("unsupported media type")
}
pub(crate) fn code_from_http_status(status: &Status) -> Code {
let n: usize = status.clone().into();
match n {
400 => Code::InvalidArgument,
401 | 403 => Code::Unauthenticated,
404 => Code::NotFound,
408 => Code::DeadlineExceeded,
429 => Code::ResourceExhausted,
499 => Code::Canceled,
500 => Code::Internal,
501 => Code::Unimplemented,
503 => Code::Unavailable,
504 => Code::DeadlineExceeded,
_ => Code::Unknown,
}
}