Repository: aws/s2n-quic
Pull request #2629: deps: h3-0.0.8 (s2n-quic-h3)
Author: login: junkurihara
Created: 2025-05-07T09:57:17Z | Merged: 2025-05-16T17:36:01Z

### Release Summary:
<!-- If this is a feature or bug that impacts customers and is significant enough to include in the "Summary" section of the next version release, please include a brief (1-2 sentences) description of the change. The audience of this summary is customers, not maintainers or reviewers. See https://github.com/aws/s2n-tls/releases/tag/v1.5.7 for an example. Otherwise, leave this section blank -->

This change supports h3 v0.0.8 that has breaking changes on its API. This was implemented by referring to h3-quinn v0.10.0, specifically by following the commit [efdc2d8bfda0d86c0efdd36867ee6183f92d54d6](https://github.com/hyperium/h3/commit/efdc2d8bfda0d86c0efdd36867ee6183f92d54d6).

### Resolved issues:

resolves #2628 (dependabot PR)

### Description of changes: 

<!-- Describe s2n-quic’s current behavior and how your code changes that behavior. If there are no issues this pr is resolving, explain why this change is necessary. If a callout is specific to a section of code, it might make more sense to leave a comment on your own PR file diff. -->

This change updates `quic/s2n-quic-h3/src/s2n_quic.rs` as the upstream `h3` traits changes the manner of error handling.

### Call-outs:

<!--Address any potentially confusing code. Is there code added that needs to be cleaned up later? Is there code that is missing because it’s still in development?-->

I believe the change does not need to be updated at this point since it just follows the h3's API changes.

However, I am not sure if changes [L53-L58](https://github.com/aws/s2n-quic/compare/main...junkurihara:s2n-quic:deps/h3-0.0.8#diff-327d115c00ee24f279e4a904977a907c658451a5d618a3acd908d3e98e8c3c8bR53-R58) and [L74-L79](https://github.com/aws/s2n-quic/compare/main...junkurihara:s2n-quic:deps/h3-0.0.8#diff-327d115c00ee24f279e4a904977a907c658451a5d618a3acd908d3e98e8c3c8bR74-R79) (cases when a connection closes with no error) are suitable with underlying s2n-quic layer.

### Testing:

<!--How is this change tested (unit tests, fuzz tests, etc.)? Are there any testing steps to be verified by the reviewer?
How can you convince your reviewers that this PR is safe and effective?
Is this a refactor change? If so, how have you proved that the intended behavior hasn't changed? -->

Existing tests (but there is no test in `s2n-quic-h3` as well as `h3-quinn`).

By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license.




Diff:
diff --git a/quic/s2n-quic-h3/Cargo.toml b/quic/s2n-quic-h3/Cargo.toml
index eb814838da..f541be9cec 100644
--- a/quic/s2n-quic-h3/Cargo.toml
+++ b/quic/s2n-quic-h3/Cargo.toml
@@ -12,7 +12,7 @@ publish = false
 [dependencies]
 bytes = { version = "1", default-features = false }
 futures = { version = "0.3", default-features = false }
-h3 = "0.0.7"
+h3 = "0.0.8"
 s2n-quic = { path = "../s2n-quic" }
 tracing = { version = "0.1", optional = true }
 
diff --git a/quic/s2n-quic-h3/src/s2n_quic.rs b/quic/s2n-quic-h3/src/s2n_quic.rs
index 91c33e4dac..6457128d77 100644
--- a/quic/s2n-quic-h3/src/s2n_quic.rs
+++ b/quic/s2n-quic-h3/src/s2n_quic.rs
@@ -3,14 +3,13 @@
 
 use bytes::{Buf, Bytes};
 use core::task::ready;
-use h3::quic::{self, Error, StreamId, WriteBuf};
+use h3::quic::{self, ConnectionErrorIncoming, StreamErrorIncoming, StreamId, WriteBuf};
 use s2n_quic::{
     application,
     stream::{BidirectionalStream, ReceiveStream},
 };
 use std::{
     convert::TryInto,
-    fmt::{self, Display},
     sync::Arc,
     task::{self, Poll},
 };
@@ -37,69 +36,56 @@ impl Connection {
     }
 }
 
-#[derive(Debug)]
-pub struct ConnectionError(s2n_quic::connection::Error);
-
-impl std::error::Error for ConnectionError {}
-
-impl fmt::Display for ConnectionError {
-    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
-        self.0.fmt(f)
-    }
-}
-
-impl Error for ConnectionError {
-    fn is_timeout(&self) -> bool {
-        matches!(self.0, s2n_quic::connection::Error::IdleTimerExpired { .. })
-    }
-
-    fn err_code(&self) -> Option<u64> {
-        match self.0 {
-            s2n_quic::connection::Error::Application { error, .. } => Some(error.into()),
-            _ => None,
-        }
-    }
-}
-
-impl From<s2n_quic::connection::Error> for ConnectionError {
-    fn from(e: s2n_quic::connection::Error) -> Self {
-        Self(e)
-    }
-}
-
 impl<B> quic::Connection<B> for Connection
 where
     B: Buf,
 {
     type RecvStream = RecvStream;
     type OpenStreams = OpenStreams;
-    type AcceptError = ConnectionError;
 
     #[cfg_attr(feature = "tracing", instrument(skip_all, level = "trace"))]
     fn poll_accept_recv(
         &mut self,
         cx: &mut task::Context<'_>,
-    ) -> Poll<Result<Option<Self::RecvStream>, Self::AcceptError>> {
-        let recv = match ready!(self.recv_acceptor.poll_accept_receive_stream(cx))? {
-            Some(x) => x,
-            None => return Poll::Ready(Ok(None)),
+    ) -> Poll<Result<Self::RecvStream, ConnectionErrorIncoming>> {
+        let recv = match ready!(self.recv_acceptor.poll_accept_receive_stream(cx)) {
+            Ok(Some(x)) => x,
+            Ok(None) => {
+                // This happens when the connection is closed without an error
+                return Poll::Ready(Err(ConnectionErrorIncoming::InternalError(
+                    "connection closed".to_string(),
+                )));
+            }
+            Err(e) => {
+                return Poll::Ready(Err(convert_connection_error(e)));
+            }
         };
-        Poll::Ready(Ok(Some(Self::RecvStream::new(recv))))
+
+        Poll::Ready(Ok(Self::RecvStream::new(recv)))
     }
 
     #[cfg_attr(feature = "tracing", instrument(skip_all, level = "trace"))]
     fn poll_accept_bidi(
         &mut self,
         cx: &mut task::Context<'_>,
-    ) -> Poll<Result<Option<Self::BidiStream>, Self::AcceptError>> {
-        let (recv, send) = match ready!(self.bidi_acceptor.poll_accept_bidirectional_stream(cx))? {
-            Some(x) => x.split(),
-            None => return Poll::Ready(Ok(None)),
+    ) -> Poll<Result<Self::BidiStream, ConnectionErrorIncoming>> {
+        let (recv, send) = match ready!(self.bidi_acceptor.poll_accept_bidirectional_stream(cx)) {
+            Ok(Some(x)) => x.split(),
+            Ok(None) => {
+                // This happens when the connection is closed without an error
+                return Poll::Ready(Err(ConnectionErrorIncoming::InternalError(
+                    "connection closed".to_string(),
+                )));
+            }
+            Err(e) => {
+                return Poll::Ready(Err(convert_connection_error(e)));
+            }
         };
-        Poll::Ready(Ok(Some(Self::BidiStream {
+
+        Poll::Ready(Ok(Self::BidiStream {
             send: Self::SendStream::new(send),
             recv: Self::RecvStream::new(recv),
-        })))
+        }))
     }
 
     fn opener(&self) -> Self::OpenStreams {
@@ -109,30 +95,55 @@ where
     }
 }
 
+fn convert_connection_error(e: s2n_quic::connection::Error) -> h3::quic::ConnectionErrorIncoming {
+    match e {
+        s2n_quic::connection::Error::Application { error, .. } => {
+            ConnectionErrorIncoming::ApplicationClose {
+                error_code: error.into(),
+            }
+        }
+        s2n_quic::connection::Error::IdleTimerExpired { .. } => ConnectionErrorIncoming::Timeout,
+
+        s2n_quic::connection::Error::Closed { .. } => {
+            // This happens when the connection is closed without an error
+            ConnectionErrorIncoming::InternalError("connection closed".to_string())
+        }
+
+        error => ConnectionErrorIncoming::Undefined(Arc::new(error)),
+    }
+}
+
 impl<B> quic::OpenStreams<B> for Connection
 where
     B: Buf,
 {
     type BidiStream = BidiStream<B>;
     type SendStream = SendStream<B>;
-    type OpenError = ConnectionError;
 
     #[cfg_attr(feature = "tracing", instrument(skip_all, level = "trace"))]
     fn poll_open_bidi(
         &mut self,
         cx: &mut task::Context<'_>,
-    ) -> Poll<Result<Self::BidiStream, Self::OpenError>> {
-        let stream = ready!(self.conn.poll_open_bidirectional_stream(cx))?;
-        Ok(stream.into()).into()
+    ) -> Poll<Result<Self::BidiStream, StreamErrorIncoming>> {
+        self.conn
+            .poll_open_bidirectional_stream(cx)
+            .map_ok(|stream| stream.into())
+            .map_err(|err| StreamErrorIncoming::ConnectionErrorIncoming {
+                connection_error: convert_connection_error(err),
+            })
     }
 
     #[cfg_attr(feature = "tracing", instrument(skip_all, level = "trace"))]
     fn poll_open_send(
         &mut self,
         cx: &mut task::Context<'_>,
-    ) -> Poll<Result<Self::SendStream, Self::OpenError>> {
-        let stream = ready!(self.conn.poll_open_send_stream(cx))?;
-        Ok(stream.into()).into()
+    ) -> Poll<Result<Self::SendStream, StreamErrorIncoming>> {
+        self.conn
+            .poll_open_send_stream(cx)
+            .map_ok(|stream| stream.into())
+            .map_err(|err| StreamErrorIncoming::ConnectionErrorIncoming {
+                connection_error: convert_connection_error(err),
+            })
     }
 
     #[cfg_attr(feature = "tracing", instrument(skip_all, level = "trace"))]
@@ -155,24 +166,31 @@ where
 {
     type BidiStream = BidiStream<B>;
     type SendStream = SendStream<B>;
-    type OpenError = ConnectionError;
 
     #[cfg_attr(feature = "tracing", instrument(skip_all, level = "trace"))]
     fn poll_open_bidi(
         &mut self,
         cx: &mut task::Context<'_>,
-    ) -> Poll<Result<Self::BidiStream, Self::OpenError>> {
-        let stream = ready!(self.conn.poll_open_bidirectional_stream(cx))?;
-        Ok(stream.into()).into()
+    ) -> Poll<Result<Self::BidiStream, StreamErrorIncoming>> {
+        self.conn
+            .poll_open_bidirectional_stream(cx)
+            .map_ok(|stream| stream.into())
+            .map_err(|err| StreamErrorIncoming::ConnectionErrorIncoming {
+                connection_error: convert_connection_error(err),
+            })
     }
 
     #[cfg_attr(feature = "tracing", instrument(skip_all, level = "trace"))]
     fn poll_open_send(
         &mut self,
         cx: &mut task::Context<'_>,
-    ) -> Poll<Result<Self::SendStream, Self::OpenError>> {
-        let stream = ready!(self.conn.poll_open_send_stream(cx))?;
-        Ok(stream.into()).into()
+    ) -> Poll<Result<Self::SendStream, StreamErrorIncoming>> {
+        self.conn
+            .poll_open_send_stream(cx)
+            .map_ok(|stream| stream.into())
+            .map_err(|err| StreamErrorIncoming::ConnectionErrorIncoming {
+                connection_error: convert_connection_error(err),
+            })
     }
 
     #[cfg_attr(feature = "tracing", instrument(skip_all, level = "trace"))]
@@ -218,12 +236,11 @@ where
     B: Buf,
 {
     type Buf = Bytes;
-    type Error = ReadError;
 
     fn poll_data(
         &mut self,
         cx: &mut task::Context<'_>,
-    ) -> Poll<Result<Option<Self::Buf>, Self::Error>> {
+    ) -> Poll<Result<Option<Self::Buf>, StreamErrorIncoming>> {
         self.recv.poll_data(cx)
     }
 
@@ -240,13 +257,11 @@ impl<B> quic::SendStream<B> for BidiStream<B>
 where
     B: Buf,
 {
-    type Error = SendStreamError;
-
-    fn poll_ready(&mut self, cx: &mut task::Context<'_>) -> Poll<Result<(), Self::Error>> {
+    fn poll_ready(&mut self, cx: &mut task::Context<'_>) -> Poll<Result<(), StreamErrorIncoming>> {
         self.send.poll_ready(cx)
     }
 
-    fn poll_finish(&mut self, cx: &mut task::Context<'_>) -> Poll<Result<(), Self::Error>> {
+    fn poll_finish(&mut self, cx: &mut task::Context<'_>) -> Poll<Result<(), StreamErrorIncoming>> {
         self.send.poll_finish(cx)
     }
 
@@ -254,7 +269,7 @@ where
         self.send.reset(reset_code)
     }
 
-    fn send_data<D: Into<WriteBuf<B>>>(&mut self, data: D) -> Result<(), Self::Error> {
+    fn send_data<D: Into<WriteBuf<B>>>(&mut self, data: D) -> Result<(), StreamErrorIncoming> {
         self.send.send_data(data)
     }
 
@@ -288,15 +303,15 @@ impl RecvStream {
 
 impl quic::RecvStream for RecvStream {
     type Buf = Bytes;
-    type Error = ReadError;
 
     #[cfg_attr(feature = "tracing", instrument(skip_all, level = "trace"))]
     fn poll_data(
         &mut self,
         cx: &mut task::Context<'_>,
-    ) -> Poll<Result<Option<Self::Buf>, Self::Error>> {
-        let buf = ready!(self.stream.poll_receive(cx))?;
-        Ok(buf).into()
+    ) -> Poll<Result<Option<Self::Buf>, StreamErrorIncoming>> {
+        self.stream
+            .poll_receive(cx)
+            .map_err(convert_stream_error_to_h3_stream_error_incoming)
     }
 
     #[cfg_attr(feature = "tracing", instrument(skip_all, level = "trace"))]
@@ -319,49 +334,22 @@ impl From<ReceiveStream> for RecvStream {
     }
 }
 
-#[derive(Debug)]
-pub struct ReadError(s2n_quic::stream::Error);
-
-impl std::error::Error for ReadError {}
-
-impl fmt::Display for ReadError {
-    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
-        self.0.fmt(f)
-    }
-}
-
-impl From<ReadError> for Arc<dyn Error> {
-    fn from(e: ReadError) -> Self {
-        Arc::new(e)
-    }
-}
-
-impl From<s2n_quic::stream::Error> for ReadError {
-    fn from(e: s2n_quic::stream::Error) -> Self {
-        Self(e)
-    }
-}
-
-impl Error for ReadError {
-    fn is_timeout(&self) -> bool {
-        matches!(
-            self.0,
-            s2n_quic::stream::Error::ConnectionError {
-                error: s2n_quic::connection::Error::IdleTimerExpired { .. },
-                ..
+fn convert_stream_error_to_h3_stream_error_incoming(
+    error: s2n_quic::stream::Error,
+) -> StreamErrorIncoming {
+    match error {
+        s2n_quic::stream::Error::StreamReset { error, .. } => {
+            StreamErrorIncoming::StreamTerminated {
+                error_code: error.into(),
+            }
+        }
+        s2n_quic::stream::Error::ConnectionError { error, .. } => {
+            StreamErrorIncoming::ConnectionErrorIncoming {
+                connection_error: convert_connection_error(error),
             }
-        )
-    }
-
-    fn err_code(&self) -> Option<u64> {
-        match self.0 {
-            s2n_quic::stream::Error::ConnectionError {
-                error: s2n_quic::connection::Error::Application { error, .. },
-                ..
-            } => Some(error.into()),
-            s2n_quic::stream::Error::StreamReset { error, .. } => Some(error.into()),
-            _ => None,
         }
+
+        error => StreamErrorIncoming::Unknown(Box::new(error)),
     }
 }
 
@@ -389,14 +377,13 @@ impl<B> quic::SendStream<B> for SendStream<B>
 where
     B: Buf,
 {
-    type Error = SendStreamError;
-
     #[cfg_attr(feature = "tracing", instrument(skip_all, level = "trace"))]
-    fn poll_ready(&mut self, cx: &mut task::Context<'_>) -> Poll<Result<(), Self::Error>> {
+    fn poll_ready(&mut self, cx: &mut task::Context<'_>) -> Poll<Result<(), StreamErrorIncoming>> {
         loop {
             // try to flush the current chunk if we have one
             if let Some(chunk) = self.chunk.as_mut() {
-                ready!(self.stream.poll_send(chunk, cx))?;
+                ready!(self.stream.poll_send(chunk, cx))
+                    .map_err(convert_stream_error_to_h3_stream_error_incoming)?;
 
                 // s2n-quic will take the whole chunk on send, even if it exceeds the limits
                 debug_assert!(chunk.is_empty());
@@ -433,9 +420,13 @@ where
     }
 
     #[cfg_attr(feature = "tracing", instrument(skip_all, level = "trace"))]
-    fn send_data<D: Into<WriteBuf<B>>>(&mut self, data: D) -> Result<(), Self::Error> {
+    fn send_data<D: Into<WriteBuf<B>>>(&mut self, data: D) -> Result<(), StreamErrorIncoming> {
         if self.buf.is_some() {
-            return Err(Self::Error::NotReady);
+            return Err(StreamErrorIncoming::ConnectionErrorIncoming {
+                connection_error: ConnectionErrorIncoming::InternalError(
+                    "internal error in the http stack".to_string(),
+                ),
+            });
         }
         self.buf = Some(data.into());
         Ok(())
@@ -452,10 +443,13 @@ where
     }
 
     #[cfg_attr(feature = "tracing", instrument(skip_all, level = "trace"))]
-    fn poll_finish(&mut self, cx: &mut task::Context<'_>) -> Poll<Result<(), Self::Error>> {
+    fn poll_finish(&mut self, cx: &mut task::Context<'_>) -> Poll<Result<(), StreamErrorIncoming>> {
         // ensure all chunks are flushed to the QUIC stream before finishing
         ready!(self.poll_ready(cx))?;
-        self.stream.finish()?;
+        self.stream
+            .finish()
+            .map_err(convert_stream_error_to_h3_stream_error_incoming)?;
+
         Ok(()).into()
     }
 
@@ -480,54 +474,3 @@ where
         SendStream::new(send)
     }
 }
-
-#[derive(Debug)]
-pub enum SendStreamError {
-    Write(s2n_quic::stream::Error),
-    NotReady,
-}
-
-impl std::error::Error for SendStreamError {}
-
-impl Display for SendStreamError {
-    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
-        write!(f, "{self:?}")
-    }
-}
-
-impl From<s2n_quic::stream::Error> for SendStreamError {
-    fn from(e: s2n_quic::stream::Error) -> Self {
-        Self::Write(e)
-    }
-}
-
-impl Error for SendStreamError {
-    fn is_timeout(&self) -> bool {
-        matches!(
-            self,
-            Self::Write(s2n_quic::stream::Error::ConnectionError {
-                error: s2n_quic::connection::Error::IdleTimerExpired { .. },
-                ..
-            })
-        )
-    }
-
-    fn err_code(&self) -> Option<u64> {
-        match self {
-            Self::Write(s2n_quic::stream::Error::StreamReset { error, .. }) => {
-                Some((*error).into())
-            }
-            Self::Write(s2n_quic::stream::Error::ConnectionError {
-                error: s2n_quic::connection::Error::Application { error, .. },
-                ..
-            }) => Some((*error).into()),
-            _ => None,
-        }
-    }
-}
-
-impl From<SendStreamError> for Arc<dyn Error> {
-    fn from(e: SendStreamError) -> Self {
-        Arc::new(e)
-    }
-}
diff --git a/quic/s2n-quic-qns/src/server/h3.rs b/quic/s2n-quic-qns/src/server/h3.rs
index 9583061db5..36f1205eec 100644
--- a/quic/s2n-quic-qns/src/server/h3.rs
+++ b/quic/s2n-quic-qns/src/server/h3.rs
@@ -19,7 +19,14 @@ pub async fn handle_connection(connection: Connection, www_dir: Arc<Path>) {
         .await
         .unwrap();
 
-    while let Ok(Some((req, mut stream))) = conn.accept().await {
+    while let Ok(Some(req_resolver)) = conn.accept().await {
+        let (req, mut stream) = match req_resolver.resolve_request().await {
+            Ok(res) => res,
+            Err(err) => {
+                eprintln!("Error resolving request: {err:?}");
+                continue;
+            }
+        };
         match req.uri().path() {
             "" | "/" => {
                 tokio::spawn(async move {


Reviews:
user: login: maddeleine
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-14T01:19:26Z
user: login: junkurihara
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-14T03:02:05Z
user: login: maddeleine
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-14T17:14:53Z
user: login: junkurihara
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-15T01:14:33Z
user: login: maddeleine
body: Thanks so much for doing this! Hopefully h3 stops doing breaking changes, because that was a big refactor. Can you rebase your PR to get the newest commits? I just fixed our CI so we should be able to get the tests passing now on your PR.
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-05-15T22:03:21Z

Review comments:
diff_hunk: @@ -109,29 +95,69 @@ where
     }
 }
 
+fn convert_connection_error(e: s2n_quic::connection::Error) -> h3::quic::ConnectionErrorIncoming {
+    match e {
+        s2n_quic::connection::Error::Application { error, .. } => {
+            ConnectionErrorIncoming::ApplicationClose {
+                error_code: error.into(),
+            }
+        }
+        s2n_quic::connection::Error::IdleTimerExpired { .. } => ConnectionErrorIncoming::Timeout,
+
+        s2n_quic::connection::Error::Closed { .. } => {
+            // This happens when the connection is closed without an error
+            ConnectionErrorIncoming::InternalError("connection closed".to_string())
+        }
+
+        error @ s2n_quic::connection::Error::Transport { .. }
+        | error @ s2n_quic::connection::Error::StatelessReset { .. }
+        | error @ s2n_quic::connection::Error::NoValidPath { .. }
+        | error @ s2n_quic::connection::Error::StreamIdExhausted { .. }
+        | error @ s2n_quic::connection::Error::MaxHandshakeDurationExceeded { .. }
+        | error @ s2n_quic::connection::Error::ImmediateClose { .. }
+        | error @ s2n_quic::connection::Error::EndpointClosing { .. }
+        | error @ s2n_quic::connection::Error::InvalidConfiguration { .. }
+        | error @ s2n_quic::connection::Error::Unspecified { .. }
+        | error => ConnectionErrorIncoming::Undefined(Arc::new(error)),
path: quic/s2n-quic-h3/src/s2n_quic.rs
user: login: maddeleine
body: I would use a wildcard in place of this really long logical or statement.

```suggestion
        _=> ConnectionErrorIncoming::Undefined(Arc::new(error)),
```
created_at: 2025-05-13T22:15:42Z
author_association: CONTRIBUTOR
diff_hunk: @@ -319,49 +357,51 @@ impl From<ReceiveStream> for RecvStream {
     }
 }
 
-#[derive(Debug)]
-pub struct ReadError(s2n_quic::stream::Error);
-
-impl std::error::Error for ReadError {}
-
-impl fmt::Display for ReadError {
-    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
-        self.0.fmt(f)
-    }
-}
-
-impl From<ReadError> for Arc<dyn Error> {
-    fn from(e: ReadError) -> Self {
-        Arc::new(e)
-    }
-}
+fn convert_read_error_to_stream_error(error: s2n_quic::stream::Error) -> StreamErrorIncoming {
path: quic/s2n-quic-h3/src/s2n_quic.rs
user: login: maddeleine
body: Why do you need both a `fn convert_read_error_to_stream_error` and` fn convert_write_error_to_stream_error`? Aren't the two functions the exact same?
created_at: 2025-05-13T22:22:06Z
author_association: CONTRIBUTOR
diff_hunk: @@ -288,15 +324,17 @@ impl RecvStream {
 
 impl quic::RecvStream for RecvStream {
     type Buf = Bytes;
-    type Error = ReadError;
 
     #[cfg_attr(feature = "tracing", instrument(skip_all, level = "trace"))]
     fn poll_data(
         &mut self,
         cx: &mut task::Context<'_>,
-    ) -> Poll<Result<Option<Self::Buf>, Self::Error>> {
-        let buf = ready!(self.stream.poll_receive(cx))?;
-        Ok(buf).into()
+    ) -> Poll<Result<Option<Self::Buf>, StreamErrorIncoming>> {
+        match ready!(self.stream.poll_receive(cx)) {
path: quic/s2n-quic-h3/src/s2n_quic.rs
user: login: maddeleine
body: In this instance the type returned by poll_receive is exactly the same as poll_data, except for the error type. So you should just be able to use map_err to convert the error type in the error case.
```suggestion
        self.stream
            .poll_receive(cx)
            .map_err(|err| convert_read_error_to_stream_error(err))
```
created_at: 2025-05-13T22:47:56Z
author_association: CONTRIBUTOR
diff_hunk: @@ -109,29 +95,69 @@ where
     }
 }
 
+fn convert_connection_error(e: s2n_quic::connection::Error) -> h3::quic::ConnectionErrorIncoming {
+    match e {
+        s2n_quic::connection::Error::Application { error, .. } => {
+            ConnectionErrorIncoming::ApplicationClose {
+                error_code: error.into(),
+            }
+        }
+        s2n_quic::connection::Error::IdleTimerExpired { .. } => ConnectionErrorIncoming::Timeout,
+
+        s2n_quic::connection::Error::Closed { .. } => {
+            // This happens when the connection is closed without an error
+            ConnectionErrorIncoming::InternalError("connection closed".to_string())
+        }
+
+        error @ s2n_quic::connection::Error::Transport { .. }
+        | error @ s2n_quic::connection::Error::StatelessReset { .. }
+        | error @ s2n_quic::connection::Error::NoValidPath { .. }
+        | error @ s2n_quic::connection::Error::StreamIdExhausted { .. }
+        | error @ s2n_quic::connection::Error::MaxHandshakeDurationExceeded { .. }
+        | error @ s2n_quic::connection::Error::ImmediateClose { .. }
+        | error @ s2n_quic::connection::Error::EndpointClosing { .. }
+        | error @ s2n_quic::connection::Error::InvalidConfiguration { .. }
+        | error @ s2n_quic::connection::Error::Unspecified { .. }
+        | error => ConnectionErrorIncoming::Undefined(Arc::new(error)),
+    }
+}
+
 impl<B> quic::OpenStreams<B> for Connection
 where
     B: Buf,
 {
     type BidiStream = BidiStream<B>;
     type SendStream = SendStream<B>;
-    type OpenError = ConnectionError;
 
     #[cfg_attr(feature = "tracing", instrument(skip_all, level = "trace"))]
     fn poll_open_bidi(
         &mut self,
         cx: &mut task::Context<'_>,
-    ) -> Poll<Result<Self::BidiStream, Self::OpenError>> {
-        let stream = ready!(self.conn.poll_open_bidirectional_stream(cx))?;
+    ) -> Poll<Result<Self::BidiStream, StreamErrorIncoming>> {
+        let stream = match ready!(self.conn.poll_open_bidirectional_stream(cx)) {
path: quic/s2n-quic-h3/src/s2n_quic.rs
user: login: maddeleine
body: You should be able to use map_ok and map_err to get rid of a lot of this match statement which essentially makes the entire function this:
```suggestion
         self.conn
            .poll_open_bidirectional_stream(cx)
            .map_ok(|stream| stream.into())
            .map_err(|err| StreamErrorIncoming::ConnectionErrorIncoming {
                    connection_error: convert_connection_error(err),
                })
```
Same with the `fn poll_open_send` as well.
created_at: 2025-05-14T00:22:27Z
author_association: CONTRIBUTOR
diff_hunk: @@ -389,14 +429,14 @@ impl<B> quic::SendStream<B> for SendStream<B>
 where
     B: Buf,
 {
-    type Error = SendStreamError;
-
     #[cfg_attr(feature = "tracing", instrument(skip_all, level = "trace"))]
-    fn poll_ready(&mut self, cx: &mut task::Context<'_>) -> Poll<Result<(), Self::Error>> {
+    fn poll_ready(&mut self, cx: &mut task::Context<'_>) -> Poll<Result<(), StreamErrorIncoming>> {
         loop {
             // try to flush the current chunk if we have one
             if let Some(chunk) = self.chunk.as_mut() {
-                ready!(self.stream.poll_send(chunk, cx))?;
+                if let Err(e) = ready!(self.stream.poll_send(chunk, cx)) {
path: quic/s2n-quic-h3/src/s2n_quic.rs
user: login: maddeleine
body: ```suggestion
               ready!(self.stream.poll_send(chunk, cx).map_err(|err| convert_write_error_to_stream_error(err)))?;
```
created_at: 2025-05-14T00:33:30Z
author_association: CONTRIBUTOR
diff_hunk: @@ -452,10 +496,12 @@ where
     }
 
     #[cfg_attr(feature = "tracing", instrument(skip_all, level = "trace"))]
-    fn poll_finish(&mut self, cx: &mut task::Context<'_>) -> Poll<Result<(), Self::Error>> {
+    fn poll_finish(&mut self, cx: &mut task::Context<'_>) -> Poll<Result<(), StreamErrorIncoming>> {
         // ensure all chunks are flushed to the QUIC stream before finishing
         ready!(self.poll_ready(cx))?;
-        self.stream.finish()?;
+        if let Err(e) = self.stream.finish() {
path: quic/s2n-quic-h3/src/s2n_quic.rs
user: login: maddeleine
body: I think this would work. I just think applying a map_error is cleaner than decomposing the error and then recomposing it with the right type.
```suggestion
        self.stream
            .finish()
            .map_err(|err| convert_write_error_to_stream_error(err))?;
```
created_at: 2025-05-14T01:05:27Z
author_association: CONTRIBUTOR
diff_hunk: @@ -19,7 +19,14 @@ pub async fn handle_connection(connection: Connection, www_dir: Arc<Path>) {
         .await
         .unwrap();
 
-    while let Ok(Some((req, mut stream))) = conn.accept().await {
+    while let Ok(Some(req_resolver)) = conn.accept().await {
+        let (req, mut stream) = match req_resolver.resolve_request().await {
+            Ok((req, stream)) => (req, stream),
path: quic/s2n-quic-qns/src/server/h3.rs
user: login: maddeleine
body: Nit: you shouldn't need to decompose the tuple here since you're not doing any transformation in this match arm.
```suggestion
            Ok(res) => res,
```
created_at: 2025-05-14T01:14:23Z
author_association: CONTRIBUTOR
diff_hunk: @@ -319,49 +357,51 @@ impl From<ReceiveStream> for RecvStream {
     }
 }
 
-#[derive(Debug)]
-pub struct ReadError(s2n_quic::stream::Error);
-
-impl std::error::Error for ReadError {}
-
-impl fmt::Display for ReadError {
-    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
-        self.0.fmt(f)
-    }
-}
-
-impl From<ReadError> for Arc<dyn Error> {
-    fn from(e: ReadError) -> Self {
-        Arc::new(e)
-    }
-}
+fn convert_read_error_to_stream_error(error: s2n_quic::stream::Error) -> StreamErrorIncoming {
path: quic/s2n-quic-h3/src/s2n_quic.rs
user: login: junkurihara
body: Agree. I will use only `convert_read....` and change its name to `convert_stream_error_to_h3_stream_error_incoming`.
created_at: 2025-05-14T02:56:12Z
author_association: CONTRIBUTOR
diff_hunk: @@ -109,29 +95,69 @@ where
     }
 }
 
+fn convert_connection_error(e: s2n_quic::connection::Error) -> h3::quic::ConnectionErrorIncoming {
+    match e {
+        s2n_quic::connection::Error::Application { error, .. } => {
+            ConnectionErrorIncoming::ApplicationClose {
+                error_code: error.into(),
+            }
+        }
+        s2n_quic::connection::Error::IdleTimerExpired { .. } => ConnectionErrorIncoming::Timeout,
+
+        s2n_quic::connection::Error::Closed { .. } => {
+            // This happens when the connection is closed without an error
+            ConnectionErrorIncoming::InternalError("connection closed".to_string())
+        }
+
+        error @ s2n_quic::connection::Error::Transport { .. }
+        | error @ s2n_quic::connection::Error::StatelessReset { .. }
+        | error @ s2n_quic::connection::Error::NoValidPath { .. }
+        | error @ s2n_quic::connection::Error::StreamIdExhausted { .. }
+        | error @ s2n_quic::connection::Error::MaxHandshakeDurationExceeded { .. }
+        | error @ s2n_quic::connection::Error::ImmediateClose { .. }
+        | error @ s2n_quic::connection::Error::EndpointClosing { .. }
+        | error @ s2n_quic::connection::Error::InvalidConfiguration { .. }
+        | error @ s2n_quic::connection::Error::Unspecified { .. }
+        | error => ConnectionErrorIncoming::Undefined(Arc::new(error)),
path: quic/s2n-quic-h3/src/s2n_quic.rs
user: login: junkurihara
body: Agree. Will change this as

```rust:
error @  _ => ConnectionErrorIncoming::Undefined(Arc::new(error)),
```

to use `error` for the return type.
created_at: 2025-05-14T02:58:02Z
author_association: CONTRIBUTOR
diff_hunk: @@ -109,29 +95,60 @@ where
     }
 }
 
+fn convert_connection_error(e: s2n_quic::connection::Error) -> h3::quic::ConnectionErrorIncoming {
+    match e {
+        s2n_quic::connection::Error::Application { error, .. } => {
+            ConnectionErrorIncoming::ApplicationClose {
+                error_code: error.into(),
+            }
+        }
+        s2n_quic::connection::Error::IdleTimerExpired { .. } => ConnectionErrorIncoming::Timeout,
+
+        s2n_quic::connection::Error::Closed { .. } => {
+            // This happens when the connection is closed without an error
+            ConnectionErrorIncoming::InternalError("connection closed".to_string())
+        }
+
+        error => ConnectionErrorIncoming::Undefined(Arc::new(error)),
+    }
+}
+
 impl<B> quic::OpenStreams<B> for Connection
 where
     B: Buf,
 {
     type BidiStream = BidiStream<B>;
     type SendStream = SendStream<B>;
-    type OpenError = ConnectionError;
 
     #[cfg_attr(feature = "tracing", instrument(skip_all, level = "trace"))]
     fn poll_open_bidi(
         &mut self,
         cx: &mut task::Context<'_>,
-    ) -> Poll<Result<Self::BidiStream, Self::OpenError>> {
-        let stream = ready!(self.conn.poll_open_bidirectional_stream(cx))?;
+    ) -> Poll<Result<Self::BidiStream, StreamErrorIncoming>> {
+        let stream = match ready!(self.conn.poll_open_bidirectional_stream(cx)) {
path: quic/s2n-quic-h3/src/s2n_quic.rs
user: login: maddeleine
body: Nit: This one can also use map_ok() and map_err(), as well as line 144
created_at: 2025-05-14T17:14:53Z
author_association: CONTRIBUTOR
diff_hunk: @@ -109,29 +95,60 @@ where
     }
 }
 
+fn convert_connection_error(e: s2n_quic::connection::Error) -> h3::quic::ConnectionErrorIncoming {
+    match e {
+        s2n_quic::connection::Error::Application { error, .. } => {
+            ConnectionErrorIncoming::ApplicationClose {
+                error_code: error.into(),
+            }
+        }
+        s2n_quic::connection::Error::IdleTimerExpired { .. } => ConnectionErrorIncoming::Timeout,
+
+        s2n_quic::connection::Error::Closed { .. } => {
+            // This happens when the connection is closed without an error
+            ConnectionErrorIncoming::InternalError("connection closed".to_string())
+        }
+
+        error => ConnectionErrorIncoming::Undefined(Arc::new(error)),
+    }
+}
+
 impl<B> quic::OpenStreams<B> for Connection
 where
     B: Buf,
 {
     type BidiStream = BidiStream<B>;
     type SendStream = SendStream<B>;
-    type OpenError = ConnectionError;
 
     #[cfg_attr(feature = "tracing", instrument(skip_all, level = "trace"))]
     fn poll_open_bidi(
         &mut self,
         cx: &mut task::Context<'_>,
-    ) -> Poll<Result<Self::BidiStream, Self::OpenError>> {
-        let stream = ready!(self.conn.poll_open_bidirectional_stream(cx))?;
+    ) -> Poll<Result<Self::BidiStream, StreamErrorIncoming>> {
+        let stream = match ready!(self.conn.poll_open_bidirectional_stream(cx)) {
path: quic/s2n-quic-h3/src/s2n_quic.rs
user: login: junkurihara
body: Thanks! I just updated lines with map_ok and map_err.
created_at: 2025-05-15T01:14:33Z
author_association: CONTRIBUTOR

PR comments:
user: login: boquan-fang
created_at: 2025-05-08T23:32:07Z
body: Hi Jun, thanks for cutting this PR to us. We will take a look and determine how to include `h3` updates to our codebase.
author_association: CONTRIBUTOR
user: login: boquan-fang
created_at: 2025-05-12T17:42:31Z
body: Hello Jun, we have assigned a reviewer for this PR and we will give a review for this PR later. To get started, this PR doesn't pass our CI, since it is not compiled properly. You can find the specific error in the CI run.
author_association: CONTRIBUTOR
user: login: junkurihara
created_at: 2025-05-12T18:14:24Z
body: @boquan-fang Hi, thanks!
As you mentioned, from the results of CI, I found that I forgot to update `s2n-quic-qns` package according to `h3` reexports. So I just fixed. I hope this passes all required tests.
author_association: CONTRIBUTOR
user: login: maddeleine
created_at: 2025-05-12T19:20:25Z
body: Also our CI is currently broken. So qns/interop-report isn't expected to pass right now.
author_association: CONTRIBUTOR
user: login: junkurihara
created_at: 2025-05-14T02:37:55Z
body: Thanks for the comments! Most of commented part just followed the structure of `h3-quinn`, which actually doesn't fit to `s2n-quic-h3`. So I will update the branch and respond to comments shortly.
author_association: CONTRIBUTOR
user: login: junkurihara
created_at: 2025-05-14T03:04:53Z
body: Hi. I just pushed a commit responding to the comments. I hope everything has been resolved now.
author_association: CONTRIBUTOR
user: login: junkurihara
created_at: 2025-05-16T08:29:16Z
body: @maddeleine Thank you for reviewing! I just rebased my branch to the HEAD of main, and squashed several commits :-)
author_association: CONTRIBUTOR