From 3520806d1ff2304d95cfffdb1038c2d612205756 Mon Sep 17 00:00:00 2001 From: Brandur Date: Thu, 27 Jun 2024 22:27:21 -0700 Subject: [PATCH] Interpret some types of Postgres errors to be user facing A partial fix for #73. Instead of always returning an internal error on problems like insufficient privileges, instead return a user-facing API error with a description of what's wrong. This is only a partial fix because it requires the new API infrastructure from #71 to work properly, so we'll need to convert the rest of the endpoints over for it to be fully effective. One possibility is that we might want to just take any kind of `*pgconn.PgError` and change that to a user-facing error. I didn't do that here, but it strikes me as fairly plausible. I figure we should maybe wait for one more type of error that's commonly encountered before taking this step. --- api_handler.go | 2 +- go.mod | 1 + go.sum | 4 ++-- internal/apiendpoint/api_endpoint.go | 15 +++++++++++++++ internal/apiendpoint/api_endpoint_test.go | 21 +++++++++++++++++++++ 5 files changed, 40 insertions(+), 3 deletions(-) diff --git a/api_handler.go b/api_handler.go index 4f2c119f..291b1ee2 100644 --- a/api_handler.go +++ b/api_handler.go @@ -121,7 +121,7 @@ func (a *jobCancelEndpoint) Execute(ctx context.Context, req *jobCancelRequest) if errors.Is(err, river.ErrNotFound) { return nil, apierror.NewNotFoundJob(jobID) } - return nil, fmt.Errorf("error canceling jobs: %w", err) + return nil, err } updatedJobs[jobID] = job } diff --git a/go.mod b/go.mod index ba8d8788..e5e5b0d1 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,7 @@ module github.com/riverqueue/riverui go 1.22 require ( + github.com/jackc/pgerrcode v0.0.0-20240316143900-6e2875d9b438 github.com/jackc/pgx/v5 v5.6.0 github.com/joho/godotenv v1.5.1 github.com/riverqueue/river v0.7.0 diff --git a/go.sum b/go.sum index 422b8837..9f7f7692 100644 --- a/go.sum +++ b/go.sum @@ -5,8 +5,8 @@ github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/jackc/pgerrcode v0.0.0-20220416144525-469b46aa5efa h1:s+4MhCQ6YrzisK6hFJUX53drDT4UsSW3DEhKn0ifuHw= -github.com/jackc/pgerrcode v0.0.0-20220416144525-469b46aa5efa/go.mod h1:a/s9Lp5W7n/DD0VrVoyJ00FbP2ytTPDVOivvn2bMlds= +github.com/jackc/pgerrcode v0.0.0-20240316143900-6e2875d9b438 h1:Dj0L5fhJ9F82ZJyVOmBx6msDp/kfd1t9GRfny/mfJA0= +github.com/jackc/pgerrcode v0.0.0-20240316143900-6e2875d9b438/go.mod h1:a/s9Lp5W7n/DD0VrVoyJ00FbP2ytTPDVOivvn2bMlds= github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= github.com/jackc/pgservicefile v0.0.0-20231201235250-de7065d80cb9 h1:L0QtFUgDarD7Fpv9jeVMgy/+Ec0mtnmYuImjTz6dtDA= diff --git a/internal/apiendpoint/api_endpoint.go b/internal/apiendpoint/api_endpoint.go index 9bbe4e3e..5de0914c 100644 --- a/internal/apiendpoint/api_endpoint.go +++ b/internal/apiendpoint/api_endpoint.go @@ -12,6 +12,9 @@ import ( "net/http" "time" + "github.com/jackc/pgerrcode" + "github.com/jackc/pgx/v5/pgconn" + "github.com/riverqueue/riverui/internal/apierror" ) @@ -139,6 +142,18 @@ func executeAPIEndpoint[TReq any, TResp any](w http.ResponseWriter, r *http.Requ return nil }() if err != nil { + // Convert certain types of Postgres errors into something more + // user-friendly than an internal server error. + var pgErr *pgconn.PgError + if errors.As(err, &pgErr) { + if pgErr.Code == pgerrcode.InsufficientPrivilege { + err = apierror.WithInternalError( + apierror.NewBadRequest("Insufficient database privilege to perform this operation."), + err, + ) + } + } + var apiErr apierror.Interface if errors.As(err, &apiErr) { logAttrs := []any{ diff --git a/internal/apiendpoint/api_endpoint_test.go b/internal/apiendpoint/api_endpoint_test.go index 287bd180..24ed15bf 100644 --- a/internal/apiendpoint/api_endpoint_test.go +++ b/internal/apiendpoint/api_endpoint_test.go @@ -5,11 +5,14 @@ import ( "context" "encoding/json" "errors" + "fmt" "net/http" "net/http/httptest" "testing" "time" + "github.com/jackc/pgerrcode" + "github.com/jackc/pgx/v5/pgconn" "github.com/stretchr/testify/require" "github.com/riverqueue/riverui/internal/apierror" @@ -99,6 +102,18 @@ func TestMountAndServe(t *testing.T) { requireStatusAndJSONResponse(t, http.StatusBadRequest, &apierror.APIError{Message: "Missing message value."}, bundle.recorder) }) + t.Run("InterpretedPostgresError", func(t *testing.T) { + t.Parallel() + + mux, bundle := setup(t) + + req := httptest.NewRequest(http.MethodPost, "/api/post-endpoint", + bytes.NewBuffer(mustMarshalJSON(t, &postRequest{MakePostgresError: true}))) + mux.ServeHTTP(bundle.recorder, req) + + requireStatusAndJSONResponse(t, http.StatusBadRequest, &apierror.APIError{Message: "Insufficient database privilege to perform this operation."}, bundle.recorder) + }) + t.Run("Timeout", func(t *testing.T) { t.Parallel() @@ -219,6 +234,7 @@ func (*postEndpoint) Meta() *EndpointMeta { type postRequest struct { MakeInternalError bool `json:"make_internal_error"` + MakePostgresError bool `json:"make_postgres_error"` Message string `json:"message"` } @@ -231,6 +247,11 @@ func (a *postEndpoint) Execute(_ context.Context, req *postRequest) (*postRespon return nil, errors.New("an internal error occurred") } + if req.MakePostgresError { + // Wrap the error to make it more realistic. + return nil, fmt.Errorf("error runnning Postgres query: %w", &pgconn.PgError{Code: pgerrcode.InsufficientPrivilege}) + } + if req.Message == "" { return nil, apierror.NewBadRequest("Missing message value.") }