pub mod connect;
pub mod grpc;
pub mod grpc_web;
use std::time::Duration;
use bytes::Bytes;
use foundation_netio::shared::http::{
SimpleHeaders, SimpleIncomingRequest, SimpleMethod, SimpleOutgoingResponse,
};
use crate::shared::compression::CompressionRegistry;
use crate::shared::context::{CancelSignal, Spec, StreamType};
use crate::shared::error::{ConnectError, ConnectResult};
use foundation_errstacks::ErrorTrace;
use crate::shared::transport::{
fail_stream, race, BodyStream, BoxFuture, ByteSink, ByteSource, ClientConn, Frame,
HandlerConn, HeadStream, ProtocolKind, TransportStream,
};
use foundation_core::valtron::PipeSender;
use foundation_netio::shared::http::Status;
use std::sync::Mutex;
pub type BoxedTask = BoxFuture<'static, ConnectResult<()>>;
pub struct UnaryOutcome {
pub frame: Bytes,
pub headers: SimpleHeaders,
pub trailers: SimpleHeaders,
}
pub struct HandlerExchange {
pub conn: Box<dyn HandlerConn>,
pub reader_task: BoxedTask,
pub writer_task: BoxedTask,
}
pub struct ClientExchange {
pub conn: Box<dyn ClientConn>,
pub reader_task: BoxedTask,
pub writer_task: BoxedTask,
}
pub struct ExchangeConfig<'a> {
pub compression: &'a CompressionRegistry,
pub cancel: CancelSignal,
pub read_max_bytes: usize,
}
pub trait ProtocolHandler: Send + Sync {
fn kind(&self) -> ProtocolKind;
fn allowed_methods(&self) -> &[SimpleMethod];
fn content_types(&self) -> Vec<String>;
fn parse_timeout(&self, headers: &SimpleHeaders) -> Option<Duration>;
fn can_handle(&self, request: &SimpleIncomingRequest) -> bool;
fn codec_name(&self, request: &SimpleIncomingRequest) -> Option<String>;
fn new_conn(
&self,
request: &SimpleIncomingRequest,
spec: Spec,
body: ByteSource,
responder: ByteSink,
config: ExchangeConfig<'_>,
) -> ConnectResult<HandlerExchange>;
fn encode_stream_error(
&self,
request: &SimpleIncomingRequest,
error: &ErrorTrace<ConnectError>,
) -> ConnectResult<Option<Bytes>> {
let _ = (request, error);
Ok(None)
}
fn decode_unary_request(
&self,
request: &SimpleIncomingRequest,
body: Bytes,
compression: &CompressionRegistry,
read_max_bytes: usize,
) -> ConnectResult<Bytes>;
fn streaming_response_content_type(
&self,
request: &SimpleIncomingRequest,
codec_name: &str,
) -> String;
fn streaming_response_encoding(
&self,
request: &SimpleIncomingRequest,
compression: &CompressionRegistry,
) -> Option<(String, String)>;
fn encode_unary_response(
&self,
response: &mut SimpleOutgoingResponse,
request: &SimpleIncomingRequest,
codec_name: &str,
outcome: UnaryOutcome,
compression: &CompressionRegistry,
) -> ConnectResult<()>;
}
pub trait ProtocolClient: Send + Sync {
fn write_request_headers(
&self,
stream_type: StreamType,
headers: &mut SimpleHeaders,
codec_name: &str,
compression: Option<&str>,
);
fn encode_unary_request(&self, body: &[u8], is_compressed: bool) -> Bytes;
fn decode_unary_response(&self, body: Bytes) -> ConnectResult<Bytes>;
fn new_conn(
&self,
spec: &Spec,
headers: SimpleHeaders,
stream: TransportStream,
config: ClientExchangeConfig,
) -> ConnectResult<ClientExchange>;
}
pub struct ClientExchangeConfig {
pub cancel: CancelSignal,
pub read_max_bytes: usize,
}
pub(crate) async fn open_response_stream(
head: &mut HeadStream,
body: &mut BodyStream,
tx: &PipeSender<Frame>,
response_headers: &Mutex<Option<SimpleHeaders>>,
cancel: &CancelSignal,
read_max_bytes: usize,
) -> Option<SimpleHeaders> {
use futures::StreamExt;
let (status, headers) = match head.next().await {
Some(Ok(head)) => head,
Some(Err(e)) => {
fail_stream(tx, &ConnectError::from(e).into(), cancel).await;
return None;
}
None => {
let e = ConnectError::unavailable("transport closed without response head");
fail_stream(tx, &e.into(), cancel).await;
return None;
}
};
*response_headers
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner) = Some(headers.clone());
if status == Status::OK {
return Some(headers);
}
let mut message = Vec::new();
while let Some(Ok(chunk)) = body.next().await {
message.extend_from_slice(&chunk);
if read_max_bytes != 0 && message.len() >= read_max_bytes {
message.truncate(read_max_bytes);
break;
}
}
let message = if message.is_empty() {
format!("HTTP {status}")
} else {
String::from_utf8_lossy(&message).to_string()
};
let error: ErrorTrace<ConnectError> =
ConnectError::new(crate::shared::client::code_from_http_status(&status), message).into();
let end = Frame::EndStream {
error: Some(error),
trailers: headers,
};
let _ = race(tx.send(end), cancel.cancelled()).await;
tx.close();
None
}
#[must_use]
pub fn canonicalize_content_type(content_type: &str) -> String {
content_type
.split(';')
.next()
.unwrap_or(content_type)
.trim()
.to_ascii_lowercase()
}
const DEFAULT_CODEC: &str = "proto";
#[must_use]
pub fn codec_for_content_type<'a>(canonical: &'a str, prefix: &str) -> Option<&'a str> {
let rest = canonical.strip_prefix(prefix)?;
match rest.as_bytes().first() {
None => Some(DEFAULT_CODEC),
Some(b'+') => match &rest[1..] {
"" => None,
codec => Some(codec),
},
Some(_) => None,
}
}
#[must_use]
pub fn parse_connect_content_type(content_type: &str) -> Option<(String, bool)> {
let canonical = canonicalize_content_type(content_type);
let rest = canonical.strip_prefix("application/")?;
match rest.strip_prefix("connect+") {
Some(codec) => Some((codec.to_string(), true)),
None => {
let codec = rest.to_string();
if codec.contains('+') || codec == "grpc" || codec == "grpc-web" {
None
} else {
Some((codec, false))
}
}
}
}