use std::collections::{HashMap, VecDeque};
use std::net::SocketAddr;
use bytes::{Bytes, BytesMut};
use ringline::{ConnCtx, ParseResult};
use ringline_h2::hpack::HeaderField;
use ringline_h2::settings::Settings;
use ringline_h2::{H2Connection, H2Event};
use crate::error::HttpError;
use crate::response::Response;
struct PendingStream {
status: Option<u16>,
headers: Vec<(String, String)>,
body: BytesMut,
done: bool,
error: Option<HttpError>,
streaming: bool,
chunks: VecDeque<Bytes>,
content_encoding: Option<String>,
header_bytes: usize,
body_bytes: usize,
}
impl PendingStream {
fn new() -> Self {
Self {
status: None,
headers: Vec::new(),
body: BytesMut::new(),
done: false,
error: None,
streaming: false,
chunks: VecDeque::new(),
content_encoding: None,
header_bytes: 0,
body_bytes: 0,
}
}
fn fail(&mut self, err: HttpError) {
if self.error.is_none() {
self.error = Some(err);
}
self.done = true;
}
#[cfg_attr(
not(any(feature = "gzip", feature = "zstd", feature = "brotli")),
allow(unused_variables)
)]
fn into_response(self, max_decompressed_size: usize) -> Result<Response, HttpError> {
if let Some(err) = self.error {
return Err(err);
}
let status = self
.status
.ok_or_else(|| HttpError::InvalidMessage("response missing :status".into()))?;
if let Some(ref encoding) = self.content_encoding
&& !is_identity_encoding(encoding)
{
#[cfg(any(feature = "gzip", feature = "zstd", feature = "brotli"))]
{
let decompressed =
crate::compress::decompress(encoding, &self.body, max_decompressed_size)?;
return Ok(Response::new(
status,
self.headers,
Bytes::from(decompressed),
));
}
#[cfg(not(any(feature = "gzip", feature = "zstd", feature = "brotli")))]
{
return Err(HttpError::InvalidMessage(format!(
"unsupported content-encoding `{encoding}` (no decompressor compiled in)"
)));
}
}
Ok(Response::new(status, self.headers, self.body.freeze()))
}
}
fn is_identity_encoding(encoding: &str) -> bool {
let e = encoding.trim();
e.is_empty() || e.eq_ignore_ascii_case("identity")
}
fn parse_status(value: &[u8]) -> Option<u16> {
if value.len() != 3 {
return None;
}
if !value.iter().all(|b| b.is_ascii_digit()) {
return None;
}
let s = std::str::from_utf8(value).ok()?;
let n: u16 = s.parse().ok()?;
if (100..=999).contains(&n) {
Some(n)
} else {
None
}
}
fn handle_response_headers(
ps: &mut PendingStream,
headers: &[HeaderField],
end_stream: bool,
max_header_section: usize,
) {
for h in headers {
if h.name == b":status" {
match parse_status(&h.value) {
Some(n) => ps.status = Some(n),
None => {
ps.fail(HttpError::InvalidMessage(format!(
"invalid :status `{}`",
String::from_utf8_lossy(&h.value)
)));
return;
}
}
continue;
}
let add = h.name.len().saturating_add(h.value.len());
ps.header_bytes = ps.header_bytes.saturating_add(add);
if ps.header_bytes > max_header_section {
ps.fail(HttpError::MaxSizeExceeded(format!(
"response header section exceeds {max_header_section} bytes"
)));
return;
}
let name = String::from_utf8_lossy(&h.name).into_owned();
let value = String::from_utf8_lossy(&h.value).into_owned();
if name.eq_ignore_ascii_case("content-encoding") {
ps.content_encoding = Some(value.clone());
}
ps.headers.push((name, value));
}
if end_stream {
if ps.status.is_none() && ps.error.is_none() {
ps.fail(HttpError::InvalidMessage(
"response HEADERS missing :status".into(),
));
} else {
ps.done = true;
}
}
}
fn handle_response_data(
ps: &mut PendingStream,
payload: Vec<u8>,
end_stream: bool,
max_body_size: usize,
) {
let new_total = ps.body_bytes.saturating_add(payload.len());
if new_total > max_body_size {
ps.fail(HttpError::MaxSizeExceeded(format!(
"response body exceeds {max_body_size} bytes"
)));
return;
}
ps.body_bytes = new_total;
if ps.streaming {
ps.chunks.push_back(Bytes::from(payload));
} else {
ps.body.extend_from_slice(&payload);
}
if end_stream {
ps.done = true;
}
}
struct BlockedSend {
stream_id: u32,
data: Vec<u8>,
end_stream: bool,
}
pub struct H2AsyncConn {
conn: ConnCtx,
h2: H2Connection,
pending_streams: HashMap<u32, PendingStream>,
blocked_sends: VecDeque<BlockedSend>,
completed: VecDeque<(u32, Result<Response, HttpError>)>,
settings_acked: bool,
max_decompressed_size: usize,
max_header_section: usize,
max_body_size: usize,
goaway_received: bool,
}
impl H2AsyncConn {
pub fn set_max_decompressed_size(&mut self, n: usize) {
self.max_decompressed_size = n;
}
pub fn set_max_header_section(&mut self, n: usize) {
self.max_header_section = n;
}
pub fn set_max_body_size(&mut self, n: usize) {
self.max_body_size = n;
}
pub fn peer_will_close(&self) -> bool {
self.goaway_received
}
}
impl H2AsyncConn {
pub async fn connect(addr: SocketAddr, host: &str) -> Result<Self, HttpError> {
let conn = ringline::connect_tls(addr, host)?.await?;
Self::from_conn(conn).await
}
pub async fn connect_with_timeout(
addr: SocketAddr,
host: &str,
timeout_ms: u64,
) -> Result<Self, HttpError> {
let conn = ringline::connect_tls_with_timeout(addr, host, timeout_ms)?.await?;
Self::from_conn(conn).await
}
pub async fn from_conn(conn: ConnCtx) -> Result<Self, HttpError> {
let h2 = H2Connection::new(Settings::client_default());
let mut this = Self {
conn,
h2,
pending_streams: HashMap::new(),
blocked_sends: VecDeque::new(),
completed: VecDeque::new(),
settings_acked: false,
max_decompressed_size: crate::compress::DEFAULT_MAX_DECOMPRESSED_SIZE,
max_header_section: crate::h1_conn::DEFAULT_MAX_HEADER_SECTION,
max_body_size: crate::h1_conn::DEFAULT_MAX_BODY_SIZE,
goaway_received: false,
};
this.flush_pending_send()?;
while !this.settings_acked {
this.pump_once().await?;
}
Ok(this)
}
pub fn close(&self) {
self.conn.close();
}
pub fn conn(&self) -> ConnCtx {
self.conn
}
pub fn pending_count(&self) -> usize {
self.pending_streams.len()
}
pub async fn send_request(
&mut self,
method: &str,
path: &str,
host: &str,
extra_headers: &[(&str, &str)],
body: Option<&[u8]>,
) -> Result<Response, HttpError> {
let stream_id = self.fire_request(method, path, host, extra_headers, body)?;
self.recv_stream(stream_id).await
}
pub fn fire_request(
&mut self,
method: &str,
path: &str,
host: &str,
extra_headers: &[(&str, &str)],
body: Option<&[u8]>,
) -> Result<u32, HttpError> {
let has_body = body.is_some_and(|b| !b.is_empty());
let mut headers = vec![
HeaderField::new(b":method", method.as_bytes()),
HeaderField::new(b":path", path.as_bytes()),
HeaderField::new(b":scheme", b"https"),
HeaderField::new(b":authority", host.as_bytes()),
];
let has_accept_encoding = extra_headers
.iter()
.any(|(name, _)| name.eq_ignore_ascii_case("accept-encoding"));
for (name, value) in extra_headers {
headers.push(HeaderField::new(name.as_bytes(), value.as_bytes()));
}
if !has_accept_encoding && let Some(ae) = crate::compress::accept_encoding_value() {
headers.push(HeaderField::new(b"accept-encoding", ae.as_bytes()));
}
let end_stream = !has_body;
let stream_id = self.h2.send_request(&headers, end_stream)?;
if let Some(data) = body
&& !data.is_empty()
&& let Err(e) = self.h2.send_data(stream_id, data, true)
{
if matches!(e, ringline_h2::H2Error::FlowControlError) {
self.blocked_sends.push_back(BlockedSend {
stream_id,
data: data.to_vec(),
end_stream: true,
});
} else {
return Err(HttpError::H2(e));
}
}
self.pending_streams.insert(stream_id, PendingStream::new());
self.flush_pending_send()?;
Ok(stream_id)
}
pub async fn recv(&mut self) -> Result<(u32, Response), HttpError> {
if let Some((id, result)) = self.completed.pop_front() {
return result.map(|r| (id, r));
}
loop {
self.pump_once().await?;
if let Some((id, result)) = self.completed.pop_front() {
return result.map(|r| (id, r));
}
}
}
pub async fn recv_stream(&mut self, stream_id: u32) -> Result<Response, HttpError> {
if let Some(idx) = self.completed.iter().position(|(sid, _)| *sid == stream_id) {
let (_, result) = self.completed.remove(idx).unwrap();
return result;
}
loop {
self.pump_once().await?;
if let Some(idx) = self.completed.iter().position(|(sid, _)| *sid == stream_id) {
let (_, result) = self.completed.remove(idx).unwrap();
return result;
}
}
}
async fn pump_once(&mut self) -> Result<(), HttpError> {
self.flush_pending_send()?;
let h2 = &mut self.h2;
let pending = &mut self.pending_streams;
let blocked = &mut self.blocked_sends;
let settings_acked = &mut self.settings_acked;
let goaway_received = &mut self.goaway_received;
let max_header_section = self.max_header_section;
let max_body_size = self.max_body_size;
let mut connection_error: Option<HttpError> = None;
let n = self
.conn
.with_data(|data| {
if let Err(e) = h2.recv(data) {
connection_error = Some(HttpError::H2(e));
return ParseResult::Consumed(data.len());
}
while let Some(event) = h2.poll_event() {
match event {
H2Event::SettingsAcknowledged => {
*settings_acked = true;
}
H2Event::Response {
stream_id,
headers,
end_stream,
} => {
if let Some(ps) = pending.get_mut(&stream_id) {
handle_response_headers(
ps,
&headers,
end_stream,
max_header_section,
);
}
}
H2Event::Data {
stream_id,
data: payload,
end_stream,
} => {
if let Some(ps) = pending.get_mut(&stream_id) {
handle_response_data(ps, payload, end_stream, max_body_size);
}
}
H2Event::Trailers { stream_id, .. } => {
if let Some(ps) = pending.get_mut(&stream_id) {
ps.done = true;
}
}
H2Event::StreamReset {
stream_id,
error_code,
} => {
if let Some(ps) = pending.get_mut(&stream_id) {
ps.fail(HttpError::H2(ringline_h2::H2Error::StreamError(
stream_id, error_code,
)));
}
}
H2Event::GoAway { .. } => {
*goaway_received = true;
for ps in pending.values_mut() {
ps.done = true;
}
}
H2Event::Error(e) => {
connection_error = Some(HttpError::H2(e));
}
H2Event::PingAcknowledged { .. } => {}
}
}
let mut retry = VecDeque::new();
std::mem::swap(blocked, &mut retry);
for bs in retry {
match h2.send_data(bs.stream_id, &bs.data, bs.end_stream) {
Ok(()) => {}
Err(ringline_h2::H2Error::FlowControlError) => {
blocked.push_back(bs);
}
Err(e) => {
if let Some(ps) = pending.get_mut(&bs.stream_id) {
ps.fail(HttpError::H2(e));
}
}
}
}
ParseResult::Consumed(data.len())
})
.await;
if let Some(e) = connection_error {
return Err(e);
}
if n == 0 {
return Err(HttpError::ConnectionClosed);
}
let done_ids: Vec<u32> = self
.pending_streams
.iter()
.filter(|(_, ps)| ps.done && !ps.streaming)
.map(|(id, _)| *id)
.collect();
for id in done_ids {
if let Some(ps) = self.pending_streams.remove(&id) {
let result = ps.into_response(self.max_decompressed_size);
self.completed.push_back((id, result));
}
}
self.flush_pending_send()?;
Ok(())
}
pub async fn send_request_streaming(
&mut self,
method: &str,
path: &str,
host: &str,
extra_headers: &[(&str, &str)],
body: Option<&[u8]>,
) -> Result<H2StreamingResponse<'_>, HttpError> {
let stream_id = self.fire_request(method, path, host, extra_headers, body)?;
if let Some(ps) = self.pending_streams.get_mut(&stream_id) {
ps.streaming = true;
}
loop {
if let Some(ps) = self.pending_streams.get_mut(&stream_id) {
if let Some(err) = ps.error.take() {
self.pending_streams.remove(&stream_id);
return Err(err);
}
if ps.status.is_some() {
break;
}
} else {
return Err(HttpError::Protocol("stream vanished".into()));
}
self.pump_once().await?;
}
Ok(H2StreamingResponse {
conn: self,
stream_id,
})
}
fn flush_pending_send(&mut self) -> Result<(), HttpError> {
let pending = self.h2.take_pending_send();
if !pending.is_empty() {
self.conn.send_nowait(&pending)?;
}
Ok(())
}
}
pub struct H2StreamingResponse<'a> {
conn: &'a mut H2AsyncConn,
stream_id: u32,
}
impl<'a> H2StreamingResponse<'a> {
pub fn status(&self) -> u16 {
self.conn
.pending_streams
.get(&self.stream_id)
.and_then(|ps| ps.status)
.unwrap_or(0)
}
pub fn headers(&self) -> &[(String, String)] {
self.conn
.pending_streams
.get(&self.stream_id)
.map(|ps| ps.headers.as_slice())
.unwrap_or(&[])
}
pub fn header(&self, name: &str) -> Option<&str> {
let lower = name.to_ascii_lowercase();
self.headers()
.iter()
.find(|(k, _)| k.to_ascii_lowercase() == lower)
.map(|(_, v)| v.as_str())
}
pub async fn next_chunk(&mut self) -> Result<Option<Bytes>, HttpError> {
loop {
if let Some(ps) = self.conn.pending_streams.get_mut(&self.stream_id) {
if let Some(err) = ps.error.take() {
self.conn.pending_streams.remove(&self.stream_id);
return Err(err);
}
if let Some(chunk) = ps.chunks.pop_front() {
return Ok(Some(chunk));
}
if ps.done {
self.conn.pending_streams.remove(&self.stream_id);
return Ok(None);
}
} else {
return Ok(None);
}
self.conn.pump_once().await?;
}
}
}
impl Drop for H2StreamingResponse<'_> {
fn drop(&mut self) {
self.conn.pending_streams.remove(&self.stream_id);
}
}
#[cfg(test)]
mod tests {
use super::*;
fn header(name: &[u8], value: &[u8]) -> HeaderField {
HeaderField::new(name, value)
}
#[test]
fn parse_status_accepts_three_digit_codes() {
assert_eq!(parse_status(b"200"), Some(200));
assert_eq!(parse_status(b"404"), Some(404));
assert_eq!(parse_status(b"100"), Some(100));
assert_eq!(parse_status(b"599"), Some(599));
assert_eq!(parse_status(b"999"), Some(999));
}
#[test]
fn parse_status_rejects_wrong_length() {
assert_eq!(parse_status(b""), None);
assert_eq!(parse_status(b"2"), None);
assert_eq!(parse_status(b"20"), None);
assert_eq!(parse_status(b"2000"), None);
}
#[test]
fn parse_status_rejects_non_digits() {
assert_eq!(parse_status(b"abc"), None);
assert_eq!(parse_status(b"2x0"), None);
assert_eq!(parse_status(b"-10"), None);
}
#[test]
fn parse_status_rejects_out_of_range() {
assert_eq!(parse_status(b"099"), None);
}
#[test]
fn invalid_status_fails_stream() {
let mut ps = PendingStream::new();
let headers = vec![header(b":status", b"oops")];
handle_response_headers(&mut ps, &headers, true, 64 * 1024);
assert!(ps.error.is_some());
match ps.error.unwrap() {
HttpError::InvalidMessage(_) => {}
other => panic!("expected InvalidMessage, got {other:?}"),
}
}
#[test]
fn missing_status_with_end_stream_fails() {
let mut ps = PendingStream::new();
let headers = vec![header(b"content-type", b"text/plain")];
handle_response_headers(&mut ps, &headers, true, 64 * 1024);
assert!(matches!(ps.error, Some(HttpError::InvalidMessage(_))));
}
#[test]
fn oversize_header_section_fails_stream() {
let mut ps = PendingStream::new();
let big_value = vec![b'x'; 200];
let headers = vec![header(b":status", b"200"), header(b"x", &big_value)];
handle_response_headers(&mut ps, &headers, false, 100);
assert!(matches!(ps.error, Some(HttpError::MaxSizeExceeded(_))));
}
#[test]
fn valid_status_accepts_and_collects_headers() {
let mut ps = PendingStream::new();
let headers = vec![header(b":status", b"204"), header(b"x-custom", b"value")];
handle_response_headers(&mut ps, &headers, true, 64 * 1024);
assert_eq!(ps.status, Some(204));
assert!(ps.error.is_none());
assert!(ps.done);
assert_eq!(
ps.headers,
vec![("x-custom".to_string(), "value".to_string())]
);
}
#[test]
fn oversize_body_fails_stream() {
let mut ps = PendingStream::new();
let payload = vec![0u8; 200];
handle_response_data(&mut ps, payload, false, 100);
assert!(matches!(ps.error, Some(HttpError::MaxSizeExceeded(_))));
}
#[test]
fn body_within_cap_accumulates() {
let mut ps = PendingStream::new();
handle_response_data(&mut ps, vec![1, 2, 3], false, 100);
handle_response_data(&mut ps, vec![4, 5], true, 100);
assert!(ps.error.is_none());
assert!(ps.done);
assert_eq!(&ps.body[..], &[1, 2, 3, 4, 5]);
}
#[test]
fn into_response_surfaces_stream_error() {
let mut ps = PendingStream::new();
ps.fail(HttpError::InvalidMessage("test".into()));
let res = ps.into_response(usize::MAX);
assert!(matches!(res, Err(HttpError::InvalidMessage(_))));
}
#[cfg(not(any(feature = "gzip", feature = "zstd", feature = "brotli")))]
#[test]
fn into_response_rejects_unsupported_content_encoding_without_features() {
let mut ps = PendingStream::new();
ps.status = Some(200);
ps.content_encoding = Some("gzip".into());
ps.body.extend_from_slice(&[1, 2, 3]);
let res = ps.into_response(usize::MAX);
match res {
Err(HttpError::InvalidMessage(msg)) => assert!(msg.contains("gzip")),
other => panic!("expected InvalidMessage for gzip without features, got {other:?}"),
}
}
#[test]
fn into_response_identity_encoding_returns_body_as_is() {
let mut ps = PendingStream::new();
ps.status = Some(200);
ps.content_encoding = Some("identity".into());
ps.body.extend_from_slice(&[1, 2, 3]);
let res = ps.into_response(usize::MAX);
let resp = res.expect("identity encoding must not error");
assert_eq!(resp.status(), 200);
assert_eq!(resp.bytes().as_ref(), &[1, 2, 3]);
}
#[test]
fn is_identity_encoding_recognises_variants() {
assert!(is_identity_encoding(""));
assert!(is_identity_encoding("identity"));
assert!(is_identity_encoding("Identity"));
assert!(is_identity_encoding(" identity "));
assert!(!is_identity_encoding("gzip"));
assert!(!is_identity_encoding("br"));
}
}