Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 11 additions & 1 deletion apps/cli-e2e/src/tests/database-core.e2e.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -252,7 +252,17 @@ describe("db push", () => {
expect(result.stderr).toContain("connect");
});

testParity(["db", "push", "--local"]);
// Known parity divergence: Go never emits the Docker-start hint for --local (CLI-1995).
testParity(["db", "push", "--local"], {
normalize: {
stderr: {
stripPatterns: [
/\nMake sure your local IP is allowed in Network Restrictions and Network Bans\.\n[^\n]*/g, // Expected from Go
/\nMake sure Docker is running, then run: supabase start/g, // Expected from TS
],
},
},
});
Comment on lines +255 to +265

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Let me know if a different fix is desired @Coly010.

});

describe("db push:linked", () => {
Expand Down
2 changes: 2 additions & 0 deletions apps/cli-e2e/src/tests/migrations.e2e.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ const CONNECT_REFUSED_STDERR_STRIP: readonly RegExp[] = [
/(?<=failed to connect to postgres:).*/g,
// Go's SetConnectSuggestion: Network-Restrictions hint + dashboard URL line.
/\nMake sure your local IP is allowed in Network Restrictions and Network Bans\.\n[^\n]*/g,
// TS's local-only Docker hint (CLI-1995); Go never emits this for --local.
/\nMake sure Docker is running, then run: supabase start/g,
// TS's generic --debug suggestion.
/\nTry rerunning the command with --debug to troubleshoot the error\./g,
];
Expand Down
40 changes: 32 additions & 8 deletions apps/cli/src/legacy/shared/legacy-connect-errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,26 @@ export function legacyIsIPv6ConnectivityError(message: string): boolean {
export const LEGACY_SUGGEST_ENV_VAR =
"Connect to your database by setting the env var correctly: SUPABASE_DB_PASSWORD";

/**
* TS-only addition — Go's `SetConnectSuggestion` has no local/remote distinction,
* so a refused `--local` connection (Docker/Postgres not running) got the same
* remote-only "Network Restrictions" dashboard hint as an actual network-restricted
* connection, which is a dead end locally. Shown instead of that hint when
* `ctx.isLocal` is true — see `legacyConnectSuggestion`.
*/
export const LEGACY_SUGGEST_LOCAL_STACK = "Make sure Docker is running, then run: supabase start";
Comment thread
annabkr marked this conversation as resolved.

/**
* Go's `SetConnectSuggestion` remote-only "Network Restrictions" hint
* (`internal/utils/connect.go:319-321`), shown for a connection refused/blocked
* by IP allow-listing. Shared by both the always-remote `Address not in tenant
* allow_list` branch and the non-local `ECONNREFUSED`/`connection refused`
* branch in `legacyConnectSuggestion`.
*/
function legacySuggestNetworkRestrictions(dashboardUrl: string): string {
return `Make sure your local IP is allowed in Network Restrictions and Network Bans.\n${dashboardUrl}/project/_/database/settings`;
}

/** Context the connect-suggestion needs but cannot derive from the error alone. */
export interface LegacyConnectSuggestionContext {
/** Active profile's dashboard URL (Go's `CurrentProfile.DashboardURL`). */
Expand Down Expand Up @@ -355,16 +375,20 @@ function legacyHasIPv6DialCause(error: unknown, depth = 0): boolean {
*/
export function legacyConnectSuggestion(
error: unknown,
ctx: LegacyConnectSuggestionContext,
ctx: LegacyConnectSuggestionContext & { readonly isLocal: boolean },
): string | undefined {
const text = legacyCollectConnectErrorText(error);
// connect: connection refused / Address not in tenant allow_list → network restrictions.
if (
text.includes("ECONNREFUSED") ||
text.includes("connection refused") ||
text.includes("Address not in tenant allow_list")
) {
return `Make sure your local IP is allowed in Network Restrictions and Network Bans.\n${ctx.dashboardUrl}/project/_/database/settings`;
// connect: connection refused - "Address not in tenant allow_list" only ever comes from the remote pooler
// rejecting the caller's IP, so it always means network restrictions.
if (text.includes("Address not in tenant allow_list")) {
return legacySuggestNetworkRestrictions(ctx.dashboardUrl);
}
// connect: connection refused — don't send the user to the
// dashboard's Network Restrictions page for a --local connection.
if (text.includes("ECONNREFUSED") || text.includes("connection refused")) {
return ctx.isLocal
? LEGACY_SUGGEST_LOCAL_STACK
: legacySuggestNetworkRestrictions(ctx.dashboardUrl);
}
// SSL connection is required (only under --debug, which disables TLS).
if (text.includes("SSL connection is required") && ctx.debug) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { describe, expect, it } from "vitest";

import {
LEGACY_SUGGEST_ENV_VAR,
LEGACY_SUGGEST_LOCAL_STACK,
legacyConnectFailureMessage,
legacyConnectSuggestion,
legacyIpv6Suggestion,
Expand Down Expand Up @@ -277,6 +278,7 @@ describe("legacyConnectSuggestion", () => {
dashboardUrl: "https://supabase.com/dashboard",
profileName: "supabase",
debug: false,
isLocal: false,
} as const;

// The @effect/sql SqlError wraps the node driver error on `.cause`; a multi-address
Expand All @@ -293,6 +295,13 @@ describe("legacyConnectSuggestion", () => {
);
});

it("maps a refused local connection to the local stack hint", () => {
const err = sqlError(systemError("connect ECONNREFUSED 127.0.0.1:54322", "ECONNREFUSED"));
expect(legacyConnectSuggestion(err, { ...ctx, isLocal: true })).toBe(
LEGACY_SUGGEST_LOCAL_STACK,
);
});

it("maps an AggregateError of refused dials to the network-restrictions hint", () => {
const err = sqlError(
Object.assign(new AggregateError([], "all attempts failed"), {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import * as net from "node:net";
import { describe, expect, it } from "@effect/vitest";
import { Effect } from "effect";

import { LEGACY_SUGGEST_ENV_VAR } from "./legacy-connect-errors.ts";
import { LEGACY_SUGGEST_ENV_VAR, LEGACY_SUGGEST_LOCAL_STACK } from "./legacy-connect-errors.ts";
import type { LegacyDbConnectError, LegacyDbExecError } from "./legacy-db-connection.errors.ts";
import { type LegacyPgConnInput, LegacyDbConnection } from "./legacy-db-connection.service.ts";
import { legacyDbConnectionSqlPgLayer } from "./legacy-db-connection.sql-pg.layer.ts";
Expand All @@ -26,9 +26,13 @@ const SUGGESTION_CONTEXT = {
// host/user/database — never the password).
const SENTINEL_PASSWORD = "s3cr3t-pw-do-not-leak";

/** Connect through the real layer and flip the expected failure into the value. */
/**
* Connect through the real layer and flip the expected failure into the value.
* `isLocal` defaults to `true`, pass `false` to drive the remote/`--linked` suggestion branch instead.
*/
const connectFailure = (
cfg: Partial<LegacyPgConnInput> & { readonly port: number },
isLocal = true,
): Effect.Effect<LegacyDbConnectError> =>
Effect.gen(function* () {
const conn = yield* LegacyDbConnection;
Expand All @@ -42,7 +46,7 @@ const connectFailure = (
suggestionContext: SUGGESTION_CONTEXT,
...cfg,
},
{ isLocal: true, dnsResolver: "native" },
{ isLocal, dnsResolver: "native" },
)
.pipe(
Effect.scoped,
Expand Down Expand Up @@ -176,11 +180,11 @@ const fakeQueryServer = (

describe("legacyDbConnectionSqlPgLayer connect failures", () => {
it.live(
"surfaces host, user, database, and the driver cause when the connection is refused",
"surfaces host, user, database, and the driver cause when a remote (--linked) connection is refused",
() =>
Effect.gen(function* () {
const port = yield* Effect.promise(acquireClosedPort);
const error = yield* connectFailure({ port });
const error = yield* connectFailure({ port }, false);
expect(error._tag).toBe("LegacyDbConnectError");
expect(error.message).toBe(
"failed to connect to postgres: failed to connect to `host=127.0.0.1 user=postgres database=postgres`: " +
Expand All @@ -194,6 +198,14 @@ describe("legacyDbConnectionSqlPgLayer connect failures", () => {
}),
);

it.live("surfaces the local-stack hint when a local connection is refused", () =>
Effect.gen(function* () {
const port = yield* Effect.promise(acquireClosedPort);
const error = yield* connectFailure({ port });
expect(error.suggestion).toBe(LEGACY_SUGGEST_LOCAL_STACK);
}),
);

it.live(
"reproduces pgconn's server-error rendering for an auth failure and suggests SUPABASE_DB_PASSWORD",
() =>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -661,7 +661,7 @@ const connect = (
const suggestion =
cfg.suggestionContext === undefined
? undefined
: legacyConnectSuggestion(error, cfg.suggestionContext);
: legacyConnectSuggestion(error, { ...cfg.suggestionContext, isLocal });
return new LegacyDbConnectError({
message: `failed to connect to postgres: ${legacyConnectFailureMessage(cfg, error)}`,
...(suggestion === undefined ? {} : { suggestion }),
Expand Down
Loading