refactor: hide pgx errors behind database.ErrNotFound#2274
refactor: hide pgx errors behind database.ErrNotFound#2274QuentinBisson wants to merge 3 commits into
Conversation
Single-row reads in the postgres client map pgx.ErrNoRows to an exported database.ErrNotFound sentinel, wrapped with call-site context. Consumers outside the database package (share-token middleware, error-response logging, sandbox session materialization) now match on the sentinel, so pgx no longer leaks through the database.Client interface. Signed-off-by: QuentinBisson <quentin@giantswarm.io>
666c0c3 to
9be7d57
Compare
Session, session-share, and task handlers mapped every database read error to 404, so a backend outage read as "not found" to API clients that treat 404 as a definitive signal. RespondNotFoundOrError writes 404 only for database.ErrNotFound and 500 for anything else. Signed-off-by: QuentinBisson <quentin@giantswarm.io>
There was a problem hiding this comment.
Pull request overview
This PR introduces a driver-agnostic database.ErrNotFound sentinel and updates database client implementations + consumers (HTTP middleware/handlers and A2A sandbox transport) to use it instead of matching pgx.ErrNoRows, improving layering and fixing REST endpoints that previously returned 404 during backend outages.
Changes:
- Add
go/api/database.ErrNotFoundand mappgx.ErrNoRows→ErrNotFoundin Postgres single-row reads. - Migrate HTTP middleware + A2A sandbox transport off
pgxerror matching toerrors.Is(err, database.ErrNotFound). - Fix REST handler behavior by returning 404 only for
ErrNotFound, otherwise 500 viaRespondNotFoundOrError, with targeted tests.
Reviewed changes
Copilot reviewed 13 out of 13 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
| go/api/database/errors.go | Adds exported ErrNotFound sentinel for cross-package error matching. |
| go/core/internal/database/client_postgres.go | Maps pgx.ErrNoRows to dbpkg.ErrNotFound via notFoundOr across single-row getters. |
| go/core/internal/database/client_test.go | Adds live-postgres test ensuring single-row getters map missing rows to ErrNotFound. |
| go/core/internal/httpserver/middleware.go | Updates share-token middleware to treat ErrNotFound as invalid/expired token. |
| go/core/internal/httpserver/middleware_error.go | Updates error logging not-found detection to use database.ErrNotFound. |
| go/core/internal/httpserver/server_share_middleware_test.go | Updates test stubs/comments to use dbpkg.ErrNotFound instead of pgx.ErrNoRows. |
| go/core/internal/httpserver/handlers/helpers.go | Adds RespondNotFoundOrError to gate 404 vs 500 based on database.ErrNotFound. |
| go/core/internal/httpserver/handlers/helpers_notfound_test.go | Adds unit test for RespondNotFoundOrError 404/500 split. |
| go/core/internal/httpserver/handlers/sessions.go | Routes session/agent “get” failures through RespondNotFoundOrError (404 only on ErrNotFound). |
| go/core/internal/httpserver/handlers/session_shares.go | Routes session ownership checks through RespondNotFoundOrError. |
| go/core/internal/httpserver/handlers/tasks.go | Routes GetTask failures through RespondNotFoundOrError. |
| go/core/internal/a2a/substrate_sandbox_transport.go | Updates missing-session detection to use database.ErrNotFound. |
| go/core/internal/a2a/substrate_sandbox_transport_test.go | Updates test assertions to match database.ErrNotFound. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| if underlying != nil && !errors.Is(underlying, database.ErrNotFound) { | ||
| log.Error(underlying, message) | ||
| } else { |
There was a problem hiding this comment.
Fixed in 8dab542: 5xx bodies now carry only the generic message; the underlying error is logged server-side (that path already ran through log.Error above). Non-5xx responses keep the detail, which callers rely on for 400/403/404 context.
RespondWithError echoed the underlying error into the JSON body for every status code, so 500s exposed backend internals such as database connection errors. 5xx bodies now carry only the generic message; the underlying error stays in the server log. Signed-off-by: QuentinBisson <quentin@giantswarm.io>
Why
Follow-up to the review discussion on #2187: the
ErrNotFoundsentinel was dropped there so the pgx-error cleanup could land on its own, covering all the places pgx leaks through thedatabase.Clientinterface rather than just one.Today, callers outside the database package match driver errors directly (
errors.Is(err, pgx.ErrNoRows)in the share-token middleware, error-response logging, and sandbox session materialization). That couples HTTP and A2A code to the storage driver. It also means REST handlers cannot tell a missing row from a backend failure, so several endpoints answer 404 during a database outage.What changed
Sentinel and boundary (no behavior change):
go/api/database/errors.go: exportedErrNotFoundsentinel. Implementations wrap it with call-site context; callers match witherrors.Is.client_postgres.go: every single-row read (GetAgent,GetSession,GetSessionShareByToken,GetTask,GetPushNotification,GetTool,GetToolServer) mapspgx.ErrNoRowsto the sentinel via a smallnotFoundOrhelper. Other errors pass through unchanged.GetCrewAIFlowStatekeeps its existing nil-on-missing contract.httpserver/middleware.go(share token 403 vs 500),httpserver/middleware_error.go(log-level decision),a2a/substrate_sandbox_transport.go(ensureSessionRow). No non-test file outside the database package imports pgx for error matching anymore;pkg/migrationskeeps its own driver-level handling, which is not client-API leakage.REST not-found gating (behavior fix):
RespondNotFoundOrErrorinhandlers/helpers.gowrites 404 only forErrNotFoundand 500 for anything else; all ten blanket sites insessions.go,session_shares.go, andtasks.gonow go through it.Tests
TestSingleRowReadsMapMissingToErrNotFoundcovers all seven getters against a live postgres.TestRespondNotFoundOrErrorcovers the 404/500 split.What this unblocks
#2187 can map
errors.Is(err, database.ErrNotFound)on thecontextIdpath ofListTaskstoa2a.ErrTaskNotFound, giving A2A clients a typed not-found (-32001) instead of a generic internal error. External clients probing a session overtasks/list(orGET /api/sessions/{id}) can then tell "gone" from "backend failure".