fix(postgres): surface upstream Postgres errors and log run-token auth failures#54
Conversation
…h failures Two Postgres data-plane diagnosability gaps, closed without changing what the client ever sees (still the generic "could not authenticate to upstream database" — the underlying detail could echo the upstream server's identifiers): - upstreamErrorResponse collapsed every upstream ErrorResponse to either the bare errUpstreamAuthFailed sentinel or "upstream error (SQLSTATE %s)", discarding the upstream server's own Message. An IP-allowlist rejection and a bad password were indistinguishable in the DEBUG log, and the message text was never logged at all. It now folds the upstream's Severity, Code, and Message into the error text — these are the upstream SERVER's own error fields, never a credential — so the existing DEBUG "postgres upstream connection failed" line carries the real reason. Auth-failure SQLSTATEs (28P01, 28000) still wrap errUpstreamAuthFailed via %w, so connectWithRetry's errors.Is check and its invalidate-and-retry-once behavior are unchanged. That log line also gained a stage attribute (upstream_auth vs upstream_connect). - A client whose run token failed authentication produced zero log output server-side. handleConn now logs a WARN-level line naming the client address and SNI host (stage=run_token_auth) — visible without DEBUG, never logging the token itself. Grepped the diff to confirm no credential/token/password value can reach a log call.
📦 Previous Review (superseded by newer review)Claude finished @andybons's task in 3m 49s —— View job Reviewing PR #54
SummaryThis is a well-scoped, well-reasoned change. It only touches the error-detail path ( Security review:
Concurrency: No new shared mutable state; the WARN log and enriched error are constructed and logged synchronously within existing per-connection goroutines. Test helpers ( One finding posted inline ( Tests: Read through Note: I was unable to run |
PR #54 review found the stage attribute on the "postgres upstream connection failed" log produced only upstream_auth and upstream_connect. A credential-resolution failure (resolver.ResolvePassword, wrapped as "resolving postgres password: %w" in connectWithRetry) is not errUpstreamAuthFailed, so it fell into stage=upstream_connect — the same bucket as a real dial/TLS/SCRAM failure. An operator filtering by stage could not tell a Neon API resolution failure (missing project, wrong scope) apart from a network/TLS failure to the endpoint, which was the whole point of the stage attribute. Introduce an errResolvePassword sentinel, wrap the resolver failure with it (alongside the existing human-readable "resolving postgres password:" text), and classify it as stage=resolve via errors.Is before the generic upstream_connect fallback. Only errUpstreamAuthFailed still drives connectWithRetry's invalidate-and-retry, so a resolver failure never invalidates a cached password. Doc comment updated to name all three stages.
📦 Previous Review (superseded by newer review)Claude finished @andybons's task in 1m 27s —— View job Reviewing PR #54 (new commit pushed)
SummaryThe new commit ( Previous finding, now fixed: The fix:
I verified this reasoning against the diff directly ( No new issues found. The change is narrowly scoped to the stage-classification path, doesn't touch TLS/auth/credential-injection logic, and doesn't introduce new shared mutable state. CI ( Note: As in my previous pass, I could not execute |
# Conflicts: # CHANGELOG.md
|
Claude finished @andybons's task in 1m 54s —— View job Reviewing PR #54 (new commit pushed)
Summary
Previous finding, now fixed: Verified in the current diff (
Re-checked the rest of the diff for regressions:
No new issues found. This is a narrowly-scoped, correct fix for the one thing I flagged. CI ( Note: As in my previous passes, I could not execute |
Problem
Discovered while debugging a Neon connectivity failure: the Postgres data plane collapses distinct root causes into one opaque signal.
ErrorResponse, gatekeeper discarded the server'sMessageand collapsed everything to the internalerrUpstreamAuthFailedsentinel — so an IP-allowlist denial and a28P01 invalid_passwordwere indistinguishable in the log, and the actual reason never appeared.28P01and produced zero server-side log output — invisible to any dashboard or log search.Fix
upstreamErrorResponsenow folds the upstream server'sSeverity/Code/Messageinto the error text (the upstream server's fields — never a credential), so the existing DEBUGpostgres upstream connection failedline carries the real reason. An allowlist rejection now reads e.g.upstream error FATAL 08004: ... IP address ... is not authorized for this endpoint.28P01,28000) still wraperrUpstreamAuthFailedvia%w, soconnectWithRetry's invalidate-and-retry-once behavior is unchanged; non-auth codes (like08004) correctly don't trigger a pointless password invalidation.stageattribute (upstream_authvsupstream_connect) to distinguish auth rejections from dial/TLS failures.WARNline (visible without DEBUG) naming client address + SNI host — never the token.Net: the run-token, network-policy, credential-resolution, and upstream-connect boundaries are now each independently identifiable in the logs.
Security
No credential can reach a log:
Detail/Hintare deliberately excluded (onlySeverity/Code/Message), SCRAM client errors are deliberately left unwrapped (they embed username+password), the resolver path logs status codes and identifiers only, and the run-token WARN carries no token. Verified by grep + independent review.Tests
Test-first, all three mutation-checked (reverting the fix / neutering the WARN each flips a test red), encoding real incident values (
28P01,08004, a concrete allowlist message).go test -race ./..., vet, gofmt clean.CHANGELOG
v0.17.3.