Skip to content

Commit 94c5bbb

Browse files
committed
Correctly follow protocol spec about errors in extended protocol
The Postgres protocol spec describes that if an error occurs during an extended protocol message, then all other received messages will be ignored until the next Sync message. This was not the way that psql-wire was handling this. I ran into this because I updated to version of pgx that contained this change, which caused some of our tests to suddenly fail: jackc/pgx#2422 This changes the behaviour of psql-wire to match the protocol spec.
1 parent 4837c68 commit 94c5bbb

14 files changed

Lines changed: 596 additions & 60 deletions

auth.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@ func ClearTextPassword(validate func(ctx context.Context, database, username, pa
7878

7979
if !valid {
8080
authErr := pgerror.WithCode(errors.New("invalid username/password"), codes.InvalidPassword)
81-
err = ErrorCode(writer, authErr)
81+
err = RawErrorCode(writer, authErr)
8282
if err != nil {
8383
return ctx, err
8484
}

cache.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -130,7 +130,8 @@ func (cache *DefaultPortalCache) Execute(ctx context.Context, name string, limit
130130
return nil
131131
}
132132

133-
return portal.statement.fn(ctx, NewDataWriter(ctx, portal.statement.columns, portal.formats, limit, reader, writer), portal.parameters)
133+
session, _ := GetSession(ctx)
134+
return portal.statement.fn(ctx, NewDataWriter(ctx, session, portal.statement.columns, portal.formats, limit, reader, writer), portal.parameters)
134135
}
135136

136137
func (cache *DefaultPortalCache) Close() {}

command.go

Lines changed: 89 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,30 @@ type Session struct {
6060
// pipelining
6161
ParallelPipeline ParallelPipelineConfig
6262
ResponseQueue *ResponseQueue
63+
64+
// inExtendedQuery is true when the current message being handled is an
65+
// extended query protocol message (Parse, Bind, Describe, Execute, Close).
66+
// This lets Session.ErrorCode behave correctly for both protocols.
67+
inExtendedQuery bool
68+
69+
// discardUntilSync is set when an error occurs during extended query
70+
// processing. Per the PostgreSQL protocol, after an error the server must
71+
// discard messages until it receives a Sync, then respond with
72+
// ReadyForQuery.
73+
discardUntilSync bool
74+
}
75+
76+
// isExtendedQueryMessage returns true for message types that belong to the
77+
// extended query protocol and should not emit ReadyForQuery on error.
78+
func isExtendedQueryMessage(t types.ClientMessage) bool {
79+
switch t {
80+
case types.ClientParse, types.ClientBind, types.ClientDescribe,
81+
types.ClientExecute, types.ClientClose,
82+
types.ClientSync, types.ClientFlush:
83+
return true
84+
default:
85+
return false
86+
}
6387
}
6488

6589
// consumeCommands consumes incoming commands sent over the Postgres wire connection.
@@ -90,9 +114,11 @@ func (srv *Session) consumeSingleCommand(ctx context.Context, reader *buffer.Rea
90114
return err
91115
}
92116

117+
srv.inExtendedQuery = isExtendedQueryMessage(t)
118+
93119
// NOTE: we could recover from this scenario
94120
if errors.Is(err, buffer.ErrMessageSizeExceeded) {
95-
err = handleMessageSizeExceeded(reader, writer, err)
121+
err = srv.handleMessageSizeExceeded(reader, writer, err)
96122
if err != nil {
97123
return err
98124
}
@@ -130,7 +156,7 @@ func (srv *Session) consumeSingleCommand(ctx context.Context, reader *buffer.Rea
130156
// type. A fatal error is returned when an unexpected error is returned while
131157
// consuming the expected message size or when attempting to write the error
132158
// message back to the client.
133-
func handleMessageSizeExceeded(reader *buffer.Reader, writer *buffer.Writer, exceeded error) (err error) {
159+
func (srv *Session) handleMessageSizeExceeded(reader *buffer.Reader, writer *buffer.Writer, exceeded error) (err error) {
134160
unwrapped, has := buffer.UnwrapMessageSizeExceeded(exceeded)
135161
if !has {
136162
return exceeded
@@ -141,14 +167,21 @@ func handleMessageSizeExceeded(reader *buffer.Reader, writer *buffer.Writer, exc
141167
return err
142168
}
143169

144-
return ErrorCode(writer, exceeded)
170+
return srv.ErrorCode(writer, exceeded)
145171
}
146172

147173
// handleCommand handles the given client message. A client message includes a
148174
// message type and reader buffer containing the actual message. The type
149175
// indicates a action executed by the client.
150176
// https://www.postgresql.org/docs/14/protocol-message-formats.html
151177
func (srv *Session) handleCommand(ctx context.Context, conn net.Conn, t types.ClientMessage, reader *buffer.Reader, writer *buffer.Writer) error {
178+
// Per the PostgreSQL protocol, after an error during extended query
179+
// processing the server discards all messages until it receives a Sync.
180+
if srv.discardUntilSync && t != types.ClientSync && t != types.ClientTerminate {
181+
srv.logger.Debug("discarding message until sync", slog.String("type", t.String()))
182+
return nil
183+
}
184+
152185
switch t {
153186
case types.ClientSimpleQuery:
154187
return srv.handleSimpleQuery(ctx, reader, writer)
@@ -232,9 +265,7 @@ func (srv *Session) handleCommand(ctx context.Context, conn net.Conn, t types.Cl
232265
// https://github.com/postgres/postgres/blob/6e1dd2773eb60a6ab87b27b8d9391b756e904ac3/src/backend/tcop/postgres.c#L4295
233266
return nil
234267
case types.ClientClose:
235-
writer.Start(types.ServerCloseComplete) //nolint:errcheck
236-
writer.End() //nolint:errcheck
237-
return nil
268+
return srv.handleClose(ctx, reader, writer)
238269
case types.ClientTerminate:
239270
err := srv.handleConnTerminate(ctx)
240271
if err != nil {
@@ -248,13 +279,13 @@ func (srv *Session) handleCommand(ctx context.Context, conn net.Conn, t types.Cl
248279

249280
return io.EOF
250281
default:
251-
return ErrorCode(writer, NewErrUnimplementedMessageType(t))
282+
return srv.ErrorCode(writer, NewErrUnimplementedMessageType(t))
252283
}
253284
}
254285

255286
func (srv *Session) handleSimpleQuery(ctx context.Context, reader *buffer.Reader, writer *buffer.Writer) error {
256287
if srv.parse == nil {
257-
return ErrorCode(writer, NewErrUnimplementedMessageType(types.ClientSimpleQuery))
288+
return srv.ErrorCode(writer, NewErrUnimplementedMessageType(types.ClientSimpleQuery))
258289
}
259290

260291
query, err := reader.GetString()
@@ -280,23 +311,23 @@ func (srv *Session) handleSimpleQuery(ctx context.Context, reader *buffer.Reader
280311

281312
statements, err := srv.parse(ctx, query)
282313
if err != nil {
283-
return ErrorCode(writer, err)
314+
return srv.ErrorCode(writer, err)
284315
}
285316

286317
if len(statements) == 0 {
287-
return ErrorCode(writer, NewErrUndefinedStatement())
318+
return srv.ErrorCode(writer, NewErrUndefinedStatement())
288319
}
289320

290321
// NOTE: it is possible to send multiple statements in one simple query.
291322
for index := range statements {
292323
err = statements[index].columns.Define(ctx, writer, nil)
293324
if err != nil {
294-
return ErrorCode(writer, err)
325+
return srv.ErrorCode(writer, err)
295326
}
296327

297-
err = statements[index].fn(ctx, NewDataWriter(ctx, statements[index].columns, nil, NoLimit, reader, writer), nil)
328+
err = statements[index].fn(ctx, NewDataWriter(ctx, srv, statements[index].columns, nil, NoLimit, reader, writer), nil)
298329
if err != nil {
299-
return ErrorCode(writer, err)
330+
return srv.ErrorCode(writer, err)
300331
}
301332
}
302333

@@ -309,7 +340,7 @@ func (srv *Session) handleParse(ctx context.Context, reader *buffer.Reader, writ
309340
if srv.ParallelPipeline.Enabled {
310341
return srv.drainQueueAndWriteError(ctx, writer, err)
311342
}
312-
return ErrorCode(writer, err)
343+
return srv.ErrorCode(writer, err)
313344
}
314345

315346
name, err := reader.GetString()
@@ -347,14 +378,14 @@ func (srv *Session) handleParse(ctx context.Context, reader *buffer.Reader, writ
347378

348379
statement, err := singleStatement(srv.parse(ctx, query))
349380
if err != nil {
350-
return ErrorCode(writer, err)
381+
return srv.ErrorCode(writer, err)
351382
}
352383

353384
srv.logger.Debug("incoming extended query", slog.String("query", query), slog.String("name", name), slog.Int("parameters", len(statement.parameters)))
354385

355386
err = srv.Statements.Set(ctx, name, statement)
356387
if err != nil {
357-
return ErrorCode(writer, err)
388+
return srv.ErrorCode(writer, err)
358389
}
359390

360391
writer.Start(types.ServerParseComplete)
@@ -404,7 +435,7 @@ func (srv *Session) handleDescribe(ctx context.Context, reader *buffer.Reader, w
404435
}
405436

406437
if statement == nil {
407-
return ErrorCode(writer, errors.New("unknown statement"))
438+
return srv.ErrorCode(writer, errors.New("unknown statement"))
408439
}
409440

410441
if err := srv.writeParameterDescription(writer, statement.parameters); err != nil {
@@ -419,13 +450,13 @@ func (srv *Session) handleDescribe(ctx context.Context, reader *buffer.Reader, w
419450
}
420451

421452
if portal == nil {
422-
return ErrorCode(writer, errors.New("unknown portal"))
453+
return srv.ErrorCode(writer, errors.New("unknown portal"))
423454
}
424455

425456
return srv.writeColumnDescription(ctx, writer, portal.formats, portal.statement.columns)
426457
}
427458

428-
return ErrorCode(writer, fmt.Errorf("unknown describe command: %s", string(d[0])))
459+
return srv.ErrorCode(writer, fmt.Errorf("unknown describe command: %s", string(d[0])))
429460
}
430461

431462
// describePipelined handles Describe in parallel pipeline mode
@@ -644,7 +675,7 @@ func (srv *Session) handleExecute(ctx context.Context, reader *buffer.Reader, wr
644675
if srv.ParallelPipeline.Enabled {
645676
return srv.drainQueueAndWriteError(ctx, writer, err)
646677
}
647-
return ErrorCode(writer, err)
678+
return srv.ErrorCode(writer, err)
648679
}
649680

650681
name, err := reader.GetString()
@@ -667,12 +698,37 @@ func (srv *Session) handleExecute(ctx context.Context, reader *buffer.Reader, wr
667698

668699
err = srv.Portals.Execute(ctx, name, Limit(limit), reader, writer)
669700
if err != nil {
670-
return ErrorCode(writer, err)
701+
return srv.ErrorCode(writer, err)
671702
}
672703

673704
return nil
674705
}
675706

707+
// handleClose handles the Close message (extended query protocol).
708+
// The Close message tells the server to close a prepared statement or portal.
709+
// https://www.postgresql.org/docs/current/protocol-message-formats.html
710+
func (srv *Session) handleClose(ctx context.Context, reader *buffer.Reader, writer *buffer.Writer) error {
711+
d, err := reader.GetBytes(1)
712+
if err != nil {
713+
return err
714+
}
715+
716+
name, err := reader.GetString()
717+
if err != nil {
718+
return err
719+
}
720+
721+
srv.logger.Debug("incoming close request", slog.String("type", string(d[0])), slog.String("name", name))
722+
723+
if srv.ParallelPipeline.Enabled {
724+
srv.ResponseQueue.Enqueue(NewCloseCompleteEvent())
725+
return nil
726+
}
727+
728+
writer.Start(types.ServerCloseComplete)
729+
return writer.End()
730+
}
731+
676732
// executePipelined handles Execute in parallel pipeline mode
677733
func (srv *Session) executePipelined(ctx context.Context, writer *buffer.Writer, name string, limit uint32) error {
678734
portal, err := srv.Portals.Get(ctx, name)
@@ -732,6 +788,9 @@ func (srv *Session) handleSync(ctx context.Context, writer *buffer.Writer) error
732788
}
733789
}
734790

791+
// Sync always resets discardUntilSync, even if queue processing set it.
792+
srv.discardUntilSync = false
793+
735794
// Original synchronous behavior - just return ReadyForQuery
736795
return readyForQuery(writer, types.ServerIdle)
737796
}
@@ -747,7 +806,7 @@ func (srv *Session) processResponseQueue(ctx context.Context, writer *buffer.Wri
747806
}
748807

749808
if queueErr != nil {
750-
if err := ErrorCode(writer, queueErr); err != nil {
809+
if err := srv.ErrorCode(writer, queueErr); err != nil {
751810
return err
752811
}
753812
}
@@ -782,7 +841,7 @@ func (srv *Session) drainQueueOnError(ctx context.Context, writer *buffer.Writer
782841
srv.ResponseQueue.Clear()
783842

784843
if queueErr != nil {
785-
return ErrorCode(writer, queueErr)
844+
return srv.ErrorCode(writer, queueErr)
786845
}
787846

788847
return nil
@@ -794,7 +853,7 @@ func (srv *Session) drainQueueAndWriteError(ctx context.Context, writer *buffer.
794853
if drainErr := srv.drainQueueOnError(ctx, writer); drainErr != nil {
795854
return drainErr
796855
}
797-
return ErrorCode(writer, err)
856+
return srv.ErrorCode(writer, err)
798857
}
799858

800859
func singleStatement(stmts PreparedStatements, err error) (*PreparedStatement, error) {
@@ -824,6 +883,10 @@ func (srv *Session) writeQueuedResponse(ctx context.Context, writer *buffer.Writ
824883
writer.Start(types.ServerBindComplete)
825884
return writer.End()
826885

886+
case ResponseCloseComplete:
887+
writer.Start(types.ServerCloseComplete)
888+
return writer.End()
889+
827890
case ResponseStmtDescribe:
828891
// Statement Describe writes ParameterDescription followed by RowDescription
829892
if err := srv.writeParameterDescription(writer, event.Parameters); err != nil {
@@ -845,12 +908,12 @@ func (srv *Session) writeQueuedResponse(ctx context.Context, writer *buffer.Writ
845908

846909
// Check for execution error
847910
if err := event.Result.GetError(); err != nil {
848-
return ErrorCode(writer, err)
911+
return srv.ErrorCode(writer, err)
849912
}
850913

851914
// Use DataWriter for correct encoding
852915
// Note: We use NoLimit here because the result is already limited during execution
853-
dataWriter := NewDataWriter(ctx, event.Result.Columns(), event.Formats, NoLimit, nil, writer)
916+
dataWriter := NewDataWriter(ctx, srv, event.Result.Columns(), event.Formats, NoLimit, nil, writer)
854917

855918
return event.Result.Replay(ctx, dataWriter)
856919

command_bind_test.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@ func TestHandleBind_ParallelPipeline_Error(t *testing.T) {
7575
Portals: &DefaultPortalCache{},
7676
ParallelPipeline: ParallelPipelineConfig{Enabled: true},
7777
ResponseQueue: NewResponseQueue(),
78+
inExtendedQuery: true,
7879
}
7980

8081
// Enqueue a previous event

0 commit comments

Comments
 (0)