Skip to content

Commit 92c95a2

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 92c95a2

14 files changed

Lines changed: 514 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: 90 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,31 @@ 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+
// Flush, Sync). This lets Session.ErrorCode behave correctly for both
67+
// protocols.
68+
inExtendedQuery bool
69+
70+
// discardUntilSync is set when an error occurs during extended query
71+
// processing. Per the PostgreSQL protocol, after an error the server must
72+
// discard messages until it receives a Sync, then respond with
73+
// ReadyForQuery.
74+
discardUntilSync bool
75+
}
76+
77+
// isExtendedQueryMessage returns true for message types that belong to the
78+
// extended query protocol and should not emit ReadyForQuery on error.
79+
func isExtendedQueryMessage(t types.ClientMessage) bool {
80+
switch t {
81+
case types.ClientParse, types.ClientBind, types.ClientDescribe,
82+
types.ClientExecute, types.ClientClose,
83+
types.ClientSync, types.ClientFlush:
84+
return true
85+
default:
86+
return false
87+
}
6388
}
6489

6590
// consumeCommands consumes incoming commands sent over the Postgres wire connection.
@@ -90,9 +115,11 @@ func (srv *Session) consumeSingleCommand(ctx context.Context, reader *buffer.Rea
90115
return err
91116
}
92117

118+
srv.inExtendedQuery = isExtendedQueryMessage(t)
119+
93120
// NOTE: we could recover from this scenario
94121
if errors.Is(err, buffer.ErrMessageSizeExceeded) {
95-
err = handleMessageSizeExceeded(reader, writer, err)
122+
err = srv.handleMessageSizeExceeded(reader, writer, err)
96123
if err != nil {
97124
return err
98125
}
@@ -130,7 +157,7 @@ func (srv *Session) consumeSingleCommand(ctx context.Context, reader *buffer.Rea
130157
// type. A fatal error is returned when an unexpected error is returned while
131158
// consuming the expected message size or when attempting to write the error
132159
// message back to the client.
133-
func handleMessageSizeExceeded(reader *buffer.Reader, writer *buffer.Writer, exceeded error) (err error) {
160+
func (srv *Session) handleMessageSizeExceeded(reader *buffer.Reader, writer *buffer.Writer, exceeded error) (err error) {
134161
unwrapped, has := buffer.UnwrapMessageSizeExceeded(exceeded)
135162
if !has {
136163
return exceeded
@@ -141,14 +168,21 @@ func handleMessageSizeExceeded(reader *buffer.Reader, writer *buffer.Writer, exc
141168
return err
142169
}
143170

144-
return ErrorCode(writer, exceeded)
171+
return srv.ErrorCode(writer, exceeded)
145172
}
146173

147174
// handleCommand handles the given client message. A client message includes a
148175
// message type and reader buffer containing the actual message. The type
149176
// indicates a action executed by the client.
150177
// https://www.postgresql.org/docs/14/protocol-message-formats.html
151178
func (srv *Session) handleCommand(ctx context.Context, conn net.Conn, t types.ClientMessage, reader *buffer.Reader, writer *buffer.Writer) error {
179+
// Per the PostgreSQL protocol, after an error during extended query
180+
// processing the server discards all messages until it receives a Sync.
181+
if srv.discardUntilSync && t != types.ClientSync && t != types.ClientTerminate {
182+
srv.logger.Debug("discarding message until sync", slog.String("type", t.String()))
183+
return nil
184+
}
185+
152186
switch t {
153187
case types.ClientSimpleQuery:
154188
return srv.handleSimpleQuery(ctx, reader, writer)
@@ -232,9 +266,7 @@ func (srv *Session) handleCommand(ctx context.Context, conn net.Conn, t types.Cl
232266
// https://github.com/postgres/postgres/blob/6e1dd2773eb60a6ab87b27b8d9391b756e904ac3/src/backend/tcop/postgres.c#L4295
233267
return nil
234268
case types.ClientClose:
235-
writer.Start(types.ServerCloseComplete) //nolint:errcheck
236-
writer.End() //nolint:errcheck
237-
return nil
269+
return srv.handleClose(ctx, reader, writer)
238270
case types.ClientTerminate:
239271
err := srv.handleConnTerminate(ctx)
240272
if err != nil {
@@ -248,13 +280,13 @@ func (srv *Session) handleCommand(ctx context.Context, conn net.Conn, t types.Cl
248280

249281
return io.EOF
250282
default:
251-
return ErrorCode(writer, NewErrUnimplementedMessageType(t))
283+
return srv.ErrorCode(writer, NewErrUnimplementedMessageType(t))
252284
}
253285
}
254286

255287
func (srv *Session) handleSimpleQuery(ctx context.Context, reader *buffer.Reader, writer *buffer.Writer) error {
256288
if srv.parse == nil {
257-
return ErrorCode(writer, NewErrUnimplementedMessageType(types.ClientSimpleQuery))
289+
return srv.ErrorCode(writer, NewErrUnimplementedMessageType(types.ClientSimpleQuery))
258290
}
259291

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

281313
statements, err := srv.parse(ctx, query)
282314
if err != nil {
283-
return ErrorCode(writer, err)
315+
return srv.ErrorCode(writer, err)
284316
}
285317

286318
if len(statements) == 0 {
287-
return ErrorCode(writer, NewErrUndefinedStatement())
319+
return srv.ErrorCode(writer, NewErrUndefinedStatement())
288320
}
289321

290322
// NOTE: it is possible to send multiple statements in one simple query.
291323
for index := range statements {
292324
err = statements[index].columns.Define(ctx, writer, nil)
293325
if err != nil {
294-
return ErrorCode(writer, err)
326+
return srv.ErrorCode(writer, err)
295327
}
296328

297-
err = statements[index].fn(ctx, NewDataWriter(ctx, statements[index].columns, nil, NoLimit, reader, writer), nil)
329+
err = statements[index].fn(ctx, NewDataWriter(ctx, srv, statements[index].columns, nil, NoLimit, reader, writer), nil)
298330
if err != nil {
299-
return ErrorCode(writer, err)
331+
return srv.ErrorCode(writer, err)
300332
}
301333
}
302334

@@ -309,7 +341,7 @@ func (srv *Session) handleParse(ctx context.Context, reader *buffer.Reader, writ
309341
if srv.ParallelPipeline.Enabled {
310342
return srv.drainQueueAndWriteError(ctx, writer, err)
311343
}
312-
return ErrorCode(writer, err)
344+
return srv.ErrorCode(writer, err)
313345
}
314346

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

348380
statement, err := singleStatement(srv.parse(ctx, query))
349381
if err != nil {
350-
return ErrorCode(writer, err)
382+
return srv.ErrorCode(writer, err)
351383
}
352384

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

355387
err = srv.Statements.Set(ctx, name, statement)
356388
if err != nil {
357-
return ErrorCode(writer, err)
389+
return srv.ErrorCode(writer, err)
358390
}
359391

360392
writer.Start(types.ServerParseComplete)
@@ -404,7 +436,7 @@ func (srv *Session) handleDescribe(ctx context.Context, reader *buffer.Reader, w
404436
}
405437

406438
if statement == nil {
407-
return ErrorCode(writer, errors.New("unknown statement"))
439+
return srv.ErrorCode(writer, errors.New("unknown statement"))
408440
}
409441

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

421453
if portal == nil {
422-
return ErrorCode(writer, errors.New("unknown portal"))
454+
return srv.ErrorCode(writer, errors.New("unknown portal"))
423455
}
424456

425457
return srv.writeColumnDescription(ctx, writer, portal.formats, portal.statement.columns)
426458
}
427459

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

431463
// describePipelined handles Describe in parallel pipeline mode
@@ -644,7 +676,7 @@ func (srv *Session) handleExecute(ctx context.Context, reader *buffer.Reader, wr
644676
if srv.ParallelPipeline.Enabled {
645677
return srv.drainQueueAndWriteError(ctx, writer, err)
646678
}
647-
return ErrorCode(writer, err)
679+
return srv.ErrorCode(writer, err)
648680
}
649681

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

668700
err = srv.Portals.Execute(ctx, name, Limit(limit), reader, writer)
669701
if err != nil {
670-
return ErrorCode(writer, err)
702+
return srv.ErrorCode(writer, err)
671703
}
672704

673705
return nil
674706
}
675707

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

792+
// Sync always resets discardUntilSync, even if queue processing set it.
793+
srv.discardUntilSync = false
794+
735795
// Original synchronous behavior - just return ReadyForQuery
736796
return readyForQuery(writer, types.ServerIdle)
737797
}
@@ -747,7 +807,7 @@ func (srv *Session) processResponseQueue(ctx context.Context, writer *buffer.Wri
747807
}
748808

749809
if queueErr != nil {
750-
if err := ErrorCode(writer, queueErr); err != nil {
810+
if err := srv.ErrorCode(writer, queueErr); err != nil {
751811
return err
752812
}
753813
}
@@ -782,7 +842,7 @@ func (srv *Session) drainQueueOnError(ctx context.Context, writer *buffer.Writer
782842
srv.ResponseQueue.Clear()
783843

784844
if queueErr != nil {
785-
return ErrorCode(writer, queueErr)
845+
return srv.ErrorCode(writer, queueErr)
786846
}
787847

788848
return nil
@@ -794,7 +854,7 @@ func (srv *Session) drainQueueAndWriteError(ctx context.Context, writer *buffer.
794854
if drainErr := srv.drainQueueOnError(ctx, writer); drainErr != nil {
795855
return drainErr
796856
}
797-
return ErrorCode(writer, err)
857+
return srv.ErrorCode(writer, err)
798858
}
799859

800860
func singleStatement(stmts PreparedStatements, err error) (*PreparedStatement, error) {
@@ -824,6 +884,10 @@ func (srv *Session) writeQueuedResponse(ctx context.Context, writer *buffer.Writ
824884
writer.Start(types.ServerBindComplete)
825885
return writer.End()
826886

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

846910
// Check for execution error
847911
if err := event.Result.GetError(); err != nil {
848-
return ErrorCode(writer, err)
912+
return srv.ErrorCode(writer, err)
849913
}
850914

851915
// Use DataWriter for correct encoding
852916
// 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)
917+
dataWriter := NewDataWriter(ctx, srv, event.Result.Columns(), event.Formats, NoLimit, nil, writer)
854918

855919
return event.Result.Replay(ctx, dataWriter)
856920

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)