use std::{collections::HashMap, io, task::Waker};
pub(crate) mod headers;
pub(crate) mod read;
pub(crate) mod write;
pub(crate) const H3_NO_ERROR: u64 = 0x0100;
pub(crate) const H3_INTERNAL_ERROR: u64 = 0x0102;
pub(crate) const H3_REQUEST_CANCELLED: u64 = 0x010C;
#[derive(Debug)]
pub enum H3Error {
Reset(u64),
ConnectionClosed,
H3(squiche::h3::Error),
}
impl std::fmt::Display for H3Error {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
H3Error::Reset(code) => write!(f, "stream reset by peer (code {code:#x})"),
H3Error::ConnectionClosed => write!(f, "connection closed"),
H3Error::H3(err) => write!(f, "h3 error: {err}"),
}
}
}
impl std::error::Error for H3Error {}
impl From<H3Error> for io::Error {
fn from(err: H3Error) -> io::Error {
match err {
H3Error::Reset(_) => io::Error::new(io::ErrorKind::ConnectionReset, err.to_string()),
H3Error::ConnectionClosed => {
io::Error::new(io::ErrorKind::NotConnected, err.to_string())
}
H3Error::H3(_) => io::Error::other(err.to_string()),
}
}
}
pub(crate) fn send_error(err: squiche::h3::Error, read_state: Option<&ReadState>) -> H3Error {
match err {
squiche::h3::Error::TransportError(squiche::Error::StreamStopped(code))
| squiche::h3::Error::TransportError(squiche::Error::StreamReset(code)) => {
H3Error::Reset(code)
}
squiche::h3::Error::TransportError(squiche::Error::InvalidStreamState(_)) => {
match read_state {
Some(ReadState::Reset(code)) => H3Error::Reset(*code),
Some(ReadState::Closed) => H3Error::ConnectionClosed,
_ => H3Error::H3(err),
}
}
err => H3Error::H3(err),
}
}
pub(crate) trait H3App {
fn h3_streams(
&mut self,
) -> (
Option<&mut squiche::h3::Connection>,
&mut HashMap<u64, StreamState>,
);
}
#[derive(Default)]
pub(crate) struct StreamState {
pub(crate) read_state: ReadState,
pub(crate) read_waker: Option<Waker>,
pub(crate) write_waker: Option<Waker>,
}
#[derive(Default)]
pub(crate) enum ReadState {
#[default]
Streaming,
Trailers(http::HeaderMap),
Eof,
Reset(u64),
Closed,
}
pub(crate) fn is_terminated(conn: &squiche::Connection) -> bool {
conn.is_closed()
|| conn.is_draining()
|| conn.local_error().is_some()
|| conn.peer_error().is_some()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_stopped_or_reset_stream_is_a_peer_reset() {
for err in [
squiche::Error::StreamStopped(0x10c),
squiche::Error::StreamReset(0x10c),
] {
let mapped = send_error(squiche::h3::Error::TransportError(err), None);
assert!(matches!(mapped, H3Error::Reset(0x10c)), "{mapped}");
assert_eq!(
io::Error::from(mapped).kind(),
io::ErrorKind::ConnectionReset
);
}
let other = send_error(
squiche::h3::Error::TransportError(squiche::Error::StreamLimit),
None,
);
assert!(matches!(other, H3Error::H3(_)), "{other}");
assert_eq!(io::Error::from(other).kind(), io::ErrorKind::Other);
}
#[test]
fn a_forgotten_stream_takes_its_verdict_from_the_read_state() {
let gone = || squiche::h3::Error::TransportError(squiche::Error::InvalidStreamState(0));
assert!(matches!(
send_error(gone(), Some(&ReadState::Reset(0x10c))),
H3Error::Reset(0x10c)
));
assert!(matches!(
send_error(gone(), Some(&ReadState::Closed)),
H3Error::ConnectionClosed
));
assert!(matches!(
send_error(gone(), Some(&ReadState::Eof)),
H3Error::H3(_)
));
assert!(matches!(send_error(gone(), None), H3Error::H3(_)));
}
}