Skip to content

fix(cli): auto-retry db dump/pull via the IPv4 pooler on IPv6-only networks#5493

Merged
jgoux merged 9 commits into
developfrom
claude/brave-maxwell-YoIfd
Jun 12, 2026
Merged

fix(cli): auto-retry db dump/pull via the IPv4 pooler on IPv6-only networks#5493
jgoux merged 9 commits into
developfrom
claude/brave-maxwell-YoIfd

Conversation

@avallete

@avallete avallete commented Jun 5, 2026

Copy link
Copy Markdown
Member

Closes CLI-1593

What

supabase db dump and db pull run pg_dump inside 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 opaque error 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_dump container 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.&lt;ref&gt;.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"]
Loading

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 ✓
Loading

How

  • internal/db/dump/pooler_fallback.goRunWithPoolerFallback wraps the Docker-backed pg_dump operations. 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. resetOutput rewinds the destination between attempts (bytes.Buffer.Reset, file Truncate+Seek, stdout ignored) so a partial first attempt isn't left behind. --dry-run skips the wrapper entirely.
  • internal/db/dump/dump.go + internal/db/pull/pull.go route their remote dump paths through RunWithPoolerFallback (dump data/role/schema; pull's experimental role+schema dump and dumpRemoteSchema).
  • internal/utils/flags/db_url.goResolvePoolerConfigForFallback returns an authenticated IPv4 transaction-pooler config: it prefers the pooler URL persisted at supabase link time, otherwise fetches it from the Management API, forces the transaction port, and authenticates via SUPABASE_DB_PASSWORD or a temporary login role. It's injected through a package variable so tests can stub the network call.
  • internal/utils/connect.go — detection (isIPv6ConnectivityError) covers Address 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 existing SetConnectSuggestion / SuggestIPv6Pooler provide 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:

Your network does not support IPv6, which is required for direct connections to the database.
Retry through the IPv4 transaction pooler by passing it to --db-url "postgres://postgres.<ref>:[YOUR-PASSWORD]@aws-0-<region>.pooler.supabase.com:6543/postgres"

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 address classification.
  • connect_test.go: detection matrix incl. the new signatures and ProjectRefFromDirectDbHost; SuggestIPv6Pooler enrichment.
  • db_url_test.go: ResolvePoolerConfigForFallback (persisted-URL vs Management API resolution).

Notes

  • These db commands are still proxied to the bundled Go binary, so the fix lives in apps/cli-go.
  • The auto-retry only triggers for direct Supabase hosts — explicit --db-url/--local targets are never silently rerouted.

https://claude.ai/code/session_01UaPk7dGPmiCqoKJHyV7SLz

@avallete avallete requested a review from a team as a code owner June 5, 2026 19:43
@avallete avallete force-pushed the claude/brave-maxwell-YoIfd branch from ae9ab9c to 2156bff Compare June 5, 2026 19:47
@coveralls

coveralls commented Jun 5, 2026

Copy link
Copy Markdown

Coverage Report for CI Build 27142638966

Warning

No base build found for commit 8a05105 on develop.
Coverage changes can't be calculated without a base build.
If a base build is processing, this comment will update automatically when it completes.

Coverage: 64.231%

Details

  • Patch coverage: No coverable lines changed in this PR.

Uncovered Changes

No uncovered changes found.

Coverage Regressions

Requires a base build to compare against. How to fix this →


Coverage Stats

Coverage Status
Relevant Lines: 16014
Covered Lines: 10286
Line Coverage: 64.23%
Coverage Strength: 7.16 hits per line

💛 - Coveralls

@github-actions

github-actions Bot commented Jun 5, 2026

Copy link
Copy Markdown

Supabase CLI preview

npx --yes https://pkg.pr.new/supabase@5493

Preview package for commit 288f5cc.

@avallete avallete force-pushed the claude/brave-maxwell-YoIfd branch 2 times, most recently from 7f9c903 to 8353f12 Compare June 8, 2026 08:40
claude added 4 commits June 8, 2026 09:19
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
@avallete avallete force-pushed the claude/brave-maxwell-YoIfd branch from 8353f12 to 66ac522 Compare June 8, 2026 09:20
@avallete avallete marked this pull request as draft June 8, 2026 09:26
avallete and others added 2 commits June 8, 2026 14:52
…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.
@avallete avallete marked this pull request as ready for review June 8, 2026 13:55
@avallete avallete changed the title fix(cli): detect IPv6 connectivity errors and suggest the IPv4 pooler fix(cli): auto-retry db dump/pull via the IPv4 pooler on IPv6-only networks Jun 9, 2026

@jgoux jgoux 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.

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.

Comment thread apps/cli-go/internal/db/pull/pull.go
Comment thread apps/cli-go/internal/db/dump/dump.go
… 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
@avallete avallete requested a review from jgoux June 11, 2026 16:11
Comment on lines +59 to +64
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)
}

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

issue

This could leak the password to a terminal, we don't want that.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

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
@jgoux jgoux added this pull request to the merge queue Jun 12, 2026
Merged via the queue into develop with commit 0616225 Jun 12, 2026
35 checks passed
@jgoux jgoux deleted the claude/brave-maxwell-YoIfd branch June 12, 2026 07:23
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.

4 participants