use std::io;
use std::pin::Pin;
use std::sync::Arc;
use std::task::{Context, Poll};
use std::time::Instant;
use bytes::Bytes;
use foundation_core::io::readers::Data;
use foundation_core::valtron::Pipe;
#[cfg(not(target_family = "wasm"))]
use foundation_core::valtron::Stream;
use foundation_core::valtron::{PipeReceiver, PipeSender};
use foundation_errstacks::ErrorTrace;
use foundation_http::shared::context::ContextBag;
use futures::future::{select, Either};
use futures::StreamExt;
#[cfg(not(target_family = "wasm"))]
use foundation_netio::http2::types::{H2Frame, H2IncomingFrame, SimpleIncomingRequestHeader};
#[cfg(not(target_family = "wasm"))]
use foundation_netio::shared::client::body_reader::{
SendSafeBodyBytesItem, SendSafeBodyBytesIterator,
};
use foundation_netio::shared::http::{
ChunkedData, HttpReaderError, Proto, SendSafeBody, SimpleHeader, SimpleHeaders,
SimpleIncomingRequest, SimpleMethod, SimpleOutgoingResponse, Status,
};
use crate::shared::context::{CancelSignal, Ctx, IdempotencyLevel, Peer, RequestContext, Spec};
use crate::shared::error::{ConnectError, ConnectResult};
use crate::shared::error_writer::ErrorWriter;
use crate::shared::protocol::{ExchangeConfig, ProtocolHandler, UnaryOutcome};
use crate::shared::transport::{
check_compatible, requirements, BoxFuture, ProtocolKind, TransportCapabilities,
};
use super::{HandlerEntry, HandlerKind, Router};
pub struct ConnectRpcHandler {
router: Arc<Router>,
}
impl ConnectRpcHandler {
pub(crate) fn new(router: Router) -> Self {
Self {
router: Arc::new(router),
}
}
#[must_use]
pub fn router(&self) -> &Router {
&self.router
}
#[must_use]
pub fn dispatch(
&self,
bag: Arc<ContextBag>,
mut request: SimpleIncomingRequest,
) -> BoxFuture<'static, SimpleOutgoingResponse> {
let router = self.router.clone();
Box::pin(async move {
let proto = request.proto.clone();
let body = in_memory_body(request.body.take());
let mut sink = CollectingSink::default();
match dispatch_request(router, bag, request, body, &mut sink, CallHooks::default())
.await
{
Ok(()) => sink.into_response().unwrap_or_else(|| {
tracing::error!("dispatch finished without producing a response");
internal_error_response(proto)
}),
Err(e) => {
tracing::error!(error = %e, "collecting sink failed");
internal_error_response(proto)
}
}
})
}
pub fn dispatch_to<'a>(
&self,
bag: Arc<ContextBag>,
request: SimpleIncomingRequest,
body: RequestBody<'a>,
sink: &'a mut dyn ResponseSink,
hooks: CallHooks<'a>,
) -> BoxFuture<'a, io::Result<()>> {
let router = self.router.clone();
Box::pin(dispatch_request(router, bag, request, body, sink, hooks))
}
}
pub trait DeadlineTimer: Send + Sync {
fn sleep_until(&self, deadline: Instant) -> BoxFuture<'_, ()>;
}
#[derive(Default)]
pub struct CallHooks<'a> {
cancel: CancelSignal,
timer: Option<&'a dyn DeadlineTimer>,
}
impl<'a> CallHooks<'a> {
#[must_use]
pub fn new(cancel: CancelSignal) -> Self {
Self {
cancel,
timer: None,
}
}
#[must_use]
pub fn with_timer(mut self, timer: &'a dyn DeadlineTimer) -> Self {
self.timer = Some(timer);
self
}
}
#[cfg(not(target_family = "wasm"))]
pub(crate) use crate::native::deadline_timer::ThreadTimer;
pub(crate) async fn deadline_expiry(timer: Option<&dyn DeadlineTimer>, deadline: Option<Instant>) {
match (timer, deadline) {
(Some(timer), Some(deadline)) => timer.sleep_until(deadline).await,
_ => std::future::pending().await,
}
}
pub(crate) fn deadline_exceeded() -> ErrorTrace<ConnectError> {
ConnectError::deadline_exceeded("the call's deadline passed").into()
}
fn deadline_response(request: &SimpleIncomingRequest) -> SimpleOutgoingResponse {
let mut response = blank_response(request);
write_error(&mut response, request, &deadline_exceeded());
response
}
pub type RequestBody<'a> = Pin<Box<dyn futures::Stream<Item = io::Result<Bytes>> + Send + 'a>>;
pub fn in_memory_body(body: Option<SendSafeBody>) -> RequestBody<'static> {
Box::pin(InMemoryBody(BodyReader::new(body)))
}
struct InMemoryBody(BodyReader);
impl futures::Stream for InMemoryBody {
type Item = io::Result<Bytes>;
fn poll_next(mut self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
Poll::Ready(match self.0.step() {
BodyStep::Chunk(bytes) => Some(Ok(bytes)),
BodyStep::End => None,
BodyStep::Failed(e) => Some(Err(e)),
BodyStep::WouldBlock => Some(Err(io::Error::new(
io::ErrorKind::WouldBlock,
"request body is not ready and there is no connection to wait on",
))),
})
}
}
pub(crate) enum BodyStep {
Chunk(Bytes),
WouldBlock,
End,
Failed(io::Error),
}
pub(crate) struct BodyReader {
body: SendSafeBody,
}
impl BodyReader {
pub(crate) fn new(body: Option<SendSafeBody>) -> Self {
Self {
body: body.unwrap_or(SendSafeBody::None),
}
}
pub(crate) fn step(&mut self) -> BodyStep {
loop {
match &mut self.body {
SendSafeBody::None => return BodyStep::End,
SendSafeBody::Bytes(bytes) => {
let bytes = std::mem::take(bytes);
self.body = SendSafeBody::None;
if !bytes.is_empty() {
return BodyStep::Chunk(Bytes::from(bytes));
}
}
SendSafeBody::Text(text) => {
let text = std::mem::take(text);
self.body = SendSafeBody::None;
if !text.is_empty() {
return BodyStep::Chunk(Bytes::from(text.into_bytes()));
}
}
SendSafeBody::Stream(iter) => match iter.as_mut().and_then(Iterator::next) {
None => self.body = SendSafeBody::None,
Some(Ok(Data::Bytes(bytes))) => {
if !bytes.is_empty() {
return BodyStep::Chunk(Bytes::from(bytes));
}
}
Some(Ok(Data::Retry)) => return BodyStep::WouldBlock,
Some(Err(e)) => return classify_body_error(&e),
},
SendSafeBody::ChunkedStream(iter) => {
match iter.as_mut().and_then(Iterator::next) {
None => self.body = SendSafeBody::None,
Some(Ok(ChunkedData::Data(bytes, _))) => {
if !bytes.is_empty() {
return BodyStep::Chunk(Bytes::from(bytes));
}
}
Some(Ok(ChunkedData::DataEnded | ChunkedData::Trailers(_))) => {}
Some(Err(e)) => return classify_body_error(&e),
}
}
SendSafeBody::LineFeedStream(_) | SendSafeBody::SseStream(_) => {
self.body = SendSafeBody::None;
return BodyStep::Failed(io::Error::new(
io::ErrorKind::InvalidData,
"line-feed and event-stream request bodies are not RPC bodies",
));
}
}
}
}
}
fn classify_body_error(e: &foundation_core::extensions::result_ext::BoxedError) -> BodyStep {
let would_block = e
.downcast_ref::<HttpReaderError>()
.is_some_and(HttpReaderError::is_would_block)
|| e
.downcast_ref::<io::Error>()
.is_some_and(|e| e.kind() == io::ErrorKind::WouldBlock);
if would_block {
BodyStep::WouldBlock
} else {
BodyStep::Failed(io::Error::other(e.to_string()))
}
}
pub trait ResponseSink: Send {
fn whole(&mut self, response: SimpleOutgoingResponse) -> BoxFuture<'_, io::Result<()>>;
fn open(&mut self, head: SimpleOutgoingResponse) -> BoxFuture<'_, io::Result<()>>;
fn chunk(&mut self, bytes: Bytes) -> BoxFuture<'_, io::Result<()>>;
fn finish(&mut self) -> BoxFuture<'_, io::Result<()>>;
}
#[derive(Default)]
pub struct CollectingSink {
response: Option<SimpleOutgoingResponse>,
body: Vec<u8>,
}
impl CollectingSink {
#[must_use]
pub fn into_response(self) -> Option<SimpleOutgoingResponse> {
let mut response = self.response?;
if response.body.is_none() {
response.body = Some(SendSafeBody::Bytes(self.body));
}
Some(response)
}
}
impl ResponseSink for CollectingSink {
fn whole(&mut self, response: SimpleOutgoingResponse) -> BoxFuture<'_, io::Result<()>> {
self.response = Some(response);
Box::pin(std::future::ready(Ok(())))
}
fn open(&mut self, mut head: SimpleOutgoingResponse) -> BoxFuture<'_, io::Result<()>> {
head.body = None;
self.response = Some(head);
Box::pin(std::future::ready(Ok(())))
}
fn chunk(&mut self, bytes: Bytes) -> BoxFuture<'_, io::Result<()>> {
self.body.extend_from_slice(&bytes);
Box::pin(std::future::ready(Ok(())))
}
fn finish(&mut self) -> BoxFuture<'_, io::Result<()>> {
Box::pin(std::future::ready(Ok(())))
}
}
fn internal_error_response(proto: Proto) -> SimpleOutgoingResponse {
SimpleOutgoingResponse {
proto,
status: Status::InternalServerError,
headers: SimpleHeaders::new(),
body: None,
trailers: SimpleHeaders::new(),
}
}
async fn dispatch_request(
router: Arc<Router>,
bag: Arc<ContextBag>,
request: SimpleIncomingRequest,
body: RequestBody<'_>,
sink: &mut dyn ResponseSink,
hooks: CallHooks<'_>,
) -> io::Result<()> {
let path = extract_path(&request.request_url.url);
let Some(entry) = router.lookup(&path) else {
return sink.whole(status_only(&request, Status::NotFound)).await;
};
let allowed = allowed_methods(entry);
if !allowed.contains(&request.method) {
return sink.whole(method_not_allowed(&request, &allowed)).await;
}
let Some(protocol) = entry
.protocol_handlers
.iter()
.find(|p| p.can_handle(&request))
else {
return sink.whole(unsupported_media_type(&request, entry)).await;
};
let protocol = protocol.as_ref();
let Some(codec_name) = protocol.codec_name(&request) else {
return sink.whole(unsupported_media_type(&request, entry)).await;
};
if !entry.codec_meta.has_codec(&codec_name) {
return sink.whole(unsupported_media_type(&request, entry)).await;
}
let caps = capabilities_for(&request.proto);
let reqs = requirements(protocol.kind(), entry.spec.stream_type);
if check_compatible(&reqs, &caps).is_err() {
return sink.whole(http_version_not_supported(&request)).await;
}
if entry.options.require_connect_protocol_header && protocol.kind() == ProtocolKind::Connect {
if let Err(e) = require_connect_version(&request) {
let mut response = blank_response(&request);
write_error(&mut response, &request, &e);
return sink.whole(response).await;
}
}
let compression = entry
.options
.compression
.as_ref()
.unwrap_or_else(|| router.global_compression());
let ctx = build_ctx(bag, &request, entry.spec.clone(), protocol, hooks.cancel);
let spec = entry.spec.clone();
let pipe_depth = entry.options.pipe_depth;
let timer = hooks.timer;
match &entry.handler {
HandlerKind::Unary(handler) => {
let cancel = ctx.cancel_signal().clone();
let response = run_unary_until_deadline(
handler.clone(),
protocol,
request,
body,
codec_name,
compression,
ctx,
entry.options.limits.read_max_bytes,
timer,
)
.await;
let written = sink.whole(response).await;
if written.is_err() {
cancel.cancel();
}
written
}
HandlerKind::ServerStream(handler)
| HandlerKind::ClientStream(handler)
| HandlerKind::BidiStream(handler) => {
run_streaming(
handler.clone(),
protocol,
request,
body,
spec,
codec_name,
compression,
ctx,
pipe_depth,
entry.options.limits.read_max_bytes,
sink,
timer,
)
.await
}
}
}
#[allow(clippy::too_many_arguments)]
pub(crate) async fn run_unary_until_deadline(
handler: Arc<dyn super::erased::ErasedUnaryHandler>,
protocol: &dyn ProtocolHandler,
request: SimpleIncomingRequest,
body: RequestBody<'_>,
codec_name: String,
compression: &crate::shared::compression::CompressionRegistry,
ctx: Ctx,
read_max_bytes: usize,
timer: Option<&dyn DeadlineTimer>,
) -> SimpleOutgoingResponse {
let deadline = ctx.request.deadline;
let (Some(timer), Some(deadline)) = (timer, deadline) else {
return run_unary_body(
handler,
protocol,
request,
body,
codec_name,
compression,
ctx,
read_max_bytes,
)
.await;
};
let cancel = ctx.cancel_signal().clone();
let timed_out = deadline_response(&request);
let call = std::pin::pin!(run_unary_body(
handler,
protocol,
request,
body,
codec_name,
compression,
ctx,
read_max_bytes,
));
match select(call, timer.sleep_until(deadline)).await {
Either::Left((response, _)) => response,
Either::Right(((), _call)) => {
cancel.cancel();
timed_out
}
}
}
#[allow(clippy::too_many_arguments)]
pub(crate) async fn run_unary(
handler: Arc<dyn super::erased::ErasedUnaryHandler>,
protocol: &dyn ProtocolHandler,
mut request: SimpleIncomingRequest,
codec_name: String,
compression: &crate::shared::compression::CompressionRegistry,
ctx: Ctx,
read_max_bytes: usize,
timer: Option<&dyn DeadlineTimer>,
) -> SimpleOutgoingResponse {
let body = in_memory_body(request.body.take());
run_unary_until_deadline(
handler,
protocol,
request,
body,
codec_name,
compression,
ctx,
read_max_bytes,
timer,
)
.await
}
#[allow(clippy::too_many_arguments)]
async fn run_unary_body(
handler: Arc<dyn super::erased::ErasedUnaryHandler>,
protocol: &dyn ProtocolHandler,
request: SimpleIncomingRequest,
mut body: RequestBody<'_>,
codec_name: String,
compression: &crate::shared::compression::CompressionRegistry,
ctx: Ctx,
read_max_bytes: usize,
) -> SimpleOutgoingResponse {
let mut response = blank_response(&request);
let body = match read_unary_body(&mut body, read_max_bytes).await {
Ok(b) => b,
Err(UnaryBodyError::TooLarge(len)) => {
tracing::warn!(
len,
read_max_bytes,
"request body exceeded read_max_bytes; returning 413"
);
response.status = Status::PayloadTooLarge;
return response;
}
Err(UnaryBodyError::Read(e)) => {
tracing::warn!(error = %e, "request body could not be read");
let error = ConnectError::new(
crate::shared::error::Code::InvalidArgument,
format!("request body could not be read: {e}"),
);
write_error(&mut response, &request, &error.into());
return response;
}
};
let decoded = protocol.decode_unary_request(&request, body, compression, read_max_bytes);
let outcome: ConnectResult<UnaryOutcome> = match decoded {
Ok(msg) => match handler.handle(ctx, &codec_name, msg).await {
Ok((frame, headers, trailers)) => Ok(UnaryOutcome {
frame,
headers,
trailers,
}),
Err(e) => Err(e),
},
Err(e) => Err(e),
};
match outcome {
Ok(outcome) => {
if let Err(e) = protocol.encode_unary_response(
&mut response,
&request,
&codec_name,
outcome,
compression,
) {
write_error(&mut response, &request, &e);
}
}
Err(e) => write_error(&mut response, &request, &e),
}
response
}
#[allow(clippy::too_many_arguments)]
async fn run_streaming(
handler: Arc<dyn super::erased::ErasedStreamHandler>,
protocol: &dyn ProtocolHandler,
request: SimpleIncomingRequest,
mut body: RequestBody<'_>,
spec: Spec,
codec_name: String,
compression: &crate::shared::compression::CompressionRegistry,
ctx: Ctx,
pipe_depth: usize,
read_max_bytes: usize,
sink: &mut dyn ResponseSink,
timer: Option<&dyn DeadlineTimer>,
) -> io::Result<()> {
let (req_tx, req_rx) = Pipe::<Bytes>::with_depth(pipe_depth);
let (resp_tx, resp_rx) = Pipe::<Bytes>::with_depth(pipe_depth);
let cancel = ctx.cancel_signal().clone();
let deadline = ctx.request.deadline;
let config = ExchangeConfig {
compression,
cancel: cancel.clone(),
read_max_bytes,
};
let exchange = match protocol.new_conn(&request, spec, req_rx, resp_tx, config) {
Ok(exchange) => exchange,
Err(e) => {
let mut response = blank_response(&request);
write_error(&mut response, &request, &e);
return sink.whole(response).await;
}
};
let crate::shared::protocol::HandlerExchange {
conn,
reader_task,
writer_task,
} = exchange;
let feeder = async move {
while let Some(chunk) = body.next().await {
if req_tx.send(chunk?).await.is_err() {
break;
}
}
req_tx.close();
Ok::<(), io::Error>(())
};
let content_type = protocol.streaming_response_content_type(&request, &codec_name);
let response_encoding = protocol.streaming_response_encoding(&request, compression);
let mut head = blank_response(&request);
head.status = Status::OK;
head.headers
.insert(SimpleHeader::CONTENT_TYPE, vec![content_type.clone()]);
if let Some((name, enc)) = response_encoding.clone() {
head.headers.insert(SimpleHeader::from(name), vec![enc]);
}
let pump = async {
let resp_rx = resp_rx;
let mut head = Some(head);
while let Some(chunk) = resp_rx.receive().await {
if let Some(head) = head.take() {
sink.open(head).await?;
}
sink.chunk(chunk).await?;
}
Ok::<_, io::Error>(head.is_none())
};
let handler_fut = handler.handle(ctx, &codec_name, conn);
let request_side = futures::future::try_join(feeder, async {
Ok::<_, io::Error>(reader_task.await)
});
let response_side = futures::future::join(writer_task, handler_fut);
let rest = std::pin::pin!(exchange_until_aborted(
request_side,
response_side,
deadline_expiry(timer, deadline),
std::future::pending::<()>(),
));
let (opened, end) = {
let pump = std::pin::pin!(pump);
match select(pump, rest).await {
Either::Left((Err(e), _rest)) => {
cancel.cancel();
return Err(e);
}
Either::Left((Ok(opened), rest)) => (opened, rest.await),
Either::Right((end, pump)) => match pump.await {
Ok(opened) => (opened, end),
Err(e) => {
cancel.cancel();
return Err(e);
}
},
}
};
let failure = match end {
StreamEnd::Done {
reader,
writer,
handler,
} => {
if opened {
if let Some(e) = unclosed_stream_error(writer, handler) {
if let Some(frame) = stream_error_frame(protocol, &request, &e) {
sink.chunk(frame).await?;
}
}
return sink.finish().await;
}
stream_failure(reader, writer, handler)
}
StreamEnd::Aborted(abort) => {
cancel.cancel();
match abort {
Abort::Deadline => Some(deadline_exceeded()),
Abort::BodyFailed(e) => Some(body_read_error(&e)),
Abort::PeerGone => return Ok(()),
}
}
};
match failure {
Some(e) if opened => {
if let Some(frame) = stream_error_frame(protocol, &request, &e) {
sink.chunk(frame).await?;
}
sink.finish().await
}
Some(e) => {
let mut response = blank_response(&request);
write_error(&mut response, &request, &e);
sink.whole(response).await
}
None if opened => sink.finish().await,
None => {
let mut response = blank_response(&request);
response.status = Status::OK;
response
.headers
.insert(SimpleHeader::CONTENT_TYPE, vec![content_type]);
if let Some((name, enc)) = response_encoding {
response.headers.insert(SimpleHeader::from(name), vec![enc]);
}
response.body = Some(SendSafeBody::Bytes(Vec::new()));
sink.whole(response).await
}
}
}
pub(crate) enum StreamEnd {
Done {
reader: ConnectResult<()>,
writer: ConnectResult<()>,
handler: ConnectResult<()>,
},
Aborted(Abort),
}
pub(crate) enum Abort {
Deadline,
BodyFailed(io::Error),
PeerGone,
}
pub(crate) async fn exchange_until_aborted<Req, Resp, Expiry, Gone>(
request_side: Req,
response_side: Resp,
expiry: Expiry,
gone: Gone,
) -> StreamEnd
where
Req: std::future::Future<Output = Result<((), ConnectResult<()>), io::Error>>,
Resp: std::future::Future<Output = (ConnectResult<()>, ConnectResult<()>)>,
Expiry: std::future::Future<Output = ()>,
Gone: std::future::Future<Output = ()>,
{
let request_side = std::pin::pin!(request_side);
let response_side = std::pin::pin!(response_side);
let exchange = std::pin::pin!(async move {
match select(request_side, response_side).await {
Either::Left((Ok(((), reader)), response_side)) => {
let (writer, handler) = response_side.await;
StreamEnd::Done {
reader,
writer,
handler,
}
}
Either::Left((Err(e), _response_side)) => StreamEnd::Aborted(Abort::BodyFailed(e)),
Either::Right(((writer, handler), _request_side)) => StreamEnd::Done {
reader: Ok(()),
writer,
handler,
},
}
});
let expiry = std::pin::pin!(expiry);
let gone = std::pin::pin!(gone);
let cut_short = select(expiry, gone);
match select(exchange, cut_short).await {
Either::Left((end, _)) => end,
Either::Right((Either::Left(_), _exchange)) => StreamEnd::Aborted(Abort::Deadline),
Either::Right((Either::Right(_), _exchange)) => StreamEnd::Aborted(Abort::PeerGone),
}
}
pub(crate) fn unclosed_stream_error(
writer: ConnectResult<()>,
handler: ConnectResult<()>,
) -> Option<ErrorTrace<ConnectError>> {
match (writer, handler) {
(Err(writer), Err(_)) => Some(writer),
(Ok(()), Err(handler)) => Some(handler),
(_, Ok(())) => None,
}
}
pub(crate) fn stream_failure(
reader: ConnectResult<()>,
writer: ConnectResult<()>,
handler: ConnectResult<()>,
) -> Option<ErrorTrace<ConnectError>> {
handler.err().or(writer.err()).or(reader.err())
}
pub(crate) fn body_read_error(error: &io::Error) -> ErrorTrace<ConnectError> {
tracing::warn!(error = %error, "request body stream failed; cancelling the RPC");
let code = if error.kind() == io::ErrorKind::TimedOut {
crate::shared::error::Code::DeadlineExceeded
} else {
crate::shared::error::Code::Canceled
};
ConnectError::new(code, format!("request body could not be read: {error}")).into()
}
pub(crate) fn stream_error_frame(
protocol: &dyn ProtocolHandler,
request: &SimpleIncomingRequest,
error: &ErrorTrace<ConnectError>,
) -> Option<Bytes> {
match protocol.encode_stream_error(request, error) {
Ok(frame) => frame,
Err(e) => {
tracing::error!(error = ?e, "could not encode the stream's terminal error");
None
}
}
}
pub(crate) fn extract_path(url: &str) -> String {
url.split('?').next().unwrap_or(url).to_string()
}
pub(crate) fn allowed_methods(entry: &HandlerEntry) -> Vec<SimpleMethod> {
let mut methods: Vec<SimpleMethod> = Vec::new();
let is_nosideeffects = entry.spec.idempotency == IdempotencyLevel::NoSideEffects;
for protocol in &entry.protocol_handlers {
for method in protocol.allowed_methods() {
if !is_nosideeffects && method == &SimpleMethod::GET {
continue;
}
if !methods.contains(method) {
methods.push(method.clone());
}
}
}
methods
}
pub(crate) fn capabilities_for(proto: &Proto) -> TransportCapabilities {
let h2 = *proto >= Proto::HTTP20;
TransportCapabilities {
request_streaming: true,
full_duplex: h2,
h2_trailers: h2,
http_versions: match proto {
Proto::HTTP10 => &[Proto::HTTP10],
Proto::HTTP11 => &[Proto::HTTP11],
Proto::HTTP20 => &[Proto::HTTP20],
Proto::HTTP30 => &[Proto::HTTP30],
Proto::Custom(_) => &[Proto::HTTP11],
},
multiplexed: h2,
}
}
pub(crate) fn build_ctx(
bag: Arc<ContextBag>,
request: &SimpleIncomingRequest,
spec: Spec,
protocol: &dyn ProtocolHandler,
cancel: CancelSignal,
) -> Ctx {
let deadline = protocol
.parse_timeout(&request.headers)
.map(|d| Instant::now() + d);
let peer = Peer {
addr: request
.connection
.peer_addr
.as_ref()
.map(|a| format!("{a:?}"))
.unwrap_or_default(),
protocol: protocol_name(protocol.kind()).to_string(),
};
let request_context = RequestContext::for_dispatch(
spec,
peer,
request.headers.clone(),
deadline,
foundation_netio::shared::http::Extensions::new(),
request.connection.clone(),
cancel,
);
Ctx {
bag,
request: request_context,
}
}
pub(crate) fn require_connect_version(request: &SimpleIncomingRequest) -> ConnectResult<()> {
use crate::shared::protocol::connect::constants;
let present = if request.method == SimpleMethod::GET {
request
.request_url
.queries
.as_ref()
.and_then(|q| q.get(constants::QUERY_CONNECT_VERSION))
.is_some_and(|v| v == constants::QUERY_CONNECT_VERSION_VALUE)
} else {
request
.headers
.get(&SimpleHeader::from(
constants::HEADER_PROTOCOL_VERSION.to_string(),
))
.and_then(|v| v.first())
.is_some_and(|v| v == constants::PROTOCOL_VERSION)
};
if present {
Ok(())
} else {
Err(ConnectError::invalid_argument(format!(
"missing required {} header",
constants::HEADER_PROTOCOL_VERSION
))
.into())
}
}
fn protocol_name(kind: ProtocolKind) -> &'static str {
match kind {
ProtocolKind::Connect => "connect",
ProtocolKind::Grpc => "grpc",
ProtocolKind::GrpcWeb => "grpc-web",
}
}
enum UnaryBodyError {
TooLarge(usize),
Read(io::Error),
}
async fn read_unary_body(
body: &mut RequestBody<'_>,
max_bytes: usize,
) -> Result<Bytes, UnaryBodyError> {
let mut buf = bytes::BytesMut::new();
while let Some(chunk) = body.next().await {
let chunk = chunk.map_err(UnaryBodyError::Read)?;
buf.extend_from_slice(&chunk);
if max_bytes > 0 && buf.len() > max_bytes {
return Err(UnaryBodyError::TooLarge(buf.len()));
}
}
Ok(buf.freeze())
}
pub(crate) fn blank_response(request: &SimpleIncomingRequest) -> SimpleOutgoingResponse {
SimpleOutgoingResponse {
proto: request.proto.clone(),
status: Status::OK,
headers: SimpleHeaders::new(),
body: None,
trailers: SimpleHeaders::new(),
}
}
pub(crate) fn status_only(
request: &SimpleIncomingRequest,
status: Status,
) -> SimpleOutgoingResponse {
let mut response = blank_response(request);
response.status = status;
response
}
pub(crate) fn method_not_allowed(
request: &SimpleIncomingRequest,
allowed: &[SimpleMethod],
) -> SimpleOutgoingResponse {
let mut response = status_only(request, Status::MethodNotAllowed);
let allow = allowed
.iter()
.map(SimpleMethod::to_string)
.collect::<Vec<_>>()
.join(", ");
response
.headers
.insert(SimpleHeader::from("allow".to_string()), vec![allow]);
response
}
pub(crate) fn unsupported_media_type(
request: &SimpleIncomingRequest,
entry: &HandlerEntry,
) -> SimpleOutgoingResponse {
let mut response = status_only(request, Status::UnsupportedMediaType);
let names = entry.codec_meta.codec_names();
let accept_post = names
.iter()
.map(|codec| format!("application/{codec}"))
.collect::<Vec<_>>()
.join(", ");
if !accept_post.is_empty() {
response.headers.insert(
SimpleHeader::from("accept-post".to_string()),
vec![accept_post],
);
}
response
}
pub(crate) fn http_version_not_supported(
request: &SimpleIncomingRequest,
) -> SimpleOutgoingResponse {
status_only(request, Status::HttpVersionNotSupported)
}
pub(crate) fn write_error(
response: &mut SimpleOutgoingResponse,
request: &SimpleIncomingRequest,
error: &foundation_errstacks::ErrorTrace<ConnectError>,
) {
if let Err(e) = ErrorWriter::new().write(response, request, error) {
tracing::error!(error = ?e, "could not render the error response");
}
}
#[cfg(not(target_family = "wasm"))]
pub(crate) fn request_from_header(header: &SimpleIncomingRequestHeader) -> SimpleIncomingRequest {
SimpleIncomingRequest {
proto: Proto::HTTP20,
request_uri: header.uri.clone(),
request_url: header.url.clone(),
body: None,
headers: header.headers.clone(),
method: header.method.clone(),
extensions: None,
connection: header.connection.clone(),
}
}
#[cfg(not(target_family = "wasm"))]
const H2_FORBIDDEN_HEADERS: [&str; 5] = [
"connection",
"keep-alive",
"proxy-connection",
"transfer-encoding",
"upgrade",
];
#[cfg(not(target_family = "wasm"))]
fn h2_headers(headers: &SimpleHeaders) -> Vec<(Bytes, Bytes)> {
let mut out = Vec::new();
for (key, values) in headers.iter() {
let name = key.to_string().to_lowercase();
if H2_FORBIDDEN_HEADERS.contains(&name.as_str()) {
continue;
}
for value in values {
out.push((Bytes::from(name.clone()), Bytes::from(value.clone())));
}
}
out
}
#[cfg(not(target_family = "wasm"))]
fn stream_gone<T>(_: T) -> io::Error {
io::Error::new(io::ErrorKind::BrokenPipe, "h2 response stream closed")
}
#[cfg(not(target_family = "wasm"))]
async fn send_whole_response(
tx: &PipeSender<H2Frame>,
mut response: SimpleOutgoingResponse,
) -> io::Result<()> {
let status = response.status.into_usize() as u16;
let headers = h2_headers(&response.headers);
let has_trailers = !response.trailers.is_empty();
let body = response.body.take();
fn next_chunk(it: &mut Option<SendSafeBodyBytesIterator>) -> io::Result<Option<Bytes>> {
let Some(ref mut iter) = it else {
return Ok(None);
};
loop {
match iter.next() {
Some(Stream::Next(SendSafeBodyBytesItem::Chunk(b))) => return Ok(Some(b)),
Some(Stream::Next(SendSafeBodyBytesItem::StreamError(e))) => {
return Err(io::Error::other(e.to_string()));
}
None => return Ok(None),
_ => {} }
}
}
let mut iter = body.map(SendSafeBodyBytesIterator::new);
let mut chunk = next_chunk(&mut iter)?;
let end_stream = chunk.is_none() && !has_trailers;
tx.send(H2Frame::Headers {
status,
headers,
end_stream,
})
.await
.map_err(stream_gone)?;
while let Some(cur) = chunk.take() {
let next = next_chunk(&mut iter)?;
let data_end = next.is_none() && !has_trailers;
tx.send(H2Frame::Data {
payload: cur,
end_stream: data_end,
})
.await
.map_err(stream_gone)?;
chunk = next;
}
if has_trailers {
tx.send(H2Frame::Trailers {
headers: h2_headers(&response.trailers),
})
.await
.map_err(stream_gone)?;
}
Ok(())
}
#[cfg(not(target_family = "wasm"))]
enum H2Body {
Collected(Vec<u8>),
Reset,
TooLarge(usize),
}
#[cfg(not(target_family = "wasm"))]
async fn next_h2_chunk(
body: &PipeReceiver<H2IncomingFrame>,
tx: &PipeSender<H2Frame>,
) -> io::Result<Option<Bytes>> {
match body.receive().await {
Some(H2IncomingFrame::Data(chunk)) => Ok(Some(chunk)),
Some(H2IncomingFrame::Reset(code)) => Err(io::Error::new(
io::ErrorKind::ConnectionReset,
format!("the peer reset the h2 stream ({code:?})"),
)),
None if tx.is_closed() => Err(io::Error::new(
io::ErrorKind::ConnectionReset,
"the h2 stream was dropped before its request ended",
)),
None => Ok(None),
}
}
#[cfg(not(target_family = "wasm"))]
async fn collect_h2_body(
body: &PipeReceiver<H2IncomingFrame>,
tx: &PipeSender<H2Frame>,
max_bytes: usize,
) -> H2Body {
let mut out = Vec::new();
loop {
match next_h2_chunk(body, tx).await {
Ok(Some(chunk)) => {
out.extend_from_slice(&chunk);
if max_bytes > 0 && out.len() > max_bytes {
return H2Body::TooLarge(out.len());
}
}
Ok(None) => return H2Body::Collected(out),
Err(e) => {
tracing::debug!(error = %e, "h2 unary request abandoned");
return H2Body::Reset;
}
}
}
}
#[cfg(not(target_family = "wasm"))]
impl ConnectRpcHandler {
pub fn dispatch_h2(
&self,
bag: Arc<ContextBag>,
header: SimpleIncomingRequestHeader,
body: PipeReceiver<H2IncomingFrame>,
tx: PipeSender<H2Frame>,
) -> BoxFuture<'static, io::Result<()>> {
let router = self.router.clone();
Box::pin(dispatch_h2_stream(router, bag, header, body, tx))
}
}
#[cfg(not(target_family = "wasm"))]
async fn dispatch_h2_stream(
router: Arc<Router>,
bag: Arc<ContextBag>,
header: SimpleIncomingRequestHeader,
body: PipeReceiver<H2IncomingFrame>,
tx: PipeSender<H2Frame>,
) -> io::Result<()> {
let request = request_from_header(&header);
let path = extract_path(&request.request_url.url);
let Some(entry) = router.lookup(&path) else {
return send_whole_response(&tx, status_only(&request, Status::NotFound)).await;
};
let allowed = allowed_methods(entry);
if !allowed.contains(&request.method) {
return send_whole_response(&tx, method_not_allowed(&request, &allowed)).await;
}
let Some(protocol) = entry
.protocol_handlers
.iter()
.find(|p| p.can_handle(&request))
else {
return send_whole_response(&tx, unsupported_media_type(&request, entry)).await;
};
let protocol = protocol.as_ref();
let Some(codec_name) = protocol.codec_name(&request) else {
return send_whole_response(&tx, unsupported_media_type(&request, entry)).await;
};
if !entry.codec_meta.has_codec(&codec_name) {
return send_whole_response(&tx, unsupported_media_type(&request, entry)).await;
}
let caps = capabilities_for(&request.proto);
let reqs = requirements(protocol.kind(), entry.spec.stream_type);
if check_compatible(&reqs, &caps).is_err() {
return send_whole_response(&tx, http_version_not_supported(&request)).await;
}
if entry.options.require_connect_protocol_header && protocol.kind() == ProtocolKind::Connect {
if let Err(e) = require_connect_version(&request) {
let mut response = blank_response(&request);
write_error(&mut response, &request, &e);
return send_whole_response(&tx, response).await;
}
}
let compression = entry
.options
.compression
.as_ref()
.unwrap_or_else(|| router.global_compression());
let ctx = build_ctx(
bag,
&request,
entry.spec.clone(),
protocol,
CancelSignal::new(),
);
let spec = entry.spec.clone();
let pipe_depth = entry.options.pipe_depth;
match &entry.handler {
HandlerKind::Unary(handler) => {
let read_max_bytes = entry.options.limits.read_max_bytes;
let cancel = ctx.cancel_signal().clone();
let deadline = ctx.request.deadline;
let timed_out = deadline.map(|_| deadline_response(&request));
let too_large = status_only(&request, Status::PayloadTooLarge);
let handler = handler.clone();
let (body, tx_ref) = (&body, &tx);
let call = std::pin::pin!(async move {
let collected = match collect_h2_body(body, tx_ref, read_max_bytes).await {
H2Body::Collected(collected) => collected,
H2Body::Reset => return None,
H2Body::TooLarge(len) => {
tracing::warn!(
len,
read_max_bytes,
"request body exceeded read_max_bytes; returning 413"
);
return Some(too_large);
}
};
let mut request = request;
request.body = Some(SendSafeBody::Bytes(collected));
Some(
run_unary(
handler,
protocol,
request,
codec_name,
compression,
ctx,
read_max_bytes,
None,
)
.await,
)
});
let expiry = std::pin::pin!(deadline_expiry(Some(&ThreadTimer), deadline));
let gone = std::pin::pin!(tx.closed());
let cut_short = select(expiry, gone);
match select(call, cut_short).await {
Either::Left((Some(response), _)) => send_whole_response(&tx, response).await,
Either::Left((None, _)) | Either::Right((Either::Right(_), _)) => {
cancel.cancel();
Ok(())
}
Either::Right((Either::Left(_), _call)) => {
cancel.cancel();
match timed_out {
Some(response) => send_whole_response(&tx, response).await,
None => Ok(()),
}
}
}
}
HandlerKind::ServerStream(handler)
| HandlerKind::ClientStream(handler)
| HandlerKind::BidiStream(handler) => {
run_streaming_h2(
handler.clone(),
protocol,
request,
spec,
codec_name,
compression,
ctx,
pipe_depth,
entry.options.limits.read_max_bytes,
body,
&tx,
)
.await
}
}
}
#[allow(clippy::too_many_arguments)]
#[cfg(not(target_family = "wasm"))]
async fn run_streaming_h2(
handler: Arc<dyn super::erased::ErasedStreamHandler>,
protocol: &dyn ProtocolHandler,
request: SimpleIncomingRequest,
spec: Spec,
codec_name: String,
compression: &crate::shared::compression::CompressionRegistry,
ctx: Ctx,
pipe_depth: usize,
read_max_bytes: usize,
body: PipeReceiver<H2IncomingFrame>,
tx: &PipeSender<H2Frame>,
) -> io::Result<()> {
let (req_tx, req_rx) = Pipe::<Bytes>::with_depth(pipe_depth);
let (resp_tx, resp_rx) = Pipe::<Bytes>::with_depth(pipe_depth);
let cancel = ctx.cancel_signal().clone();
let deadline = ctx.request.deadline;
let config = ExchangeConfig {
compression,
cancel: cancel.clone(),
read_max_bytes,
};
let exchange = match protocol.new_conn(&request, spec, req_rx, resp_tx, config) {
Ok(exchange) => exchange,
Err(e) => {
let mut response = blank_response(&request);
write_error(&mut response, &request, &e);
return send_whole_response(tx, response).await;
}
};
let crate::shared::protocol::HandlerExchange {
conn,
reader_task,
writer_task,
} = exchange;
let feeder = async {
while let Some(chunk) = next_h2_chunk(&body, tx).await? {
if req_tx.send(chunk).await.is_err() {
break;
}
}
req_tx.close();
Ok::<(), io::Error>(())
};
let content_type = protocol.streaming_response_content_type(&request, &codec_name);
let response_encoding = protocol.streaming_response_encoding(&request, compression);
let pump_content_type = content_type.clone();
let pump = async {
let resp_rx = resp_rx;
let mut opened = false;
while let Some(chunk) = resp_rx.receive().await {
if !opened {
opened = true;
let mut headers = vec![(
Bytes::from_static(b"content-type"),
Bytes::from(pump_content_type.clone()),
)];
if let Some((name, enc)) = &response_encoding {
headers.push((Bytes::from(name.clone()), Bytes::from(enc.clone())));
}
tx.send(H2Frame::Headers {
status: 200,
headers,
end_stream: false,
})
.await
.map_err(stream_gone)?;
}
tx.send(H2Frame::Data {
payload: chunk,
end_stream: false,
})
.await
.map_err(stream_gone)?;
}
Ok::<bool, io::Error>(opened)
};
let handler_fut = handler.handle(ctx, &codec_name, conn);
let request_side = futures::future::try_join(feeder, async {
Ok::<_, io::Error>(reader_task.await)
});
let response_side = futures::future::join(writer_task, handler_fut);
let rest = std::pin::pin!(exchange_until_aborted(
request_side,
response_side,
deadline_expiry(Some(&ThreadTimer), deadline),
tx.closed(),
));
let (opened, end) = {
let pump = std::pin::pin!(pump);
match select(pump, rest).await {
Either::Left((Err(e), _rest)) => {
cancel.cancel();
return Err(e);
}
Either::Left((Ok(opened), rest)) => (opened, rest.await),
Either::Right((end, pump)) => match pump.await {
Ok(opened) => (opened, end),
Err(e) => {
cancel.cancel();
return Err(e);
}
},
}
};
let is_grpc = protocol.kind() == ProtocolKind::Grpc;
let failure = match end {
StreamEnd::Aborted(Abort::PeerGone | Abort::BodyFailed(_)) => {
cancel.cancel();
return Ok(());
}
StreamEnd::Aborted(Abort::Deadline) => {
cancel.cancel();
Some(deadline_exceeded())
}
StreamEnd::Done {
writer, handler, ..
} if opened && !is_grpc => unclosed_stream_error(writer, handler),
StreamEnd::Done {
reader,
writer,
handler,
} => stream_failure(reader, writer, handler),
};
if opened {
let closing = if is_grpc {
H2Frame::Trailers {
headers: grpc_status_trailers(failure.as_ref()),
}
} else {
if let Some(frame) = failure
.as_ref()
.and_then(|e| stream_error_frame(protocol, &request, e))
{
tx.send(H2Frame::Data {
payload: frame,
end_stream: false,
})
.await
.map_err(stream_gone)?;
}
H2Frame::Data {
payload: Bytes::new(),
end_stream: true,
}
};
tx.send(closing).await.map_err(stream_gone)?;
return Ok(());
}
let mut response = blank_response(&request);
if let Some(e) = failure {
write_error(&mut response, &request, &e);
} else {
response.status = Status::OK;
response
.headers
.insert(SimpleHeader::CONTENT_TYPE, vec![content_type]);
if is_grpc {
response
.trailers
.insert(SimpleHeader::from("grpc-status".to_string()), vec!["0".to_string()]);
}
}
send_whole_response(tx, response).await
}
fn grpc_status_trailers(
error: Option<&foundation_errstacks::ErrorTrace<crate::ConnectError>>,
) -> Vec<(Bytes, Bytes)> {
match error {
None => vec![(Bytes::from_static(b"grpc-status"), Bytes::from_static(b"0"))],
Some(e) => {
let ctx = e.current_context();
let code = ctx.code() as u32;
let message = ctx.message().to_string();
let mut encoded = String::new();
for b in message.bytes() {
if b == b'%' || !(0x20..=0x7e).contains(&b) {
encoded.push_str(&format!("%{b:02X}"));
} else {
encoded.push(b as char);
}
}
vec![
(Bytes::from_static(b"grpc-status"), Bytes::from(code.to_string())),
(Bytes::from_static(b"grpc-message"), Bytes::from(encoded)),
]
}
}
}