io-http 0.3.0

HTTP client library for Rust
Documentation
//! I/O-free coroutine reading and parsing an HTTP/1.X response head
//! ([RFC 9112 §6]). Composed by `Http10Send`, `Http11Send`, and the SSE
//! bootstrap.
//!
//! [RFC 9112 §6]: https://www.rfc-editor.org/rfc/rfc9112#section-6

use alloc::vec::Vec;

use httparse::{EMPTY_HEADER, Error as HttparseError, Response, Status};
use log::{debug, trace};
use thiserror::Error;

use crate::{
    coroutine::*,
    rfc1945::version::HTTP_10,
    rfc9110::{
        headers::HTTP_CONNECTION,
        response::{HttpResponse, HttpResponseBuilder},
        status::HttpStatusCode,
    },
    rfc9112::version::HTTP_11,
};

/// Failure causes during the HTTP/1.X read-headers flow.
#[derive(Debug, Error)]
pub enum Http11HeadersReadError {
    /// The stream reached EOF before the head was complete.
    #[error("HTTP/1.X read headers failed: reached unexpected EOF before headers were complete")]
    Eof,
    /// The response head could not be parsed.
    #[error("HTTP/1.X read headers failed: parse response headers: {0}")]
    ParseResponseHeaders(HttparseError),
}

/// Terminal output of [`Http11HeadersRead`]; the response body is
/// empty at this stage.
#[derive(Debug)]
pub struct Http11HeadersReadOutput {
    /// The parsed response, head only.
    pub response: HttpResponse,
    /// Bytes already buffered past the end of the head.
    pub remaining: Vec<u8>,
    /// Whether the server signalled the connection can be reused.
    pub keep_alive: bool,
}

/// I/O-free coroutine to read and parse an HTTP/1.X response head.
#[derive(Debug, Default)]
pub struct Http11HeadersRead {
    buf: Vec<u8>,
}

impl HttpCoroutine for Http11HeadersRead {
    type Yield = HttpYield;
    type Return = Result<Http11HeadersReadOutput, Http11HeadersReadError>;

    fn resume(&mut self, arg: Option<&[u8]>) -> HttpCoroutineState<Self::Yield, Self::Return> {
        match arg {
            Some(&[]) => {
                return HttpCoroutineState::Complete(Err(Http11HeadersReadError::Eof));
            }
            Some(data) => self.buf.extend_from_slice(data),
            None => {}
        }

        let mut headers = [EMPTY_HEADER; 64];
        let mut parsed = Response::new(&mut headers);

        let header_end = match parsed.parse(&self.buf) {
            Ok(Status::Complete(n)) => n,
            Ok(Status::Partial) => {
                trace!("received incomplete headers");
                return HttpCoroutineState::Yielded(HttpYield::WantsRead);
            }
            Err(err) => {
                return HttpCoroutineState::Complete(Err(
                    Http11HeadersReadError::ParseResponseHeaders(err),
                ));
            }
        };

        let mut builder = HttpResponseBuilder::default();
        let is_http10 = matches!(parsed.version, Some(0));
        builder.version = if is_http10 { HTTP_10 } else { HTTP_11 }.into();
        if let Some(code) = parsed.code {
            builder.status = Some(HttpStatusCode(code));
        }
        for header in parsed.headers.iter() {
            builder.header(header.name, header.value);
        }

        let keep_alive = match builder.get_header(HTTP_CONNECTION) {
            Some(conn) => !conn.eq_ignore_ascii_case("close"),
            None => !is_http10,
        };

        debug!("received complete headers");
        trace!("{builder:?}");

        let response = builder.build(Vec::new());
        let remaining = self.buf.split_off(header_end);

        HttpCoroutineState::Complete(Ok(Http11HeadersReadOutput {
            response,
            remaining,
            keep_alive,
        }))
    }
}

#[cfg(test)]
mod tests {
    use crate::{coroutine::*, rfc9112::read_headers::*};

    #[test]
    fn parses_complete_head() {
        let mut coroutine = Http11HeadersRead::default();
        let reply = b"HTTP/1.1 200 OK\r\nContent-Type: text/plain\r\n\r\nbody";

        let out = expect_complete_ok(&mut coroutine, Some(reply));
        assert_eq!(*out.response.status, 200);
        assert_eq!(out.response.version, "HTTP/1.1");
        assert_eq!(out.response.header("content-type"), Some("text/plain"));
        assert_eq!(out.remaining, b"body");
    }

    #[test]
    fn incomplete_head_wants_read() {
        let mut coroutine = Http11HeadersRead::default();
        expect_wants_read(&mut coroutine, Some(b"HTTP/1.1 200 OK\r\n"));
    }

    #[test]
    fn eof_returns_eof_error() {
        let mut coroutine = Http11HeadersRead::default();
        let err = expect_complete_err(&mut coroutine, Some(b""));
        assert!(matches!(err, Http11HeadersReadError::Eof));
    }

    #[test]
    fn http10_keep_alive_defaults_false() {
        let mut coroutine = Http11HeadersRead::default();
        let reply = b"HTTP/1.0 200 OK\r\n\r\n";
        let out = expect_complete_ok(&mut coroutine, Some(reply));
        assert!(!out.keep_alive);
        assert_eq!(out.response.version, "HTTP/1.0");
    }

    #[test]
    fn http11_keep_alive_defaults_true() {
        let mut coroutine = Http11HeadersRead::default();
        let reply = b"HTTP/1.1 200 OK\r\n\r\n";
        let out = expect_complete_ok(&mut coroutine, Some(reply));
        assert!(out.keep_alive);
    }

    #[test]
    fn connection_close_overrides_default() {
        let mut coroutine = Http11HeadersRead::default();
        let reply = b"HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n";
        let out = expect_complete_ok(&mut coroutine, Some(reply));
        assert!(!out.keep_alive);
    }

    fn expect_wants_read(cor: &mut Http11HeadersRead, arg: Option<&[u8]>) {
        match cor.resume(arg) {
            HttpCoroutineState::Yielded(HttpYield::WantsRead) => {}
            state => panic!("expected WantsRead, got {state:?}"),
        }
    }

    fn expect_complete_ok(
        cor: &mut Http11HeadersRead,
        arg: Option<&[u8]>,
    ) -> Http11HeadersReadOutput {
        match cor.resume(arg) {
            HttpCoroutineState::Complete(Ok(out)) => out,
            state => panic!("expected Complete(Ok), got {state:?}"),
        }
    }

    fn expect_complete_err(
        cor: &mut Http11HeadersRead,
        arg: Option<&[u8]>,
    ) -> Http11HeadersReadError {
        match cor.resume(arg) {
            HttpCoroutineState::Complete(Err(err)) => err,
            state => panic!("expected Complete(Err), got {state:?}"),
        }
    }
}