Skip to content

Instantly share code, notes, and snippets.

@antonfirsov
Last active June 9, 2022 16:42
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save antonfirsov/cefa39d866826eba3de1afd8bbfa0119 to your computer and use it in GitHub Desktop.
Save antonfirsov/cefa39d866826eba3de1afd8bbfa0119 to your computer and use it in GitHub Desktop.
Exposing HTTP/2 and HTTP/3 protocol error details from `SocketsHttpHandler`

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

This proposal builds on the decisions made in our QUIC Exceptions Design proposal.

Proposed design

  • Define a new exception type ProtocolException, and embed it as HttpRequestException.InnerException
  • Throw ProtocolException directly from HttpResponse content read streams
  • In case of HTTP/3, embed QuicException as ProtocolException.InnerException
public class ProtocolException : IOException
{
    public ProtocolException(long? protocolErrorCode, string message, Exception innerException) { }

    // ProtocolErrorCode == null means we have a QuicException with ApplicationLevelErrorCode == null
    public long? ProtocolErrorCode { get; }

    // CONSIDER:
    // Normally, ProtocolErrorCode >= 256 means HTTP/3 but the peer free to put there anything.
    // Exposing exposing HTTP version directly might help with that & it can be also convenient:
    // public Version HttpVersion { get; }
}

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("Error: " + protocolException.ProtocolErrorCode)
    if (protocolException.InnerException is QuicException quicException)
        Console.WriteLine("Underlying QUIC error: " + quicException.Message);
}

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);
}
// HTTP/2 protocol errors
catch (ProtocolException protocolException)
{
    Console.WriteLine("Error: " + protocolException.ProtocolErrorCode)
}

Pros & cons

  • (+) Single exception to catch them all
  • (-) Embedding IOException into another IOException is weird

Open questions

Distinguishing our own errors from peer errors & distinguishing RST_STREAM from GOAWAY

Currently, our internal HTTP/2 exceptions report ProtocolError in the following cases:

  • GOAWAY received
  • RST_STREAM received
  • We detect a protocol violation

Possible resolution

Add an enum (now or in the future) that maps HTTP/2 cases and QuicException.QuicError to something like:

public enum ProtocolErrorReason
{
    ConnectionAborted,
    StreamAborted,
    ProtocolViolationByPeer
}

Alternative designs

Parallel independent exception types

public class Http2ProtocolException : IOException
{
    public Http2ProtocolException(long protocolErrorCode, string message, Exception innerException) { }

    public int ProtocolErrorCode { 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);
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment