Skip to content

refactor: hide pgx errors behind database.ErrNotFound#2274

Open
QuentinBisson wants to merge 3 commits into
kagent-dev:mainfrom
QuentinBisson:refactor/database-not-found-sentinel
Open

refactor: hide pgx errors behind database.ErrNotFound#2274
QuentinBisson wants to merge 3 commits into
kagent-dev:mainfrom
QuentinBisson:refactor/database-not-found-sentinel

Conversation

@QuentinBisson

@QuentinBisson QuentinBisson commented Jul 16, 2026

Copy link
Copy Markdown
Contributor

Why

Follow-up to the review discussion on #2187: the ErrNotFound sentinel was dropped there so the pgx-error cleanup could land on its own, covering all the places pgx leaks through the database.Client interface 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: exported ErrNotFound sentinel. Implementations wrap it with call-site context; callers match with errors.Is.
  • client_postgres.go: every single-row read (GetAgent, GetSession, GetSessionShareByToken, GetTask, GetPushNotification, GetTool, GetToolServer) maps pgx.ErrNoRows to the sentinel via a small notFoundOr helper. Other errors pass through unchanged. GetCrewAIFlowState keeps its existing nil-on-missing contract.
  • Consumers migrated off pgx: 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/migrations keeps its own driver-level handling, which is not client-API leakage.

REST not-found gating (behavior fix):

  • Session, session-share, and task handlers mapped every database read error to 404, so a backend outage read as "session/task not found" to API clients that treat 404 as a definitive signal. RespondNotFoundOrError in handlers/helpers.go writes 404 only for ErrNotFound and 500 for anything else; all ten blanket sites in sessions.go, session_shares.go, and tasks.go now go through it.

Tests

  • TestSingleRowReadsMapMissingToErrNotFound covers all seven getters against a live postgres.
  • TestRespondNotFoundOrError covers the 404/500 split.
  • Existing middleware, share, session, and transport tests updated to match on the sentinel.

What this unblocks

#2187 can map errors.Is(err, database.ErrNotFound) on the contextId path of ListTasks to a2a.ErrTaskNotFound, giving A2A clients a typed not-found (-32001) instead of a generic internal error. External clients probing a session over tasks/list (or GET /api/sessions/{id}) can then tell "gone" from "backend failure".

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>
@QuentinBisson
QuentinBisson force-pushed the refactor/database-not-found-sentinel branch from 666c0c3 to 9be7d57 Compare July 16, 2026 19:36
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>
@QuentinBisson
QuentinBisson marked this pull request as ready for review July 16, 2026 19:50
@QuentinBisson
QuentinBisson requested a review from a team as a code owner July 16, 2026 19:50
Copilot AI review requested due to automatic review settings July 16, 2026 19:50

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.ErrNotFound and map pgx.ErrNoRowsErrNotFound in Postgres single-row reads.
  • Migrate HTTP middleware + A2A sandbox transport off pgx error matching to errors.Is(err, database.ErrNotFound).
  • Fix REST handler behavior by returning 404 only for ErrNotFound, otherwise 500 via RespondNotFoundOrError, 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.

Comment on lines +70 to 72
if underlying != nil && !errors.Is(underlying, database.ErrNotFound) {
log.Error(underlying, message)
} else {

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants