use std::future::Future;
use std::io;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::{Arc, Mutex};
use std::task::{Context, Poll};
use bytes::Bytes;
use foundation_core::valtron::{Pipe, Stream as VStream};
use foundation_http::native::serve::{BoxFuture, H3Serve};
use foundation_http::shared::context::ContextBag;
use foundation_netio::http3::connection::{error_code, H3Error, H3Request};
use foundation_netio::http3::types::{request_from_fields, response_to_fields};
use foundation_netio::netcap::context::ConnectionContext;
use foundation_netio::quic::QuinnBidiStream;
use foundation_netio::shared::client::body_reader::AsyncSendSafeBody;
use foundation_netio::shared::http::{
SendSafeBody, SimpleHeader, SimpleIncomingRequest, SimpleOutgoingResponse, Status,
};
use futures::StreamExt;
use crate::shared::compression::CompressionRegistry;
use crate::shared::context::{CancelSignal, Ctx, Spec};
use crate::shared::protocol::{ExchangeConfig, HandlerExchange, ProtocolHandler};
use crate::shared::router::dispatch::{
self, allowed_methods, blank_response, build_ctx, capabilities_for, deadline_exceeded,
exchange_until_aborted, extract_path, http_version_not_supported, method_not_allowed,
request_from_header, run_unary, status_only, stream_error_frame, stream_failure,
unclosed_stream_error, unsupported_media_type, write_error, Abort, StreamEnd, ThreadTimer,
};
use crate::shared::router::erased::ErasedStreamHandler;
use crate::shared::router::{ConnectRpcHandler, HandlerKind};
use crate::shared::transport::capabilities::{requirements, ProtocolKind};
use crate::shared::transport::check_compatible;
type Req = H3Request<QuinnBidiStream>;
pub struct ConnectRpcServeH3 {
handler: Arc<ConnectRpcHandler>,
}
impl ConnectRpcServeH3 {
#[must_use]
pub fn new(handler: ConnectRpcHandler) -> Self {
Self {
handler: Arc::new(handler),
}
}
}
impl H3Serve for ConnectRpcServeH3 {
fn serve_h3(
&self,
bag: Arc<ContextBag>,
connection: Arc<ConnectionContext>,
request: H3Request<QuinnBidiStream>,
) -> BoxFuture<'static, io::Result<()>> {
let handler = self.handler.clone();
Box::pin(dispatch_h3(handler, bag, connection, request))
}
}
pub async fn dispatch_h3(
handler: Arc<ConnectRpcHandler>,
bag: Arc<ContextBag>,
connection: Arc<ConnectionContext>,
mut req: H3Request<QuinnBidiStream>,
) -> io::Result<()> {
let fields = poll_h3(|| req.poll_headers()).await?;
let header = request_from_fields(&fields, connection)
.map_err(|e| io::Error::other(e.to_string()))?;
let request = request_from_header(&header);
let path = extract_path(&request.request_url.url);
let router = handler.router();
let Some(entry) = router.lookup(&path) else {
let resp = status_only(&request, Status::NotFound);
send_h3_response(&mut req, resp).await?;
return Ok(());
};
let allowed = allowed_methods(entry);
if !allowed.contains(&request.method) {
let resp = method_not_allowed(&request, &allowed);
send_h3_response(&mut req, resp).await?;
return Ok(());
}
let Some(protocol) = entry
.protocol_handlers
.iter()
.find(|p| p.can_handle(&request))
else {
let resp = unsupported_media_type(&request, entry);
send_h3_response(&mut req, resp).await?;
return Ok(());
};
let protocol = protocol.as_ref();
let Some(codec_name) = protocol.codec_name(&request) else {
let resp = unsupported_media_type(&request, entry);
send_h3_response(&mut req, resp).await?;
return Ok(());
};
if !entry.codec_meta.has_codec(&codec_name) {
let resp = unsupported_media_type(&request, entry);
send_h3_response(&mut req, resp).await?;
return Ok(());
}
let caps = capabilities_for(&request.proto);
let reqs = requirements(protocol.kind(), entry.spec.stream_type);
if check_compatible(&reqs, &caps).is_err() {
let resp = http_version_not_supported(&request);
send_h3_response(&mut req, resp).await?;
return Ok(());
}
if entry.options.require_connect_protocol_header && protocol.kind() == ProtocolKind::Connect {
if let Err(e) = dispatch::require_connect_version(&request) {
let mut response = blank_response(&request);
write_error(&mut response, &request, &e);
send_h3_response(&mut req, response).await?;
return Ok(());
}
}
let compression = entry
.options
.compression
.as_ref()
.unwrap_or_else(|| router.global_compression());
let spec = entry.spec.clone();
let ctx = build_ctx(bag, &request, spec.clone(), protocol, CancelSignal::new());
match &entry.handler {
HandlerKind::Unary(handler_fn) => {
let read_max_bytes = entry.options.limits.read_max_bytes;
let body = match collect_h3_body(&mut req, read_max_bytes).await {
Ok(CollectedBody::Complete(body)) => body,
Ok(CollectedBody::TooLarge { read }) => {
tracing::warn!(
read,
read_max_bytes,
"h3 unary request body exceeds read_max_bytes; returning 413"
);
req.stop_sending(error_code::H3_NO_ERROR);
let resp = status_only(&request, Status::PayloadTooLarge);
return send_h3_response(&mut req, resp).await;
}
Err(e) => return Err(abort_call(&mut req, ctx.cancel_signal(), &e)),
};
let mut request = request;
request.body = Some(SendSafeBody::Bytes(body));
let cancel = ctx.cancel_signal().clone();
let response = run_unary(
handler_fn.clone(),
protocol,
request,
codec_name,
compression,
ctx,
read_max_bytes,
Some(&ThreadTimer),
)
.await;
let sent = send_h3_response(&mut req, response).await;
if sent.is_err() {
cancel.cancel();
}
sent
}
HandlerKind::ServerStream(handler)
| HandlerKind::ClientStream(handler)
| HandlerKind::BidiStream(handler) => {
run_streaming_h3(
handler.clone(),
protocol,
request,
spec,
codec_name,
compression,
ctx,
entry.options.pipe_depth,
entry.options.limits.read_max_bytes,
req,
)
.await
}
}
}
#[allow(clippy::too_many_arguments)]
async fn run_streaming_h3(
handler: Arc<dyn ErasedStreamHandler>,
protocol: &dyn ProtocolHandler,
request: SimpleIncomingRequest,
spec: Spec,
codec_name: String,
compression: &CompressionRegistry,
ctx: Ctx,
pipe_depth: usize,
read_max_bytes: usize,
mut req: Req,
) -> 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) => {
req.stop_sending(error_code::H3_NO_ERROR);
let mut response = blank_response(&request);
write_error(&mut response, &request, &e);
return send_h3_response(&mut req, response).await;
}
};
let HandlerExchange {
conn,
reader_task,
writer_task,
} = exchange;
let shared = Mutex::new(req);
let body_done = AtomicBool::new(false);
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 handler_fut = handler.handle(ctx, &codec_name, conn);
let pumped = {
let feeder = async {
while let Some(chunk) = poll_shared(&shared, H3Request::poll_body).await? {
if req_tx.send(chunk).await.is_err() {
tracing::debug!("h3 request pipe closed by its reader; abandoning the body");
req_tx.close();
return Ok(());
}
}
body_done.store(true, Ordering::Release);
req_tx.close();
Ok::<(), io::Error>(())
};
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 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":status"), Bytes::from_static(b"200")),
(
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())));
}
let mut header_frame = Req::encode_headers(&headers);
poll_shared(&shared, |r| r.poll_send_headers(&mut header_frame)).await?;
}
let mut data_frame = Req::encode_data(chunk);
poll_shared(&shared, |r| r.poll_send_data(&mut data_frame)).await?;
}
Ok::<bool, io::Error>(opened)
};
let rest = std::pin::pin!(exchange_until_aborted(
request_side,
response_side,
dispatch::deadline_expiry(Some(&ThreadTimer), deadline),
std::future::pending::<()>(),
));
let pump = std::pin::pin!(pump);
match futures::future::select(pump, rest).await {
futures::future::Either::Left((Err(e), _rest)) => Err(e),
futures::future::Either::Left((Ok(opened), rest)) => Ok((opened, rest.await)),
futures::future::Either::Right((end, pump)) => pump.await.map(|opened| (opened, end)),
}
};
let mut req = shared
.into_inner()
.map_err(|_| io::Error::other("h3 request lock poisoned"))?;
let (opened, end) = match pumped {
Ok(pumped) => pumped,
Err(e) => {
cancel.cancel();
return Err(e);
}
};
let failure = match end {
StreamEnd::Aborted(Abort::BodyFailed(e)) => {
return Err(abort_call(&mut req, &cancel, &e));
}
StreamEnd::Aborted(Abort::PeerGone) => {
cancel.cancel();
return Ok(());
}
StreamEnd::Aborted(Abort::Deadline) => {
cancel.cancel();
Some(deadline_exceeded())
}
StreamEnd::Done {
writer, handler, ..
} if opened => unclosed_stream_error(writer, handler),
StreamEnd::Done {
reader,
writer,
handler,
} => stream_failure(reader, writer, handler),
};
if !body_done.load(Ordering::Acquire) {
req.stop_sending(error_code::H3_NO_ERROR);
}
if opened {
if let Some(frame) = failure
.as_ref()
.and_then(|e| stream_error_frame(protocol, &request, e))
{
let mut data_frame = Req::encode_data(frame);
poll_h3(|| req.poll_send_data(&mut data_frame)).await?;
}
poll_h3_finish(&mut req).await?;
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]);
}
send_h3_response(&mut req, response).await
}
fn abort_call(req: &mut Req, cancel: &CancelSignal, error: &io::Error) -> io::Error {
tracing::debug!(error = %error, "h3 request body failed; cancelling the call");
cancel.cancel();
req.reset(error_code::H3_REQUEST_CANCELLED);
io::Error::new(
io::ErrorKind::ConnectionAborted,
format!("h3 request aborted: {error}"),
)
}
fn map_h3<T>(cx: &Context<'_>, polled: VStream<Result<T, H3Error>, ()>) -> Poll<io::Result<T>> {
match polled {
VStream::Next(Ok(v)) => Poll::Ready(Ok(v)),
VStream::Next(Err(e)) => Poll::Ready(Err(io::Error::other(e.to_string()))),
_ => {
cx.waker().wake_by_ref();
Poll::Pending
}
}
}
fn poll_h3<T>(
mut f: impl FnMut() -> VStream<Result<T, H3Error>, ()>,
) -> impl Future<Output = io::Result<T>> {
std::future::poll_fn(move |cx: &mut Context<'_>| map_h3(cx, f()))
}
fn poll_shared<'a, T>(
shared: &'a Mutex<Req>,
mut f: impl FnMut(&mut Req) -> VStream<Result<T, H3Error>, ()> + 'a,
) -> impl Future<Output = io::Result<T>> + 'a {
std::future::poll_fn(move |cx: &mut Context<'_>| {
let polled = match shared.lock() {
Ok(mut req) => f(&mut req),
Err(_) => return Poll::Ready(Err(io::Error::other("h3 request lock poisoned"))),
};
map_h3(cx, polled)
})
}
enum CollectedBody {
Complete(Vec<u8>),
TooLarge { read: usize },
}
async fn collect_h3_body(req: &mut Req, max_bytes: usize) -> io::Result<CollectedBody> {
let mut body = Vec::new();
while let Some(chunk) = poll_h3(|| req.poll_body()).await? {
let read = body.len() + chunk.len();
if max_bytes > 0 && read > max_bytes {
return Ok(CollectedBody::TooLarge { read });
}
body.extend_from_slice(&chunk);
}
Ok(CollectedBody::Complete(body))
}
async fn send_h3_response(req: &mut Req, mut response: SimpleOutgoingResponse) -> io::Result<()> {
let response_fields = response_to_fields(&response);
let body = response.body.take();
let mut header_frame = Req::encode_headers(&response_fields);
poll_h3(|| req.poll_send_headers(&mut header_frame)).await?;
let stream = body
.filter(|b| !matches!(b, SendSafeBody::None))
.map(AsyncSendSafeBody::from);
if let Some(mut stream) = stream {
while let Some(chunk) = stream.next().await {
let chunk = chunk.map_err(|e| io::Error::other(e.to_string()))?;
let mut data_frame = Req::encode_data(Bytes::from(chunk));
poll_h3(|| req.poll_send_data(&mut data_frame)).await?;
}
}
poll_h3_finish(req).await
}
fn poll_h3_finish(req: &mut Req) -> impl Future<Output = io::Result<()>> + '_ {
poll_h3(move || req.poll_finish())
}