Repository: jackc/pgx

Issue:
pgtype: NUMERIC binary decode hangs/infinite loop for large-weight values (misuses dscale)
**Describe the bug**

Decoding certain valid PostgreSQL NUMERIC values in binary format can hang or do excessive work (and infinite loop). Inputs with very large `weight` and small `dscale` trigger:
- int16 overflow in an intermediate calculation
- huge loops rescaling by powers of 10
- a possible infinite loop when the accumulated integer becomes zero

Root cause: the decoder attempts to rescale to the wire `dscale` and uses `int16` math. PostgreSQL’s `numeric_recv` does not rescale to `dscale` (it’s display-only).

**To Reproduce**

Steps:
1. Use pgtype to decode the following binary NUMERIC payload.
2. It represents `ndigits=2, weight=11134, sign=0, dscale=1, digits=[6,7000]`.

Runnable example (no DB needed):

```go
package main

import (
    "encoding/base64"
    "fmt"
    "time"

    "github.com/jackc/pgx/v5"
    "github.com/jackc/pgx/v5/pgtype"
)

func main() {
    const b64 = "AAIrfgAAAAEABhtY" // ndigits=2, weight=11134, sign=0, dscale=1, digits=[6,7000]
    payload, _ := base64.StdEncoding.DecodeString(b64)

    m := pgtype.NewMap()
    var n pgtype.Numeric

    done := make(chan error, 1)
    go func() { done <- m.Scan(pgtype.NumericOID, pgx.BinaryFormatCode, payload, &n) }()

    select {
    case err := <-done:
        fmt.Println("done", err, n.Valid)
    case <-time.After(5000 * time.Millisecond):
        fmt.Println("hang / excessive work")
    }
}
```

Please run with the race detector: `go run -race main.go`.

**Expected behavior**

Numeric decodes immediately to a valid value without rescaling to `dscale`, matching PostgreSQL `numeric_recv`.

**Actual behavior**

- Decoder performs ~21k sequential big.Int div/mul operations, often making the value 0.
- Trailing-zero loop can spin on zero (infinite loop).
- CPU spikes; program appears to hang.

**Version**

- Go: `go version` -> e.g. `go1.25.x darwin/arm64`
- PostgreSQL: `psql -c 'select version()'`
- pgx: `grep 'github.com/jackc/pgx/v[0-9]' go.mod` -> e.g. `v5.7.6`

**Additional context**

- PostgreSQL reference: `src/backend/utils/adt/numeric.c` (`numeric_recv`) — `dscale` is display-only; no rescale during decode.
- Minimal fix in pgx decoder:
  - Do not rescale by `dscale` in binary decode.
  - Guard trailing-zero removal with `accum != 0`.
  - If rescaling is needed elsewhere, use `big.Int.Exp` instead of per-digit loops.




Hints from issue comments:
@ilidemi: The SQL payload that results in this binary representation is `('67' || REPEAT('0', 44535) || '.0')::numeric` or `'6.7'::numeric * power('10'::numeric, 44536)`

Solution patch:
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 68fe8c6f..1b7616cc 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -71,10 +71,10 @@ jobs:
 
     steps:
       - name: Check out code into the Go module directory
-        uses: actions/checkout@v4
+        uses: actions/checkout@v5
 
       - name: Set up Go ${{ matrix.go-version }}
-        uses: actions/setup-go@v5
+        uses: actions/setup-go@v6
         with:
           go-version: ${{ matrix.go-version }}
 
@@ -135,10 +135,10 @@ jobs:
           database: pgx_test
 
       - name: Check out code into the Go module directory
-        uses: actions/checkout@v4
+        uses: actions/checkout@v5
 
       - name: Set up Go ${{ matrix.go-version }}
-        uses: actions/setup-go@v5
+        uses: actions/setup-go@v6
         with:
           go-version: ${{ matrix.go-version }}
 
@@ -150,7 +150,10 @@ jobs:
         shell: bash
 
       - name: Test
-        # parallel testing is disabled because somehow parallel testing causes Github Actions to kill the runner.
-        run: go test -parallel=1 -race  ./...
+        # Parallel testing is disabled because somehow parallel testing causes Github Actions to kill the runner.
+        # Disabling CGO because that is not working on the Windows runner. Apparently, we could install MingW and set up
+        # CGO to work, but disabling CGO takes less CI time. Without CGO, we also must not use the race detector.
+        run: go test -parallel=1 ./...
         env:
+          CGO_ENABLED: 0
           PGX_TEST_DATABASE: ${{ steps.postgres.outputs.connection-uri }}
diff --git a/conn.go b/conn.go
index 340bca5a..42e955fc 100644
--- a/conn.go
+++ b/conn.go
@@ -65,11 +65,12 @@ func (cc *ConnConfig) ConnString() string { return cc.connString }
 // Conn is a PostgreSQL connection handle. It is not safe for concurrent usage. Use a connection pool to manage access
 // to multiple database connections from multiple goroutines.
 type Conn struct {
-	pgConn             *pgconn.PgConn
-	config             *ConnConfig // config used when establishing this connection
-	preparedStatements map[string]*pgconn.StatementDescription
-	statementCache     stmtcache.Cache
-	descriptionCache   stmtcache.Cache
+	pgConn                  *pgconn.PgConn
+	config                  *ConnConfig // config used when establishing this connection
+	preparedStatements      map[string]*pgconn.StatementDescription
+	failedDescribeStatement string
+	statementCache          stmtcache.Cache
+	descriptionCache        stmtcache.Cache
 
 	queryTracer    QueryTracer
 	batchTracer    BatchTracer
@@ -314,6 +315,14 @@ func (c *Conn) Close(ctx context.Context) error {
 // Prepare is idempotent; i.e. it is safe to call Prepare multiple times with the same name and sql arguments. This
 // allows a code path to Prepare and Query/Exec without concern for if the statement has already been prepared.
 func (c *Conn) Prepare(ctx context.Context, name, sql string) (sd *pgconn.StatementDescription, err error) {
+	if c.failedDescribeStatement != "" {
+		err = c.Deallocate(ctx, c.failedDescribeStatement)
+		if err != nil {
+			return nil, fmt.Errorf("failed to deallocate previously failed statement %q: %w", c.failedDescribeStatement, err)
+		}
+		c.failedDescribeStatement = ""
+	}
+
 	if c.prepareTracer != nil {
 		ctx = c.prepareTracer.TracePrepareStart(ctx, c, TracePrepareStartData{Name: name, SQL: sql})
 	}
@@ -346,6 +355,10 @@ func (c *Conn) Prepare(ctx context.Context, name, sql string) (sd *pgconn.Statem
 
 	sd, err = c.pgConn.Prepare(ctx, psName, sql, nil)
 	if err != nil {
+		var pErr *pgconn.PrepareError
+		if errors.As(err, &pErr) {
+			c.failedDescribeStatement = psKey
+		}
 		return nil, err
 	}
 
diff --git a/internal/faultyconn/faultyconn.go b/internal/faultyconn/faultyconn.go
new file mode 100644
index 00000000..73d4a067
--- /dev/null
+++ b/internal/faultyconn/faultyconn.go
@@ -0,0 +1,99 @@
+package faultyconn
+
+import (
+	"bytes"
+	"io"
+	"net"
+	"time"
+
+	"github.com/jackc/pgx/v5/pgproto3"
+)
+
+// Conn is a wrapper for a net.Conn that allows inspection and modification of messages between a PostgreSQL client and
+// server. It is designed to be used in tests that use a *pgx.Conn or *pgconn.PgConn connected to a real PostgreSQL
+// server. Instead of mocking an entire server connection, this is used to specify and modify only the particular
+// aspects of a connection that are necessary. This can be easier to setup and is more true to real world conditions.
+//
+// It currently only supports handling frontend messages.
+type Conn struct {
+	// HandleFrontendMessage is called for each frontend message received. It should use backendWriter to write to the
+	// backend.
+	HandleFrontendMessage func(backendWriter io.Writer, msg pgproto3.FrontendMessage) error
+
+	// TODO: Implement this if we need to handle backend messages.
+	// HandleBackendMessage  func(w io.Writer, msg pgproto3.BackendMessage) error
+
+	conn            net.Conn
+	fromFrontendBuf *bytes.Buffer
+	fromBackendBuf  *bytes.Buffer
+	backend         *pgproto3.Backend
+	frontend        *pgproto3.Frontend
+}
+
+// NewConn creates a new Conn that proxies the messages sent to and from the given net.Conn. New can be used with
+// pgconn.Config.AfterNetConnect to wrap a net.Conn for testing purposes.
+func New(c net.Conn) *Conn {
+	fromFrontendBuf := &bytes.Buffer{}
+	fromBackendBuf := &bytes.Buffer{}
+
+	return &Conn{
+		conn:            c,
+		fromFrontendBuf: fromFrontendBuf,
+		fromBackendBuf:  fromBackendBuf,
+		backend:         pgproto3.NewBackend(fromFrontendBuf, c),
+		frontend:        pgproto3.NewFrontend(fromBackendBuf, c),
+	}
+}
+
+func (c *Conn) Read(b []byte) (n int, err error) {
+	return c.conn.Read(b)
+}
+
+func (c *Conn) Write(b []byte) (n int, err error) {
+	if c.HandleFrontendMessage == nil {
+		return c.conn.Write(b)
+	}
+
+	c.fromFrontendBuf.Write(b)
+
+	for {
+		msg, err := c.backend.Receive()
+		if err != nil {
+			if err == io.ErrUnexpectedEOF {
+				break
+			}
+			return len(b), err
+		}
+
+		err = c.HandleFrontendMessage(c.conn, msg)
+		if err != nil {
+			return len(b), err
+		}
+	}
+
+	return len(b), nil
+}
+
+func (c *Conn) Close() error {
+	return c.conn.Close()
+}
+
+func (c *Conn) LocalAddr() net.Addr {
+	return c.conn.LocalAddr()
+}
+
+func (c *Conn) RemoteAddr() net.Addr {
+	return c.conn.RemoteAddr()
+}
+
+func (c *Conn) SetDeadline(t time.Time) error {
+	return c.conn.SetDeadline(t)
+}
+
+func (c *Conn) SetReadDeadline(t time.Time) error {
+	return c.conn.SetReadDeadline(t)
+}
+
+func (c *Conn) SetWriteDeadline(t time.Time) error {
+	return c.conn.SetWriteDeadline(t)
+}
diff --git a/pgconn/config.go b/pgconn/config.go
index 3937dc40..3cd5d877 100644
--- a/pgconn/config.go
+++ b/pgconn/config.go
@@ -55,6 +55,13 @@ type Config struct {
 
 	SSLNegotiation string // sslnegotiation=postgres or sslnegotiation=direct
 
+	// AfterNetConnect is called after the network connection, including TLS if applicable, is established but before any
+	// PostgreSQL protocol communication. It takes the established net.Conn and returns a net.Conn that will be used in
+	// its place. It can be used to wrap the net.Conn (e.g. for logging, diagnostics, or testing). Its functionality has
+	// some overlap with DialFunc. However, DialFunc takes place before TLS is established and cannot be used to control
+	// the final net.Conn used for PostgreSQL protocol communication while AfterNetConnect can.
+	AfterNetConnect func(ctx context.Context, config *Config, conn net.Conn) (net.Conn, error)
+
 	// ValidateConnect is called during a connection attempt after a successful authentication with the PostgreSQL server.
 	// It can be used to validate that the server is acceptable. If this returns an error the connection is closed and the next
 	// fallback config is tried. This allows implementing high availability behavior such as libpq does with target_session_attrs.
diff --git a/pgconn/errors.go b/pgconn/errors.go
index d968d3f0..bc1e31e3 100644
--- a/pgconn/errors.go
+++ b/pgconn/errors.go
@@ -254,3 +254,20 @@ func (e *NotPreferredError) SafeToRetry() bool {
 func (e *NotPreferredError) Unwrap() error {
 	return e.err
 }
+
+type PrepareError struct {
+	err error
+
+	ParseComplete bool // Indicates whether the error occurred after a ParseComplete message was received.
+}
+
+func (e *PrepareError) Error() string {
+	if e.ParseComplete {
+		return fmt.Sprintf("prepare failed after ParseComplete: %s", e.err.Error())
+	}
+	return e.err.Error()
+}
+
+func (e *PrepareError) Unwrap() error {
+	return e.err
+}
diff --git a/pgconn/pgconn.go b/pgconn/pgconn.go
index 321656f9..5c77a836 100644
--- a/pgconn/pgconn.go
+++ b/pgconn/pgconn.go
@@ -343,6 +343,14 @@ func connectOne(ctx context.Context, config *Config, connectConfig *connectOneCo
 		pgConn.conn = tlsConn
 	}
 
+	if config.AfterNetConnect != nil {
+		pgConn.conn, err = config.AfterNetConnect(ctx, config, pgConn.conn)
+		if err != nil {
+			pgConn.conn.Close()
+			return nil, newPerDialConnectError("AfterNetConnect failed", err)
+		}
+	}
+
 	pgConn.contextWatcher = ctxwatch.NewContextWatcher(config.BuildContextWatcherHandler(pgConn))
 	pgConn.contextWatcher.Watch(ctx)
 	defer pgConn.contextWatcher.Unwatch()
@@ -858,6 +866,10 @@ type StatementDescription struct {
 //
 // Prepare does not send a PREPARE statement to the server. It uses the PostgreSQL Parse and Describe protocol messages
 // directly.
+//
+// In extremely rare cases, Prepare may fail after the Parse is successful, but before the Describe is complete. In this
+// case, the returned error will be an error where errors.As with a *PrepareError succeeds and the *PrepareError has
+// ParseComplete set to true.
 func (pgConn *PgConn) Prepare(ctx context.Context, name, sql string, paramOIDs []uint32) (*StatementDescription, error) {
 	if err := pgConn.lock(); err != nil {
 		return nil, err
@@ -885,7 +897,8 @@ func (pgConn *PgConn) Prepare(ctx context.Context, name, sql string, paramOIDs [
 
 	psd := &StatementDescription{Name: name, SQL: sql}
 
-	var parseErr error
+	var ParseComplete bool
+	var pgErr *PgError
 
 readloop:
 	for {
@@ -896,20 +909,22 @@ readloop:
 		}
 
 		switch msg := msg.(type) {
+		case *pgproto3.ParseComplete:
+			ParseComplete = true
 		case *pgproto3.ParameterDescription:
 			psd.ParamOIDs = make([]uint32, len(msg.ParameterOIDs))
 			copy(psd.ParamOIDs, msg.ParameterOIDs)
 		case *pgproto3.RowDescription:
 			psd.Fields = pgConn.convertRowDescription(nil, msg)
 		case *pgproto3.ErrorResponse:
-			parseErr = ErrorResponseToPgError(msg)
+			pgErr = ErrorResponseToPgError(msg)
 		case *pgproto3.ReadyForQuery:
 			break readloop
 		}
 	}
 
-	if parseErr != nil {
-		return nil, parseErr
+	if pgErr != nil {
+		return nil, &PrepareError{err: pgErr, ParseComplete: ParseComplete}
 	}
 	return psd, nil
 }
diff --git a/pgtype/numeric.go b/pgtype/numeric.go
index 7d236902..c9620428 100644
--- a/pgtype/numeric.go
+++ b/pgtype/numeric.go
@@ -28,7 +28,6 @@ const (
 )
 
 var (
-	big0    *big.Int = big.NewInt(0)
 	big1    *big.Int = big.NewInt(1)
 	big10   *big.Int = big.NewInt(10)
 	big100  *big.Int = big.NewInt(100)
@@ -166,7 +165,7 @@ func (n *Numeric) toBigInt() (*big.Int, error) {
 	div.Exp(big10, big.NewInt(int64(-n.Exp)), nil)
 	remainder := &big.Int{}
 	num.DivMod(num, div, remainder)
-	if remainder.Cmp(big0) != 0 {
+	if remainder.Sign() != 0 {
 		return nil, fmt.Errorf("cannot convert %v to integer", n)
 	}
 	return num, nil
@@ -409,7 +408,7 @@ func encodeNumericBinary(n Numeric, buf []byte) (newBuf []byte, err error) {
 	}
 
 	var sign int16
-	if n.Int.Cmp(big0) < 0 {
+	if n.Int.Sign() < 0 {
 		sign = 16384
 	}
 
@@ -447,12 +446,12 @@ func encodeNumericBinary(n Numeric, buf []byte) (newBuf []byte, err error) {
 
 	var wholeDigits, fracDigits []int16
 
-	for wholePart.Cmp(big0) != 0 {
+	for wholePart.Sign() != 0 {
 		wholePart.DivMod(wholePart, bigNBase, remainder)
 		wholeDigits = append(wholeDigits, int16(remainder.Int64()))
 	}
 
-	if fracPart.Cmp(big0) != 0 {
+	if fracPart.Sign() != 0 {
 		for fracPart.Cmp(big1) != 0 {
 			fracPart.DivMod(fracPart, bigNBase, remainder)
 			fracDigits = append(fracDigits, int16(remainder.Int64()))
@@ -658,17 +657,18 @@ func (scanPlanBinaryNumericToNumericScanner) Scan(src []byte, dst any) error {
 	exp := (int32(weight) - int32(ndigits) + 1) * 4
 
 	if dscale > 0 {
-		fracNBaseDigits := int16(int32(ndigits) - int32(weight) - 1)
+		fracNBaseDigits := int(ndigits) - int(weight) - 1
 		fracDecimalDigits := fracNBaseDigits * 4
+		dscaleInt := int(dscale)
 
-		if dscale > fracDecimalDigits {
-			multCount := int(dscale - fracDecimalDigits)
+		if dscaleInt > fracDecimalDigits {
+			multCount := dscaleInt - fracDecimalDigits
 			for i := 0; i < multCount; i++ {
 				accum.Mul(accum, big10)
 				exp--
 			}
-		} else if dscale < fracDecimalDigits {
-			divCount := int(fracDecimalDigits - dscale)
+		} else if dscaleInt < fracDecimalDigits {
+			divCount := fracDecimalDigits - dscaleInt
 			for i := 0; i < divCount; i++ {
 				accum.Div(accum, big10)
 				exp++
@@ -681,7 +681,7 @@ func (scanPlanBinaryNumericToNumericScanner) Scan(src []byte, dst any) error {
 	if exp >= 0 {
 		for {
 			reduced.DivMod(accum, big10, remainder)
-			if remainder.Cmp(big0) != 0 {
+			if remainder.Sign() != 0 {
 				break
 			}
 			accum.Set(reduced)



Test patch:
diff --git a/batch_test.go b/batch_test.go
index b4c421e5..d45ff526 100644
--- a/batch_test.go
+++ b/batch_test.go
@@ -4,12 +4,16 @@ import (
 	"context"
 	"errors"
 	"fmt"
+	"io"
+	"net"
 	"os"
 	"testing"
 	"time"
 
 	"github.com/jackc/pgx/v5"
+	"github.com/jackc/pgx/v5/internal/faultyconn"
 	"github.com/jackc/pgx/v5/pgconn"
+	"github.com/jackc/pgx/v5/pgproto3"
 	"github.com/jackc/pgx/v5/pgxtest"
 	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/require"
@@ -1060,6 +1064,73 @@ func TestSendBatchStatementTimeout(t *testing.T) {
 
 }
 
+func TestSendBatchHandlesTimeoutBetweenParseAndDescribe(t *testing.T) {
+	// Not parallel because it is a timing sensitive test.
+
+	ctx, cancel := context.WithTimeout(context.Background(), 120*time.Second)
+	defer cancel()
+
+	var faultyConn *faultyconn.Conn
+	faultyConnTestRunner := pgxtest.DefaultConnTestRunner()
+	faultyConnTestRunner.CreateConfig = func(ctx context.Context, t testing.TB) *pgx.ConnConfig {
+		config, err := pgx.ParseConfig(os.Getenv("PGX_TEST_DATABASE"))
+		require.NoError(t, err)
+		config.AfterNetConnect = func(ctx context.Context, config *pgconn.Config, conn net.Conn) (net.Conn, error) {
+			faultyConn = faultyconn.New(conn)
+			return faultyConn, nil
+		}
+		return config
+	}
+
+	// Only need to test modes that use Parse/Describe.
+	extendedQueryModes := []pgx.QueryExecMode{
+		pgx.QueryExecModeCacheStatement,
+		pgx.QueryExecModeCacheDescribe,
+		pgx.QueryExecModeDescribeExec,
+		pgx.QueryExecModeExec,
+	}
+
+	pgxtest.RunWithQueryExecModes(ctx, t, faultyConnTestRunner, extendedQueryModes, func(ctx context.Context, t testing.TB, conn *pgx.Conn) {
+		pgxtest.SkipCockroachDB(t, conn, "Induced error does not occur on CockroachDB")
+
+		_, err := conn.Exec(ctx, "set statement_timeout = '100ms'")
+		require.NoError(t, err)
+
+		batch := &pgx.Batch{}
+		batch.Queue("select 1")
+		batch.Queue("select 2")
+
+		faultyConn.HandleFrontendMessage = func(backendWriter io.Writer, msg pgproto3.FrontendMessage) error {
+			if _, ok := msg.(*pgproto3.Describe); ok {
+				time.Sleep(200 * time.Millisecond)
+			}
+			buf, err := msg.Encode(nil)
+			if err != nil {
+				return err
+			}
+			_, err = backendWriter.Write(buf)
+			return err
+		}
+
+		err = conn.SendBatch(ctx, batch).Close()
+		require.Error(t, err)
+		var pgErr *pgconn.PgError
+		require.True(t, errors.As(err, &pgErr))
+		require.Equal(t, "57014", pgErr.Code)
+
+		faultyConn.HandleFrontendMessage = nil
+
+		_, err = conn.Exec(ctx, "set statement_timeout = default")
+		require.NoError(t, err)
+
+		batch = &pgx.Batch{}
+		batch.Queue("select 1")
+		batch.Queue("select 2")
+		err = conn.SendBatch(ctx, batch).Close()
+		require.NoError(t, err)
+	})
+}
+
 func ExampleConn_SendBatch() {
 	ctx, cancel := context.WithTimeout(context.Background(), 120*time.Second)
 	defer cancel()
diff --git a/conn_test.go b/conn_test.go
index 10959f06..d570ad8b 100644
--- a/conn_test.go
+++ b/conn_test.go
@@ -4,6 +4,8 @@ import (
 	"bytes"
 	"context"
 	"database/sql"
+	"io"
+	"net"
 	"os"
 	"strings"
 	"sync"
@@ -11,7 +13,9 @@ import (
 	"time"
 
 	"github.com/jackc/pgx/v5"
+	"github.com/jackc/pgx/v5/internal/faultyconn"
 	"github.com/jackc/pgx/v5/pgconn"
+	"github.com/jackc/pgx/v5/pgproto3"
 	"github.com/jackc/pgx/v5/pgtype"
 	"github.com/jackc/pgx/v5/pgxtest"
 	"github.com/stretchr/testify/assert"
@@ -466,6 +470,69 @@ func TestPrepare(t *testing.T) {
 	}
 }
 
+// https://github.com/jackc/pgx/issues/2223
+func TestPrepareHandlesTimeoutBetweenParseAndDescribe(t *testing.T) {
+	// Not parallel because it is a timing sensitive test.
+
+	config, err := pgx.ParseConfig(os.Getenv("PGX_TEST_DATABASE"))
+	require.NoError(t, err)
+
+	var faultyConn *faultyconn.Conn
+	config.AfterNetConnect = func(ctx context.Context, config *pgconn.Config, conn net.Conn) (net.Conn, error) {
+		faultyConn = faultyconn.New(conn)
+		return faultyConn, nil
+	}
+
+	ctx := context.Background()
+	conn, err := pgx.ConnectConfig(ctx, config)
+	require.NoError(t, err)
+	defer closeConn(t, conn)
+	require.NotNil(t, faultyConn)
+
+	pgxtest.SkipCockroachDB(t, conn, "Induced error does not occur on CockroachDB")
+
+	_, err = conn.Exec(ctx, "set statement_timeout = '100ms'")
+	require.NoError(t, err)
+
+	faultyConn.HandleFrontendMessage = func(backendWriter io.Writer, msg pgproto3.FrontendMessage) error {
+		if _, ok := msg.(*pgproto3.Describe); ok {
+			time.Sleep(200 * time.Millisecond)
+		}
+		buf, err := msg.Encode(nil)
+		if err != nil {
+			return err
+		}
+		_, err = backendWriter.Write(buf)
+		return err
+	}
+
+	psd, err := conn.Prepare(ctx, "test", "select $1::varchar")
+	var pgErr *pgconn.PgError
+	require.ErrorAs(t, err, &pgErr)
+	require.Equal(t, "57014", pgErr.Code)
+	require.Nil(t, psd)
+
+	faultyConn.HandleFrontendMessage = nil
+
+	_, err = conn.Exec(ctx, "set statement_timeout = default")
+	require.NoError(t, err)
+
+	var existsOnServer bool
+	err = conn.QueryRow(
+		ctx,
+		"select exists(select 1 from pg_prepared_statements where name = 'test')",
+		// Avoid using the prepared statement cache or it will clear the broken statement before we can check for its
+		// existence.
+		pgx.QueryExecModeExec,
+	).Scan(&existsOnServer)
+	require.NoError(t, err)
+	require.True(t, existsOnServer)
+
+	psd, err = conn.Prepare(ctx, "test", "select $1::varchar")
+	require.NoError(t, err)
+	require.NotNil(t, psd)
+}
+
 func TestPrepareBadSQLFailure(t *testing.T) {
 	t.Parallel()
 
diff --git a/pgconn/pgconn_test.go b/pgconn/pgconn_test.go
index 4d6770f8..3663bc90 100644
--- a/pgconn/pgconn_test.go
+++ b/pgconn/pgconn_test.go
@@ -544,6 +544,63 @@ func TestConnectFailsWithResolveFailureAndFailedConnectionAttempts(t *testing.T)
 	require.ErrorContains(t, err, ":2 (127.0.0.1): dial error:")
 }
 
+type testConnWrapper struct {
+	conn net.Conn
+}
+
+func (w *testConnWrapper) Read(b []byte) (n int, err error) {
+	return w.conn.Read(b)
+}
+
+func (w *testConnWrapper) Write(b []byte) (n int, err error) {
+	return w.conn.Write(b)
+}
+
+func (w *testConnWrapper) Close() error {
+	return w.conn.Close()
+}
+
+func (w *testConnWrapper) LocalAddr() net.Addr {
+	return w.conn.LocalAddr()
+}
+
+func (w *testConnWrapper) RemoteAddr() net.Addr {
+	return w.conn.RemoteAddr()
+}
+
+func (w *testConnWrapper) SetDeadline(t time.Time) error {
+	return w.conn.SetDeadline(t)
+}
+
+func (w *testConnWrapper) SetReadDeadline(t time.Time) error {
+	return w.conn.SetReadDeadline(t)
+}
+
+func (w *testConnWrapper) SetWriteDeadline(t time.Time) error {
+	return w.conn.SetWriteDeadline(t)
+}
+
+func TestConnectWithAfterNetConnect(t *testing.T) {
+	t.Parallel()
+
+	ctx, cancel := context.WithTimeout(context.Background(), 120*time.Second)
+	defer cancel()
+
+	config, err := pgconn.ParseConfig(os.Getenv("PGX_TEST_DATABASE"))
+	require.NoError(t, err)
+
+	var wrappedConn net.Conn
+	config.AfterNetConnect = func(ctx context.Context, config *pgconn.Config, conn net.Conn) (net.Conn, error) {
+		wrappedConn = &testConnWrapper{conn: conn}
+		return wrappedConn, nil
+	}
+	conn, err := pgconn.ConnectConfig(ctx, config)
+	require.NoError(t, err)
+
+	require.Equal(t, wrappedConn, conn.Conn())
+	closeConn(t, conn)
+}
+
 func TestConnectWithValidateConnect(t *testing.T) {
 	t.Parallel()
 
@@ -2143,12 +2200,11 @@ func TestConnCopyFromConnectionTerminated(t *testing.T) {
 
 	closerConn, err := pgconn.Connect(ctx, os.Getenv("PGX_TEST_DATABASE"))
 	require.NoError(t, err)
+	defer closeConn(t, closerConn)
+	errChan := make(chan error, 1)
 	time.AfterFunc(500*time.Millisecond, func() {
-		// defer inside of AfterFunc instead of outer test function because outer function can finish while Read is still in
-		// progress which could cause closerConn to be closed too soon.
-		defer closeConn(t, closerConn)
 		err := closerConn.ExecParams(ctx, "select pg_terminate_backend($1)", [][]byte{[]byte(fmt.Sprintf("%d", pgConn.PID()))}, nil, nil, nil).Read().Err
-		require.NoError(t, err)
+		errChan <- err
 	})
 
 	_, err = pgConn.Exec(ctx, `create temporary table foo(
@@ -2181,6 +2237,9 @@ func TestConnCopyFromConnectionTerminated(t *testing.T) {
 	case <-time.After(5 * time.Second):
 		t.Fatal("Connection cleanup exceeded maximum time")
 	}
+
+	err = <-errChan
+	require.NoError(t, err)
 }
 
 func TestConnCopyFromGzipReader(t *testing.T) {
diff --git a/pgtype/numeric_test.go b/pgtype/numeric_test.go
index 6cf951f4..f94f9017 100644
--- a/pgtype/numeric_test.go
+++ b/pgtype/numeric_test.go
@@ -8,6 +8,7 @@ import (
 	"math/rand"
 	"reflect"
 	"strconv"
+	"strings"
 	"testing"
 
 	pgx "github.com/jackc/pgx/v5"
@@ -87,6 +88,7 @@ func TestNumericCodec(t *testing.T) {
 		{mustParseNumeric(t, "4237234789234789289347892374324872138321894178943189043890124832108934.43219085471578891547854892438945012347981"), new(pgtype.Numeric), isExpectedEqNumeric(mustParseNumeric(t, "4237234789234789289347892374324872138321894178943189043890124832108934.43219085471578891547854892438945012347981"))},
 		{mustParseNumeric(t, "0.8925092023480223478923478978978937897879595901237890234789243679037419057877231734823098432903527585734549035904590854890345905434578345789347890402348952348905890489054234237489234987723894789234"), new(pgtype.Numeric), isExpectedEqNumeric(mustParseNumeric(t, "0.8925092023480223478923478978978937897879595901237890234789243679037419057877231734823098432903527585734549035904590854890345905434578345789347890402348952348905890489054234237489234987723894789234"))},
 		{mustParseNumeric(t, "0.000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000123"), new(pgtype.Numeric), isExpectedEqNumeric(mustParseNumeric(t, "0.000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000123"))},
+		{mustParseNumeric(t, "67"+strings.Repeat("0", 44535)+".0"), new(pgtype.Numeric), isExpectedEqNumeric(mustParseNumeric(t, "67"+strings.Repeat("0", 44535)+".0"))},
 		{pgtype.Numeric{Int: mustParseBigInt(t, "243723409723490243842378942378901237502734019231380123"), Exp: 23790, Valid: true}, new(pgtype.Numeric), isExpectedEqNumeric(pgtype.Numeric{Int: mustParseBigInt(t, "243723409723490243842378942378901237502734019231380123"), Exp: 23790, Valid: true})},
 		{pgtype.Numeric{Int: mustParseBigInt(t, "2437"), Exp: 23790, Valid: true}, new(pgtype.Numeric), isExpectedEqNumeric(pgtype.Numeric{Int: mustParseBigInt(t, "2437"), Exp: 23790, Valid: true})},
 		{pgtype.Numeric{Int: mustParseBigInt(t, "43723409723490243842378942378901237502734019231380123"), Exp: 80, Valid: true}, new(pgtype.Numeric), isExpectedEqNumeric(pgtype.Numeric{Int: mustParseBigInt(t, "43723409723490243842378942378901237502734019231380123"), Exp: 80, Valid: true})},
diff --git a/stdlib/sql_test.go b/stdlib/sql_test.go
index 84f48f6c..f9d441fe 100644
--- a/stdlib/sql_test.go
+++ b/stdlib/sql_test.go
@@ -5,6 +5,7 @@ import (
 	"context"
 	"database/sql"
 	"encoding/json"
+	"errors"
 	"fmt"
 	"math"
 	"os"
@@ -511,7 +512,7 @@ func TestConnQueryFailure(t *testing.T) {
 	testWithAllQueryExecModes(t, func(t *testing.T, db *sql.DB) {
 		_, err := db.Query("select 'foo")
 		require.Error(t, err)
-		require.IsType(t, new(pgconn.PgError), err)
+		require.ErrorAs(t, err, new(*pgconn.PgError))
 	})
 }
 
@@ -755,7 +756,8 @@ func TestBeginTxContextCancel(t *testing.T) {
 
 		var n int
 		err = db.QueryRow("select count(*) from t").Scan(&n)
-		if pgErr, ok := err.(*pgconn.PgError); !ok || pgErr.Code != "42P01" {
+		var pgErr *pgconn.PgError
+		if !errors.As(err, &pgErr) || pgErr.Code != "42P01" {
 			t.Fatalf(`err => %v, want PgError{Code: "42P01"}`, err)
 		}
 	})