Skip to content

[API Proposal]: Exposing HTTP/2 and HTTP/3 protocol error codes #70684

@antonfirsov

Description

@antonfirsov

Exposing HTTP/2 and HTTP/3 protocol error codes from SocketsHttpHandler

This proposal builds on our QuicException proposal.

Background and Motivation

GRPC (and potentially other lower-level users) need a way to get the underlying HTTP/2 or HTTP/3 error codes in case a protocol error occurs.

The error code should be observable not only when an error occurs while calling an HttpClient method (#43239), but also when invoking Stream API-s on the response's content read stream (#62228).

Proposed design

  • Define a new exception type HttpProtocolException, and embed it as HttpRequestException.InnerException
  • Throw ProtocolException directly from HttpResponse content read streams
namespace System.Net.Http;

public sealed class HttpProtocolException : IOException
{
    public HttpProtocolException(HttpProtocolError errorCode, string message, Exception? innerException);

    public HttpProtocolError ErrorCode { get; }
}

// Map enum values to H2 and H3 error codes
// H3 error codes can be 62 bit long
public enum HttpProtocolError : long 
{
    // Camel-cased names taken directly from the HTTP/2 spec
    // https://datatracker.ietf.org/doc/html/rfc7540#section-7
    NoError = 0x0,
    ProtocolError,
    InternalError,
    FlowControlError,
    SettingsTimeout,
    StreamClosed,
    FrameSizeError,
    RefusedStream,
    Cancel,
    CompressionError,
    EnhanceYourCalm,
    InadequateSecurity,
    Http11Required,

    // Camel-cased names taken directly from the HTTP/3 spec
    // https://datatracker.ietf.org/doc/html/rfc9114/#section-8.1
    H3NoError = 0x100,
    H3GeneralProtocolError,
    H3InternalERror,
    H3StreamCreationError,
    H3ClosedCriticalStream,
    H3FrameUnexpected,
    H3FrameError,
    H3ExcessiveLoad,
    H3IdError,
    H3SettingsError,
    H3MissingSettings,
    H3RequestRejected,
    H3RequestCancelled,
    H3RequestIncomplete,
    H3MessageError,
    H3ConnectError,
    H3VersionFallback
}

Edit: Added a public constructor.

Notes

  • No public constructors, because we prefer to do the bare minimum to deliver a feature. This means that WinHttpHandler and user-made HttpClientHandlers won't be able to throw HttpProtocolException for now. We can consider public constructors later, if necessary.
  • We throw ProtocolError when a connection or stream is aborted, or when we detect a protocol violation ourselves
  • ErrorCode >= 256 means HTTP/3
  • In case of HTTP/3 connection or stream errors, we are embedding QuicException as ProtocolException.InnerException
  • We don't throw HttpProtocolException for transport-level errors

API Usage

Over HttpClient

using var client = new HttpClient();

try
{
    var response = await client.GetStringAsync(".");
}
catch (HttpRequestException ex) when (ex.InnerException is ProtocolException protocolException)
{
    Console.WriteLine("HTTP error code: " + protocolException.ProtocolErrorCode)
    if (protocolException.InnerException is QuicException quicException)
        Console.WriteLine("Underlying QUIC error: " + quicException.Message);
}
catch (HttpRequestException ex) when (ex.InnerException is AuthenticationException authenticationException) {
    // TLS authentication error. Can originate from QUIC, we map some errors to high-level .NET exceptions
}

Over response stream

using var client = new HttpClient();
using var response = await client.GetAsync(".", HttpCompletionOption.ResponseHeadersRead);
using var responseStream = await response.Content.ReadAsStreamAsync();
using var memoryStream = new MemoryStream();

try {
    await responseStream.CopyToAsync(memoryStream);
}
catch (ProtocolException protocolException) {
    // HTTP(2|3) protocol error
}
catch (QuicException quicException) {
    // QUIC transport error
}
catch (IOException exception) when (ex.InnerException is SocketException socketException) {
    // TCP transport error
}

Alternative designs

Parallel independent exception types for HTTP/2 and HTTP/3

public class Http2ProtocolException : IOException
{
    public int ErrorCode { get; }
}

// Use System.Net.Quic.QuicException for HTTP/3
// public class QuicException : IOException { }
Usage
using var client = new HttpClient();

try
{
    var response = await client.GetStringAsync("foo.bar");
}
// HTTP/2
catch (HttpRequestException ex) when (ex.InnerException is Http2ProtocolException protocolException)
{
    Console.WriteLine(protocolException.ProtocolErrorCode)
}
// HTTP/3
catch (HttpRequestException ex) when (ex.InnerException is QuicException quicException)
{
    Console.WriteLine(quicException.ApplicationErrorCode);
}

Do not define an enum, use untyped long error codes

public sealed class HttpProtocolException : IOException
{
    public long ErrorCode { get; }
}

Risks

  • HttpProtocolError enum: implementations may send unspecified error codes. If spec evolves in the future, we will have to introduce new error codes. In both cases field would contain a value outside the enum range, so users would have a workaround.
  • Unless I'm missing something, this is not a breaking change, since HttpProtocolException is an IOException.

Metadata

Metadata

Assignees

Labels

api-approvedAPI was approved in API review, it can be implementedarea-System.Net.HttpblockingMarks issues that we want to fast track in order to unblock other important work

Type

No type

Projects

No projects

Milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions