Skip to content

[API Proposal] QuicError.AcceptConnectionFailed #75184

Closed

Description

@rzikm

Background and motivation

Context: #75115

QuicListener.AcceptConnectionAsync can currently throw in the following cases:

  • QuicException with QuicError.OperationAborted when the listener is disposed while there is a pending AcceptConnectionAsync call
  • ObjectDisposedException if called after the listener was disposed
  • AuthenticationException when the TLS handshake fails (e.g. due to the certificate being rejected)
  • ArgumentException when the options returned from QuicListenerOptions.ConnectionOptionsCallback are not valid (e.g. server certificate is missing)
  • any exception that was thrown from the QuicListenerOptions.ConnectionOptionsCallback

The last three cases are propagated outside to make it easier for users to diagnose why they are unable to accept a connection. Without it, would be difficult for users to debug why they are unable to connect.

The very last case is especially problematic because it invokes a user-provided callback and therefore it can throw any exception. Throwing e.g. ObjectDisposedException from the callback could confuse code that uses QuicListener (such as ASP.NET Core).

The proposed solution is to wrap the last 3 cases listed above in a QuicException with a new QuicError.AcceptConnectionFailed value. This makes it easier for users to distinguish non-fatal exceptions after which AcceptConnectionAsync can be called again.

Note: QUIC APIs are new in 7.0 and they are Preview -- therefore such change is acceptable.

There is an existing precedent for wrapping exceptions from user callbacks. For example with HttpClient, when an exception is thrown from certificate selection/validation callback, the exception is wrapped in HttpRequestException.

API Proposal

    public enum QuicError
    {
        // existing members omitted

+       /// <summary>
+       /// An error occurred when accepting a new connection, such as failed handshake or invalid <see cref="QuicConnectionOptions" /> was returned from <see cref="QuicListenerOptions.ConnectionOptionsCallback" />.
+       /// </summary>
+       AcceptConnectionFailed,
    }

    public sealed class QuicException : IOException
    {
         // existing members omitted

         // existing ctor:
         public QuicException(QuicError error, long? applicationErrorCode, string message) {...}

+        public QuicException(QuicError error, long? applicationErrorCode, string message, Exception innerException) {...}
    }
}

API Usage

while (true)
{
    try
    {
        var connection = await _listener.AcceptConnectionAsync();
        Task.Run(() => ProcessConnection(connection));
    }
    catch (QuicException ex) when (ex.QuicError == QuicError.AcceptConnectionFailed)
    {
        // transient failure, retry and wait for next connection attempt.
    }
    catch (Exception e) when (e is ObjectDisposedException || (e is QuicException ex && ex.QuicError == QuicError.OperationAborted))
    {
        // listener was disposed, stop accepting connections.
        return;
    }
}

Activity

  1. ghost added
    untriagedNew issue has not been triaged by the area owner
    on Sep 7, 2022
  2. added
    api-suggestionEarly API idea and discussion, it is NOT ready for implementation
    and removed
    untriagedNew issue has not been triaged by the area owner
    on Sep 7, 2022
  3. added this to the 7.0.0 milestone on Sep 7, 2022
  4. karelz commented on Sep 7, 2022

    @karelz
    Member

    LGTM, thanks @rzikm! @CarnaViire can you please take a look? cc @ManickaP if she will have time today.

  5. JamesNK commented on Sep 7, 2022

    @JamesNK
    Member

    Correct me if I’m wrong but I didn’t see a QuicException ctor overload with an inner exception.

  6. JamesNK commented on Sep 7, 2022

    @JamesNK
    Member

    I don’t think it changes anything, but a 6th exception is probably OperationCanceledException if the CT passed to the method is canceled.

  7. rzikm commented on Sep 7, 2022

    @rzikm
    MemberAuthor

    Correct me if I’m wrong but I didn’t see a QuicException ctor overload with an inner exception.

    Good catch! I forgot we removed that overload in the final proposal, I have added it in the proposal

  8. JamesNK commented on Sep 7, 2022

    @JamesNK
    Member

    The exception is thrown internally. You could use an internal ctor. Up to you on whether to make it public or not.

  9. rzikm commented on Sep 7, 2022

    @rzikm
    MemberAuthor

    When we had API Proposal for QuicException, there was pushback against having internal constructors, so I think we should stick to that

  10. CarnaViire commented on Sep 7, 2022

    @CarnaViire
    Member

    LGTM in general. I feel like I would like it more if it was not specifically bound to AcceptConnection though, more like "UserCallbackError" or something in the same meaning, so it could be reused for the same purpose in other callbacks. But I think we only have a callback for Accept now, so maybe it's ok.

    (Also, I feel like "AcceptConnectionFailed" might be too general name, so it might not be clear for the user, that it wouldn't include all other cases in which Accept is failing, like AuthenticationException)

  11. rzikm commented on Sep 7, 2022

    @rzikm
    MemberAuthor

    I could not find a better name, so I am open to suggestions :)

  12. modified the milestones: 7.0.0, 8.0.0 on Sep 8, 2022
  13. karelz commented on Sep 8, 2022

    @karelz
    Member

    Triage: Right now we have last couple of days to make these kind of changes and even that is border-line meeting the bar for 7.0.
    Given that there are more concerns on usability from @JamesNK we should rather properly rethink what we want long term and do the right thing in 8.0 (as a technical breaking change of this particular API).
    Moving to 8.0.

  14. ManickaP commented on Sep 12, 2022

    @ManickaP
    Member

    I like @CarnaViire suggestion with UserCallbackError.

  15. JamesNK commented on Sep 12, 2022

    @JamesNK
    Member

    AuthenticationException can come from the client rejecting the server certificate. Ie a non callback cause.

  16. ManickaP commented on Sep 12, 2022

    @ManickaP
    Member

    The proposed solution is to wrap the last 3 cases listed above in a QuicException

    I haven't read the description properly. I don't think we should wrap those three, just the last one. And if we decide to wrap AuthenticationException and ArgumentException we should reconsider what we throw from QuicConnection.ConnectAsync since those two methods throw analogous exceptions.

  17. ManickaP commented on Jan 19, 2023

    @ManickaP
    Member

    Triage: we should compare with SslStream and other relevant similar APIs (SslStream doesn't wrap) and decide whether to do anything about this or not. That should be done 8.0.

  18. rzikm commented on Jun 23, 2023

    @rzikm
    MemberAuthor

    @wfurt Can we close this proposal? Since the scenario is going to be addressed by #87259?

  19. removed their assignment
    on Jun 23, 2023
  20. wfurt commented on Jun 23, 2023

    @wfurt
    Member

    yes, closing in favor of #87259. I'll re-open #75115 to track parts that do not need API changes.

  21. ghost locked as resolved and limited conversation to collaborators on Jul 23, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

      Milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions