Skip to content

Retryability on an incoming GOAWAY frame #120

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
ruccho opened this issue Feb 18, 2025 · 0 comments
Open

Retryability on an incoming GOAWAY frame #120

ruccho opened this issue Feb 18, 2025 · 0 comments
Assignees

Comments

@ruccho
Copy link
Contributor

ruccho commented Feb 18, 2025

Some implementations of HTTP/2-compliant servers may set an upper limit on the cumulative number of requests per connection. When a request exceeding this limit is sent, a GOAWAY frame is sent from the server. The GOAWAY frame containsLast-Stream-ID, and streams with IDs lower than the last stream id will continue until completion, but streams with IDs higher than the last stream id will be canceled if the client has already sent one. Canceled requests appear as an exception in YetAnotherHttpHandler.

IOException: client error (SendRequest): http2 error

According to RFC 9113, these canceled streams can be safely retried on a new connection. However, according to hyperium/hyper#2500, hyper has already dropped the information necessary for retries when it receives GOAWAY and cannot automatically retry them. It says that it must be retried at a higher layer.

Possible Solutions

  • Retry at the YetAnotherHttpHandler layer.
    • It may be difficult to hold the information necessary to retry until the completion of the request.
  • Retry at the user code layer.
    • Currently, the exception doesn't tell us whether the request can be safely retried.

Reproduction

Unfortunately, I was not able to find a way to reproduce this behavior in ASP.NET Core, but was able to reproduce it with NGINX instead.
Enable HTTP/2 in nginx.conf and set the keepalive_requests value to a smaller value such as 10.

If you send more requests than keepalive_requests in a row with YetAnotherHttpHandler, an exception will be thrown.

    [Fact]
    public async Task GracefulShutdownRetryability()
    {
        using var httpClient = new HttpClient(new YetAnotherHttpHandler()
        {
            SkipCertificateVerification = true
        }, true);

        // Warms up the connection
        // Before the first connection is established and pooled, multiple requests may create multiple connections.
        {
            var request = new HttpRequestMessage(HttpMethod.Get, $"https://localhost/");
            await httpClient.SendAsync(request);
        }

        List<Task> tasks = new();
        for (var i = 0; i < 20; i++) // send more than keepalive_requests
        {
            var request = new HttpRequestMessage(HttpMethod.Get, $"https://localhost/");
            tasks.Add(httpClient.SendAsync(request));
        }

        var ex = await Record.ExceptionAsync(async () => await Task.WhenAll(tasks));

        testOutputHelper.WriteLine(ex?.ToString() ?? "no error");

        Assert.IsType<HttpRequestException>(ex);
    }
@mayuki mayuki self-assigned this Mar 18, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

2 participants