Skip to content

ConnectionError::is_h3_no_error() doesn't detect QUIC-layer NO_ERROR in Undefined variant #325

@iadev09

Description

@iadev09

Problem

ConnectionError::is_h3_no_error() currently only checks for H3-layer NO_ERROR, but misses QUIC-layer NO_ERROR closes that are wrapped in the Undefined variant. This causes graceful connection closes to be logged/handled as errors.

Real-World Impact

When using h3-quinn, normal client disconnects (e.g., from curl) are logged as warnings instead of debug messages:

⚠️ H3 connection closed with error: Remote(Undefined(ConnectionClosed(
    ConnectionClose { error_code: NO_ERROR, frame_type: None, reason: b"" }
))) ip:127.0.0.1

Notice error_code: NO_ERROR - this is a graceful close, not an error.

Current Implementation

ConnectionError::is_h3_no_error() in h3/src/error/error.rs:

pub fn is_h3_no_error(&self) -> bool {
    match self {
        ConnectionError::Local { error: LocalError::Application { code: Code::H3_NO_ERROR, .. } } => true,
        ConnectionError::Remote(ConnectionErrorIncoming::ApplicationClose { error_code })
            if *error_code == Code::H3_NO_ERROR.value() => true,
        _ => false,  // ← Undefined variant falls through!
    }
}

This only checks ApplicationClose, not Undefined which contains QUIC-layer errors.

Related Context

PR #315 fixed the same issue for StreamError::is_h3_no_error() by checking the RemoteTerminate variant. This is the connection-level equivalent.

The Challenge

The Undefined variant contains a trait object:

ConnectionErrorIncoming::Undefined(Arc<dyn std::error::Error + Send + Sync>)

The error code is buried inside the boxed error (e.g., quinn::ConnectionError::ConnectionClosed(ConnectionClose { error_code: ... })), but:

  • The concrete type is erased (trait object)
  • Fields are not accessible through the Error trait
  • h3 is transport-agnostic (can't depend on quinn)

Possible Approaches

I can see a few options, but I'm not sure which aligns with h3's design philosophy:

  1. String parsing (pragmatic but fragile):

    ConnectionError::Remote(ConnectionErrorIncoming::Undefined(err)) => {
        format!("{:?}", err).contains("NO_ERROR")
    }
  2. Architectural change: Expose error code in ConnectionErrorIncoming structure

  3. Trait method: Add a method that QUIC implementations can use to signal clean close

  4. Something better I haven't thought of?

Question

How should this be properly solved? I'm happy to implement a fix following your guidance.

Thanks!

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions