Skip to content

Return ErrTxClosed from Rollback when the connection is already closed (#2557)#2568

Closed
luongs3 wants to merge 1 commit into
jackc:masterfrom
luongs3:fix/2557-rollback-closed-conn-errtxclosed
Closed

Return ErrTxClosed from Rollback when the connection is already closed (#2557)#2568
luongs3 wants to merge 1 commit into
jackc:masterfrom
luongs3:fix/2557-rollback-closed-conn-errtxclosed

Conversation

@luongs3
Copy link
Copy Markdown

@luongs3 luongs3 commented May 27, 2026

Fixes #2557.

Problem

When a transaction query's context is canceled mid-flight, the low-level pgconn.PgConn is torn down (i/o timeout), but the outer pgx.dbTx state is left open (tx.closed stays false). The next Rollback then issues "rollback" on the already-dead connection and surfaces the raw "conn closed" error instead of pgx.ErrTxClosed.

This contradicts Rollback's documented contract:

Rollback will return an error where errors.Is(ErrTxClosed) is true if the Tx is already closed […]

A closed underlying connection means the transaction is, for all practical purposes, already closed — so callers reasonably expect ErrTxClosed (which they can assert on) rather than an opaque low-level error.

Reproduction (from the issue)

ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond)
defer cancel()

tx, _ := conn.BeginTx(ctx, pgx.TxOptions{})
_, _ = tx.Exec(ctx, "SELECT pg_sleep(1)") // canceled; pgconn is closed
err := tx.Rollback(context.Background())
// before: err == "conn closed"
// after:  errors.Is(err, pgx.ErrTxClosed) == true

Fix

In Rollback, check tx.conn.IsClosed() up front. If the connection is already closed, mark the tx closed and return ErrTxClosed. This mirrors how Commit already consults connection state (tx.conn.PgConn().TxStatus()), and leaves the normal rollback path — including the existing "rollback failed → die()" handling — untouched.

Tests

Added TestTransactionRollbackAfterCanceledQueryReturnsErrTxClosed, which reproduces the issue's exact scenario (cancel a pg_sleep query, then roll back) and asserts errors.Is(err, pgx.ErrTxClosed). It fails on master (conn closed) and passes with this change. The existing transaction test suite continues to pass.

When a transaction query's context is canceled mid-flight, the underlying
pgconn is torn down (i/o timeout) but the dbTx state is left open. The next
Rollback then issued "rollback" on the dead connection and surfaced the raw
"conn closed" error instead of pgx.ErrTxClosed.

Tx.Rollback documents that it returns an error matching ErrTxClosed when the
Tx is already closed; a closed underlying connection is exactly that case.
Check conn.IsClosed() up front and return ErrTxClosed, mirroring how Commit
already consults connection state.

Fixes jackc#2557.
@jackc
Copy link
Copy Markdown
Owner

jackc commented May 30, 2026

As discussed in the issue, if any change is to be made, this isn't the direction we will go.

@jackc jackc closed this May 30, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Transaction rollback returns conn closed instead of ErrTxDone when the pgconn is closed

2 participants