base-http1 0.2.0

Native HTTP/1 framing over Base scheduler stream capabilities
Documentation
// Released under the MIT License.
// Copyright, 2026, by Samuel Williams.

use base_scheduler::{Cancelled, OperationError};
use std::{fmt, io};

/// An HTTP/1 failure. Variants wrapping a cause describe this layer and report the cause as their source.
#[derive(Debug)]
pub enum Error {
    Io(io::Error),
    Cancelled,
    Invalid(&'static str),
    Limit(&'static str),
    Truncated,
    LengthMismatch,
    Body(Box<dyn std::error::Error + Send + Sync>),
}

impl fmt::Display for Error {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Self::Io(_) => f.write_str("HTTP/1 connection I/O failed"),
            Self::Cancelled => f.write_str("HTTP exchange cancelled"),
            Self::Invalid(reason) => write!(f, "invalid HTTP/1 message: {reason}"),
            Self::Limit(reason) => write!(f, "HTTP/1 limit exceeded: {reason}"),
            Self::Truncated => f.write_str("truncated HTTP/1 message"),
            Self::LengthMismatch => f.write_str("body does not match its declared length"),
            Self::Body(_) => f.write_str("HTTP/1 body failed"),
        }
    }
}
impl std::error::Error for Error {
    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
        match self {
            Self::Io(error) => Some(error),
            Self::Body(error) => Some(error.as_ref()),
            _ => None,
        }
    }
}
impl From<io::Error> for Error {
    fn from(error: io::Error) -> Self {
        Self::Io(error)
    }
}
impl From<Cancelled> for Error {
    fn from(_: Cancelled) -> Self {
        Self::Cancelled
    }
}
impl From<OperationError> for Error {
    fn from(error: OperationError) -> Self {
        match error {
            OperationError::Cancelled(_) => Self::Cancelled,
            OperationError::Io(error) => Self::Io(error),
        }
    }
}
impl From<base_http::FieldError> for Error {
    fn from(error: base_http::FieldError) -> Self {
        Self::Body(Box::new(error))
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use std::error::Error as _;

    #[test]
    fn wrapping_variants_describe_their_own_layer() {
        let error = Error::Io(io::Error::from(io::ErrorKind::ConnectionRefused));
        assert_eq!(error.to_string(), "HTTP/1 connection I/O failed");
        assert_eq!(error.source().unwrap().to_string(), "connection refused");

        let error = Error::from(base_http::FieldError::InvalidTrailer("host".to_owned()));
        assert_eq!(error.to_string(), "HTTP/1 body failed");
        assert!(error.source().is_some());
    }

    #[test]
    fn leaf_variants_describe_the_failure_without_a_source() {
        for (error, message) in [
            (Error::Cancelled, "HTTP exchange cancelled"),
            (
                Error::Invalid("missing host"),
                "invalid HTTP/1 message: missing host",
            ),
            (
                Error::Limit("message head"),
                "HTTP/1 limit exceeded: message head",
            ),
            (Error::Truncated, "truncated HTTP/1 message"),
            (
                Error::LengthMismatch,
                "body does not match its declared length",
            ),
        ] {
            assert_eq!(error.to_string(), message);
            assert!(error.source().is_none());
        }
    }
}