fix(cli): auto-retry db dump/pull via the IPv4 pooler on IPv6-only networks#5493
Conversation
ae9ab9c to
2156bff
Compare
Coverage Report for CI Build 27142638966Warning No base build found for commit Coverage: 64.231%Details
Uncovered ChangesNo uncovered changes found. Coverage RegressionsRequires a base build to compare against. How to fix this → Coverage Stats
💛 - Coveralls |
Supabase CLI previewnpx --yes https://pkg.pr.new/supabase@5493Preview package for commit |
7f9c903 to
8353f12
Compare
Supabase direct database connections (db.<ref>.supabase.co:5432) are IPv6-only unless the IPv4 add-on is enabled. Users on IPv4-only networks hit confusing failures: - in-process commands (db push/pull, migration repair) surfaced "network is unreachable" with no hint, or "no route to host" misreported as "make sure your project exists". - `supabase db dump` runs pg_dump inside a Docker container whose stderr is streamed to the terminal but never classified, so the connection failure only showed up as "error running container: exit 1". Detect the IPv6 signatures (network unreachable / no route to an IPv6 literal / libpq "Address family for hostname not supported") and point the user at the IPv4 connection pooler via `supabase link`. For db dump, tee the container's stderr so the failure can be classified after the generic container exit code. https://claude.ai/code/session_01UaPk7dGPmiCqoKJHyV7SLz
A real-world db dump on an IPv4-only network surfaced "could not translate host name ... to address: No address associated with hostname" (getaddrinfo EAI_NODATA): the host is IPv6-only and the container has no IPv6 stack, so AI_ADDRCONFIG filters out the AAAA record. The previous detection only matched the address-family variant, so the IPv6 suggestion never fired. Match the EAI_NODATA message too, and cover it with the existing connect-suggestion and db dump tests. https://claude.ai/code/session_01UaPk7dGPmiCqoKJHyV7SLz
…-url The IPv6 suggestion advised "supabase link", but the direct unblock is to pass the IPv4 transaction pooler connection string to --db-url. Reword the generic hint to advertise --db-url, and for db dump enrich it with the project's actual transaction pooler connection string fetched from the pooler config API, so the user gets a copy-pasteable command. https://claude.ai/code/session_01UaPk7dGPmiCqoKJHyV7SLz
SuggestIPv6Pooler reaches GetSupabase(), which log.Fatalln's when no access token is configured. On the --db-url path (no login) an IPv6 failure against a Supabase host would crash instead of printing the suggestion. Check for a token first and fall back to the generic hint. https://claude.ai/code/session_01UaPk7dGPmiCqoKJHyV7SLz
8353f12 to
66ac522
Compare
…failures On Docker Desktop for macOS the host can reach the IPv6-only direct database but the pg_dump container cannot, so linked db dump/pull failed even though a working IPv4 transaction pooler was available. Wrap the Docker-backed pg_dump paths so an IPv6 connectivity failure against a direct db.<ref>.supabase.co host transparently resolves the pooler, warns the user, and retries once, falling back to the existing --db-url suggestion only when no pooler exists.
jgoux
left a comment
There was a problem hiding this comment.
I found two behavior gaps worth tightening before this lands. The targeted and full apps/cli-go Go test suites pass locally, so these are about uncovered command behavior rather than current test failures.
… passes Addresses review feedback on the IPv6 auto-retry: - Gate the auto-retry to the linked connection path. RunWithPoolerFallback previously rerouted any direct-looking host, so an explicit --db-url could be silently retried against a different pooler and, with no persisted pooler URL, reach GetSupabase() and fatally exit when not logged in. ParseDatabaseConfig now records PoolerFallbackEligible (true only for --linked), and the new PoolerFallbackConfig helper refuses to reroute otherwise. - Extend the retry to db pull's diff and declarative-export passes. Only the dump pass was wrapped, so diffRemoteSchema / pullDeclarativePgDelta still ran the direct host inside a container and could fail over IPv6 after the dump fallback succeeded. Both now retry via the pooler using the shared helper (their container stderr is already embedded in the returned error). https://claude.ai/code/session_01UaPk7dGPmiCqoKJHyV7SLz
| utils.SetConnectSuggestion(connErr) | ||
| if utils.IsIPv6ConnectivityError(connErr) { | ||
| // Enrich the hint with the project's actual transaction pooler URL so the | ||
| // user gets a copy-pasteable --db-url. | ||
| utils.SuggestIPv6Pooler(ctx, config.Host) | ||
| } |
There was a problem hiding this comment.
issue
This could leak the password to a terminal, we don't want that.
There was a problem hiding this comment.
Agreed — fixed in 288f5cc. SuggestIPv6Pooler was rendering the Management API's connection_string verbatim, which can carry a real password. ipv6PoolerSuggestion now masks the userinfo password with the [YOUR-PASSWORD] placeholder before printing (via maskPoolerPassword), so the credential is never echoed to the terminal while the hint stays copy-pasteable. Added a test that feeds a real password through the API mock and asserts it's absent from the suggestion.
Generated by Claude Code
The Management API can return a real password in the pooler connection_string, and SuggestIPv6Pooler printed it verbatim to the terminal. Mask the userinfo password with the [YOUR-PASSWORD] placeholder before rendering the hint so the credential is never echoed while the suggestion stays copy-pasteable. https://claude.ai/code/session_01UaPk7dGPmiCqoKJHyV7SLz
Closes CLI-1593
What
supabase db dumpanddb pullrunpg_dumpinside a Docker container. Supabase direct database hosts (db.<ref>.supabase.co:5432) are IPv6-only unless the IPv4 add-on is enabled, so on environments without working IPv6 in the container (very common on Docker Desktop for macOS) the operation failed with an opaqueerror running container: exit 1.This PR makes that path self-healing: when a remote dump/pull fails because the direct host is unreachable over IPv6, the CLI transparently resolves the project's IPv4 transaction pooler, warns the user, and retries once. If no pooler is available it falls back to an actionable error message pointing at
--db-url.Why
The host running the CLI often does have IPv6 (so the pre-flight dial succeeds and the direct config is selected), but the
pg_dumpcontainer does not — so the failure only surfaces deep inside the container as a libpq/getaddrinfo error, hidden behind the generic container exit code. Users were left stuck with no hint, even though a working IPv4 pooler existed for their project.Behavior
flowchart TD A["db dump / db pull (remote)"] --> B["Run pg_dump in Docker container<br/>(tee stderr for classification)"] B --> C{Succeeded?} C -->|yes| OK["Write dump ✓"] C -->|no| D{"stderr is an<br/>IPv6 connectivity error?"} D -->|no| SUG["Classify error → actionable suggestion"] D -->|yes| E{"Host is a direct<br/>db.<ref>.supabase.co?"} E -->|no| SUG E -->|yes| F{"IPv4 pooler<br/>config resolvable?"} F -->|no| SUG2["Suggest --db-url with the<br/>transaction pooler URL"] F -->|yes| G["Warn user · reset output ·<br/>retry once via IPv4 pooler"] G --> H{Retry succeeded?} H -->|yes| OK2["Write dump ✓<br/>(transparent recovery)"] H -->|no| SUG3["Classify retry error → suggestion"]Happy-path auto-recovery (linked project, host has IPv6, container does not):
sequenceDiagram actor U as User participant CLI as supabase db dump participant C as pg_dump container participant API as link cache / Management API U->>CLI: db dump (linked → direct host) CLI->>C: pg_dump → db.ref.supabase.co:5432 (IPv6) C-->>CLI: error: "No address associated with hostname"<br/>/ "Network is unreachable" (no IPv6 in container) Note over CLI: classify captured stderr → IPv6 connectivity error CLI->>API: resolve IPv4 transaction pooler + login role API-->>CLI: pooler config (port 5432) CLI-->>U: ⚠ Warning: retrying via the IPv4 connection pooler CLI->>C: pg_dump → aws-0-…pooler.supabase.com (IPv4) C-->>CLI: dump output CLI-->>U: dump written ✓How
internal/db/dump/pooler_fallback.go—RunWithPoolerFallbackwraps the Docker-backedpg_dumpoperations. It runs the closure with an stderr-capturing exec; on failure it classifies the captured stderr and, if it's an IPv6 error against a direct host with a resolvable pooler, warns, resets the output, and retries once via the pooler.resetOutputrewinds the destination between attempts (bytes.Buffer.Reset, fileTruncate+Seek, stdout ignored) so a partial first attempt isn't left behind.--dry-runskips the wrapper entirely.internal/db/dump/dump.go+internal/db/pull/pull.goroute their remote dump paths throughRunWithPoolerFallback(dump data/role/schema; pull's experimental role+schema dump anddumpRemoteSchema).internal/utils/flags/db_url.go—ResolvePoolerConfigForFallbackreturns an authenticated IPv4 transaction-pooler config: it prefers the pooler URL persisted atsupabase linktime, otherwise fetches it from the Management API, forces the transaction port, and authenticates viaSUPABASE_DB_PASSWORDor a temporary login role. It's injected through a package variable so tests can stub the network call.internal/utils/connect.go— detection (isIPv6ConnectivityError) coversAddress family for hostname not supported,No address associated with hostname,Network is unreachable, and (gated on an IPv6 literal so genuine project-not-found / tenant errors keep their own hint)No route to host/Cannot assign requested address. The IPv6-literal regex matches both Go's bracketed[…]and libpq's parenthesised(…)forms.ProjectRefFromDirectDbHost,WarnIPv6PoolerFallback, and the existingSetConnectSuggestion/SuggestIPv6Poolerprovide ref extraction, the retry warning, and the non-recoverable suggestion.Non-recoverable fallback (message only)
When auto-retry isn't possible (not an IPv6 error, not a direct host, or no pooler), the command still fails — but with guidance instead of a bare exit code:
Tests
dump_test.go: auto-retry succeeds via the pooler (asserts warning, output truncation/rewrite, no leftover suggestion); IPv6 failure with no pooler still surfaces the suggestion;Cannot assign requested addressclassification.connect_test.go: detection matrix incl. the new signatures andProjectRefFromDirectDbHost;SuggestIPv6Poolerenrichment.db_url_test.go:ResolvePoolerConfigForFallback(persisted-URL vs Management API resolution).Notes
dbcommands are still proxied to the bundled Go binary, so the fix lives inapps/cli-go.--db-url/--localtargets are never silently rerouted.https://claude.ai/code/session_01UaPk7dGPmiCqoKJHyV7SLz