diff --git a/src/Npgsql/NpgsqlDataReader.cs b/src/Npgsql/NpgsqlDataReader.cs index 4add2e970d..86963afd4a 100644 --- a/src/Npgsql/NpgsqlDataReader.cs +++ b/src/Npgsql/NpgsqlDataReader.cs @@ -985,7 +985,13 @@ async Task Consume(bool async, Exception? firstException = null) // Skip over the other result sets. Note that this does tally records affected from CommandComplete messages, and properly sets // state for auto-prepared statements - while (true) + // + // The only exception is when the connector is broken (which can happen in the middle of consuming) + // As then there is no point in going forward + // + // While we can also check our local state (State == Closed) + // It's probably better to rely on connector since it's private and its state can't be changed + while (!Connector.IsBroken) { try { diff --git a/test/Npgsql.Tests/ReaderTests.cs b/test/Npgsql.Tests/ReaderTests.cs index b46342153f..839ec5b610 100644 --- a/test/Npgsql.Tests/ReaderTests.cs +++ b/test/Npgsql.Tests/ReaderTests.cs @@ -2371,6 +2371,41 @@ await pgMock Assert.That(conn.Connector!.State, Is.EqualTo(ConnectorState.Ready)); } + [Test, IssueLink("https://github.com/npgsql/npgsql/issues/6160")] + [Description("Consuming result set shouldn't go infinite in case connection is broken")] + public async Task Bug6160() + { + var csb = new NpgsqlConnectionStringBuilder(ConnectionString) + { + // Set to -1 to trigger immediate connection break on timeout + CancellationTimeout = -1, + CommandTimeout = 1 + }; + await using var postmasterMock = PgPostmasterMock.Start(csb.ConnectionString); + await using var dataSource = CreateDataSource(postmasterMock.ConnectionString); + await using var conn = await dataSource.OpenConnectionAsync(); + + var pgMock = await postmasterMock.WaitForServerConnection(); + await pgMock + .WriteParseComplete() + .WriteBindComplete() + .WriteRowDescription(new FieldDescription(Int4Oid)) + .WriteDataRow(new byte[4]) + .FlushAsync(); + + await using var cmd = new NpgsqlCommand("SELECT 1", conn); + await using (var reader = await cmd.ExecuteReaderAsync(Behavior | CommandBehavior.SingleRow)) + { + await reader.ReadAsync(); + // The second read will try to consume the whole resultset due to CommandBehavior.SingleRow + // Which will fail with timeout (and immediate connection break) since we didn't send anything else beside the first row + var ex = Assert.ThrowsAsync(async () => await reader.ReadAsync())!; + Assert.That(ex.InnerException, Is.TypeOf()); + + Assert.That(conn.State, Is.EqualTo(ConnectionState.Closed)); + } + } + #endregion #region Initialization / setup / teardown