Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions conn.go
Original file line number Diff line number Diff line change
Expand Up @@ -1029,6 +1029,24 @@ func (cn *conn) saveMessage(typ proto.ResponseCode, buf *readBuf) error {
return nil
}

// maxErrorLength matches libpq's MAX_ERRLEN for detecting pre-protocol errors.
const maxErrorLength = 30000

// readPreProtocolError reads a pre-protocol plain text error message.
// When PostgreSQL cannot start a backend (e.g., an external process limit),
// it sends plain text like "Ecould not fork new process for connection: ..."
// The 'E' looks like an ErrorResponse type, but the rest is plain text.
func readPreProtocolError(buf *bufio.Reader, header []byte) error {
// Start with the 4 "length" bytes which are actually message text
msg := string(header[1:])

// Read remaining text until null terminator
rest, _ := buf.ReadString('\x00')
msg += strings.TrimSuffix(rest, "\x00")

return fmt.Errorf("server error: %s", msg)
}

// recvMessage receives any message from the backend, or returns an error if
// a problem occurred while reading the message.
func (cn *conn) recvMessage(r *readBuf) (proto.ResponseCode, error) {
Expand All @@ -1050,6 +1068,17 @@ func (cn *conn) recvMessage(r *readBuf) (proto.ResponseCode, error) {
// read the type and length of the message that follows
t := x[0]
n := int(binary.BigEndian.Uint32(x[1:])) - 4

// Detect pre-protocol errors, matching libpq's behavior (fe-connect.c).
// When PostgreSQL cannot start a backend (e.g., an external process limit),
// it sends plain text like "Ecould not fork new process...". The 'E' gets
// parsed as an ErrorResponse type, but the "length" is actually text.
// libpq checks: if ErrorResponse && (msgLength < 8 || msgLength > 30000),
// here we check < 4 since n represents bytes remaining to be read after length
if t == 'E' && (n < 4 || n > maxErrorLength) {
return 0, readPreProtocolError(cn.buf, x)
}

var y []byte
if n <= len(cn.scratch) {
y = cn.scratch[:n]
Expand Down
48 changes: 48 additions & 0 deletions conn_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2165,3 +2165,51 @@ func TestUint64(t *testing.T) {
}
}
}

func TestPreProtocolError(t *testing.T) {
t.Parallel()

tests := []struct {
name string
msg string
wantErr string
}{
{
name: "could not fork",
msg: "could not fork new process for connection: Resource temporarily unavailable\n",
wantErr: "server error: could not fork new process for connection: Resource temporarily unavailable",
},
{
name: "too many connections",
msg: "sorry, too many clients already\n",
wantErr: "server error: sorry, too many clients already",
},
{
name: "out of memory",
msg: "out of memory\n",
wantErr: "server error: out of memory",
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
f := pqtest.NewFake(t, func(f pqtest.Fake, cn net.Conn) {
f.ReadStartup(cn)
// Send pre-protocol error: 'E' followed by plain text
// This simulates what PostgreSQL sends when it can't fork
cn.Write(append([]byte{'E'}, tt.msg...))
cn.Close()
})
defer f.Close()

db := pqtest.MustDB(t, f.DSN())
err := db.Ping()
if err == nil {
t.Fatal("expected error")
}
if !strings.Contains(err.Error(), tt.wantErr) {
t.Errorf("wrong error:\nhave: %s\nwant: %s", err, tt.wantErr)
}
})
}
}
Loading