use hyper_util::rt::TokioIo;
use crate::{
http::{server::stack::StackConfig, transport::io::IrohStream},
Body, IrohEndpoint, ALPN,
};
#[derive(Debug)]
#[non_exhaustive]
pub enum FetchError {
ConnectionFailed {
detail: String,
source: Option<hyper::Error>,
},
RequestBodyFailed {
detail: String,
source: hyper::Error,
},
HeaderTooLarge { detail: String },
BodyTooLarge,
Timeout,
Cancelled,
Internal(String),
}
impl std::fmt::Display for FetchError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
FetchError::ConnectionFailed { detail, .. } => {
write!(f, "connection failed: {detail}")
}
FetchError::RequestBodyFailed { detail, .. } => {
write!(f, "request body failed: {detail}")
}
FetchError::HeaderTooLarge { detail } => {
write!(f, "response header too large: {detail}")
}
FetchError::BodyTooLarge => f.write_str("response body too large"),
FetchError::Timeout => f.write_str("request timed out"),
FetchError::Cancelled => f.write_str("request cancelled"),
FetchError::Internal(msg) => write!(f, "internal error: {msg}"),
}
}
}
impl std::error::Error for FetchError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
FetchError::ConnectionFailed {
source: Some(s), ..
} => Some(s),
FetchError::RequestBodyFailed { source, .. } => Some(source),
_ => None,
}
}
}
#[derive(Debug)]
struct CallerRequestBodyError {
source: crate::BoxError,
}
impl std::fmt::Display for CallerRequestBodyError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "caller request body failed: {}", self.source)
}
}
impl std::error::Error for CallerRequestBodyError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
Some(self.source.as_ref())
}
}
struct MarkCallerRequestBody {
body: Body,
mask_zero_data_hint: bool,
}
impl http_body::Body for MarkCallerRequestBody {
type Data = bytes::Bytes;
type Error = CallerRequestBodyError;
fn poll_frame(
self: std::pin::Pin<&mut Self>,
cx: &mut std::task::Context<'_>,
) -> std::task::Poll<Option<Result<http_body::Frame<Self::Data>, Self::Error>>> {
let this = self.get_mut();
match std::pin::Pin::new(&mut this.body).poll_frame(cx) {
std::task::Poll::Ready(Some(Err(source))) => {
std::task::Poll::Ready(Some(Err(CallerRequestBodyError { source })))
}
std::task::Poll::Ready(Some(Ok(frame))) => std::task::Poll::Ready(Some(Ok(frame))),
std::task::Poll::Ready(None) => std::task::Poll::Ready(None),
std::task::Poll::Pending => std::task::Poll::Pending,
}
}
fn is_end_stream(&self) -> bool {
self.body.is_end_stream()
}
fn size_hint(&self) -> http_body::SizeHint {
if self.mask_zero_data_hint {
http_body::SizeHint::default()
} else {
self.body.size_hint()
}
}
}
fn caller_request_body_error_detail(error: &hyper::Error) -> Option<String> {
let mut current: Option<&(dyn std::error::Error + 'static)> = Some(error);
while let Some(source) = current {
if let Some(body_error) = source.downcast_ref::<CallerRequestBodyError>() {
return Some(body_error.source.to_string());
}
current = source.source();
}
None
}
pub async fn fetch_request(
endpoint: &IrohEndpoint,
addr: &iroh::EndpointAddr,
req: hyper::Request<Body>,
cfg: &StackConfig,
) -> Result<hyper::Response<Body>, FetchError> {
let node_id = addr.id;
let last_dial_seq: std::sync::Mutex<Option<u64>> = std::sync::Mutex::new(None);
let (parts, body) = req.into_parts();
let body_exact_size = http_body::Body::size_hint(&body).exact();
let body_at_end = http_body::Body::is_end_stream(&body);
let body_resendable = body_at_end && body_exact_size == Some(0);
let body = if body_at_end {
body
} else {
Body::new(MarkCallerRequestBody {
body,
mask_zero_data_hint: body_exact_size == Some(0),
})
};
let method = parts.method.clone();
let retry_safe = body_resendable && method.is_idempotent();
let uri = parts.uri.clone();
let version = parts.version;
let headers = parts.headers.clone();
let build_retry = |body: Body| -> hyper::Request<Body> {
let mut builder = hyper::Request::builder()
.method(method.clone())
.uri(uri.clone())
.version(version);
if let Some(h) = builder.headers_mut() {
*h = headers.clone();
}
builder
.body(body)
.expect("rebuild request from valid parts")
};
let first_req = hyper::Request::from_parts(parts, body);
let retry_seq = async {
let (result, reused) =
attempt_send(endpoint, addr, node_id, first_req, cfg, &last_dial_seq).await;
let first_err = match result {
Ok(resp) => return Ok(resp),
Err(e) => e,
};
let retryable = matches!(&first_err, FetchError::ConnectionFailed { .. });
if retryable {
let failed_seq = *last_dial_seq.lock().unwrap_or_else(|e| e.into_inner());
endpoint
.pool()
.evict_and_close(node_id, ALPN, b"iroh-http fetch failed", failed_seq)
.await;
}
if retryable && reused && retry_safe {
let (retry_result, _) = attempt_send(
endpoint,
addr,
node_id,
build_retry(Body::empty()),
cfg,
&last_dial_seq,
)
.await;
if let Err(FetchError::ConnectionFailed { .. }) = &retry_result {
let failed_seq = *last_dial_seq.lock().unwrap_or_else(|e| e.into_inner());
endpoint
.pool()
.evict_and_close(node_id, ALPN, b"iroh-http fetch retry failed", failed_seq)
.await;
}
return retry_result;
}
Err(first_err)
};
match cfg.timeout {
Some(t) => match tokio::time::timeout(t, retry_seq).await {
Ok(r) => r,
Err(_) => {
let failed_seq = *last_dial_seq.lock().unwrap_or_else(|e| e.into_inner());
endpoint.pool().evict_only(node_id, ALPN, failed_seq).await;
Err(FetchError::Timeout)
}
},
None => retry_seq.await,
}
}
async fn attempt_send(
endpoint: &IrohEndpoint,
addr: &iroh::EndpointAddr,
node_id: iroh::PublicKey,
req: hyper::Request<Body>,
cfg: &StackConfig,
last_dial_seq: &std::sync::Mutex<Option<u64>>,
) -> (Result<hyper::Response<Body>, FetchError>, bool) {
let ep_raw = endpoint.raw().clone();
let addr_clone = addr.clone();
let max_header_size = endpoint.max_header_size();
let (pooled, reused) = match endpoint
.pool()
.get_or_connect(node_id, ALPN, || async move {
ep_raw
.connect(addr_clone, ALPN)
.await
.map_err(|e| format!("connect: {e}"))
})
.await
{
Ok(v) => v,
Err(e) => {
*last_dial_seq.lock().unwrap_or_else(|e| e.into_inner()) = None;
return (
Err(FetchError::ConnectionFailed {
detail: e,
source: None,
}),
false,
);
}
};
*last_dial_seq.lock().unwrap_or_else(|e| e.into_inner()) = Some(pooled.dial_seq);
let conn = pooled.conn.clone();
let (send, recv) = match conn.open_bi().await {
Ok(pair) => pair,
Err(e) => {
return (
Err(FetchError::ConnectionFailed {
detail: format!("open_bi: {e}"),
source: None,
}),
reused,
)
}
};
let io = TokioIo::new(IrohStream::new(send, recv));
let (sender, conn_task) = match hyper::client::conn::http1::Builder::new()
.max_buf_size(max_header_size.max(8192))
.max_headers(128)
.handshake::<_, Body>(io)
.await
{
Ok(v) => v,
Err(e) => {
return (
Err(FetchError::ConnectionFailed {
detail: format!("hyper handshake: {e}"),
source: Some(e),
}),
reused,
)
}
};
tokio::spawn(conn_task);
use tower::ServiceExt;
let svc = crate::http::server::stack::build_client_stack(sender, cfg);
let result = svc.oneshot(req).await.map_err(|error| {
if let Some(detail) = caller_request_body_error_detail(&error) {
FetchError::RequestBodyFailed {
detail,
source: error,
}
} else {
FetchError::ConnectionFailed {
detail: format!("send_request: {error}"),
source: Some(error),
}
}
});
(result, reused)
}