Skip to content

Commit ecc7e47

Browse files
committed
fix: improve error handling for missing ControlPlane edge function (#438)
# Improve error handling for missing ControlPlane edge function This PR enhances error handling in the CLI when the ControlPlane edge function is not found, distinguishing between two different 404 scenarios: 1. When the flow exists but isn't found in the ControlPlane (our existing error) 2. When the ControlPlane edge function itself doesn't exist (new error case) The changes: - Add specific error message when the Supabase gateway returns a 404 for the missing edge function - Provide clear instructions to run `npx pgflow install` when the edge function is missing - Handle non-JSON responses (like HTML error pages) that some gateways return - Add tests for these new error handling cases Additionally, the PR improves the Supabase startup script to: - Better detect running services by using the project name from project.json - Add configurable retry parameters for E2E tests when waiting for the server to be ready
1 parent 6348583 commit ecc7e47

File tree

5 files changed

+105
-16
lines changed

5 files changed

+105
-16
lines changed

pkgs/cli/__tests__/commands/compile/index.test.ts

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -170,6 +170,45 @@ describe('fetchFlowSQL', () => {
170170
).rejects.toThrow('Did you add it to supabase/functions/pgflow/index.ts');
171171
});
172172

173+
it('should handle missing ControlPlane edge function (Supabase gateway 404)', async () => {
174+
// Supabase gateway returns 404 with different error format when function doesn't exist
175+
const mockResponse = {
176+
ok: false,
177+
status: 404,
178+
json: async () => ({
179+
// Supabase gateway error - not our ControlPlane's "Flow Not Found"
180+
error: 'not_found',
181+
message: 'Function not found',
182+
}),
183+
};
184+
185+
global.fetch = vi.fn().mockResolvedValue(mockResponse);
186+
187+
await expect(
188+
fetchFlowSQL('test_flow', 'http://127.0.0.1:50621/functions/v1/pgflow', 'test-publishable-key')
189+
).rejects.toThrow('ControlPlane edge function not found');
190+
await expect(
191+
fetchFlowSQL('test_flow', 'http://127.0.0.1:50621/functions/v1/pgflow', 'test-publishable-key')
192+
).rejects.toThrow('npx pgflow install');
193+
});
194+
195+
it('should handle 404 with non-JSON response (HTML error page)', async () => {
196+
// Some gateways return HTML error pages
197+
const mockResponse = {
198+
ok: false,
199+
status: 404,
200+
json: async () => {
201+
throw new Error('Unexpected token < in JSON');
202+
},
203+
};
204+
205+
global.fetch = vi.fn().mockResolvedValue(mockResponse);
206+
207+
await expect(
208+
fetchFlowSQL('test_flow', 'http://127.0.0.1:50621/functions/v1/pgflow', 'test-publishable-key')
209+
).rejects.toThrow('ControlPlane edge function not found');
210+
});
211+
173212
it('should construct correct URL with flow slug', async () => {
174213
const mockResponse = {
175214
ok: true,

pkgs/cli/src/commands/compile/index.ts

Lines changed: 26 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -27,13 +27,33 @@ export async function fetchFlowSQL(
2727
});
2828

2929
if (response.status === 404) {
30-
const errorData = await response.json();
30+
let errorData: { error?: string; message?: string } = {};
31+
try {
32+
errorData = await response.json();
33+
} catch {
34+
// JSON parse failed - likely Supabase gateway error (HTML or plain text)
35+
}
36+
37+
// Check if this is our ControlPlane's 404 (has 'Flow Not Found' error)
38+
// vs Supabase gateway's 404 (function doesn't exist)
39+
if (errorData.error === 'Flow Not Found') {
40+
throw new Error(
41+
`Flow '${flowSlug}' not found.\n\n` +
42+
`${errorData.message || 'Did you add it to supabase/functions/pgflow/index.ts?'}\n\n` +
43+
`Fix:\n` +
44+
`1. Add your flow to supabase/functions/pgflow/index.ts\n` +
45+
`2. Restart edge functions: supabase functions serve`
46+
);
47+
}
48+
49+
// ControlPlane edge function itself doesn't exist
3150
throw new Error(
32-
`Flow '${flowSlug}' not found.\n\n` +
33-
`${errorData.message || 'Did you add it to supabase/functions/pgflow/index.ts?'}\n\n` +
34-
`Fix:\n` +
35-
`1. Add your flow to supabase/functions/pgflow/index.ts\n` +
36-
`2. Restart edge functions: supabase functions serve`
51+
'ControlPlane edge function not found.\n\n' +
52+
'The pgflow edge function is not installed or not running.\n\n' +
53+
'Fix:\n' +
54+
'1. Run: npx pgflow install\n' +
55+
'2. Start edge functions: supabase functions serve\n\n' +
56+
'Or use previous version: npx pgflow@0.8.0 compile path/to/flow.ts'
3757
);
3858
}
3959

pkgs/edge-worker/tests/config.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,18 @@ export const e2eConfig = {
1919
get dbUrl() {
2020
return 'postgresql://postgres:postgres@127.0.0.1:50322/postgres';
2121
},
22+
23+
/** Max retries when waiting for server to be ready (default: 30) */
24+
get serverReadyMaxRetries() {
25+
const envValue = Deno.env.get('E2E_SERVER_READY_MAX_RETRIES');
26+
return envValue ? parseInt(envValue, 10) : 30;
27+
},
28+
29+
/** Delay between retries in ms (default: 1000) */
30+
get serverReadyRetryDelayMs() {
31+
const envValue = Deno.env.get('E2E_SERVER_READY_RETRY_DELAY_MS');
32+
return envValue ? parseInt(envValue, 10) : 1000;
33+
},
2234
};
2335

2436
/**

pkgs/edge-worker/tests/e2e/control-plane.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,8 @@ const BASE_URL = `${API_URL}/functions/v1/pgflow`;
1212
async function ensureServerReady() {
1313
log('Ensuring pgflow function is ready...');
1414

15-
const maxRetries = 15;
16-
const retryDelayMs = 1000;
15+
const maxRetries = e2eConfig.serverReadyMaxRetries;
16+
const retryDelayMs = e2eConfig.serverReadyRetryDelayMs;
1717

1818
for (let i = 0; i < maxRetries; i++) {
1919
try {

scripts/supabase-start.sh

Lines changed: 26 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -34,23 +34,26 @@ NC='\033[0m' # No Color
3434
# Required services for edge function development
3535
# Note: Services like imgproxy, studio, inbucket, analytics, vector, pg_meta are optional
3636
# Container names use project_id suffix from config.toml (e.g., supabase_db_cli for project_id="cli")
37-
# We use pattern matching to handle different project suffixes
38-
REQUIRED_SERVICES=(
37+
# The project_id matches the "name" field in project.json
38+
REQUIRED_SERVICE_PREFIXES=(
3939
"supabase_db_"
4040
"supabase_kong_"
4141
"supabase_edge_runtime_"
4242
"supabase_rest_"
4343
"supabase_realtime_"
4444
)
4545

46-
# Check if all required services are running via docker ps
46+
# Check if all required services are running for a specific project
4747
# This is more reliable than `supabase status` which returns 0 even with stopped services
48+
# Args: $1 = project_name (e.g., "cli", "edge-worker")
4849
check_required_services_running() {
50+
local project_name="$1"
4951
local running_containers
5052
running_containers=$(docker ps --format '{{.Names}}' 2>/dev/null)
5153

52-
for service_prefix in "${REQUIRED_SERVICES[@]}"; do
53-
if ! echo "$running_containers" | grep -q "^${service_prefix}"; then
54+
for service_prefix in "${REQUIRED_SERVICE_PREFIXES[@]}"; do
55+
local full_container_name="${service_prefix}${project_name}"
56+
if ! echo "$running_containers" | grep -qF "$full_container_name"; then
5457
return 1
5558
fi
5659
done
@@ -72,15 +75,30 @@ if [ ! -d "$PROJECT_DIR" ]; then
7275
exit 1
7376
fi
7477

78+
# Convert to absolute path for consistent file lookups after cd
79+
PROJECT_DIR=$(realpath "$PROJECT_DIR")
80+
7581
# Change to project directory (Supabase CLI uses current directory)
7682
cd "$PROJECT_DIR"
7783

78-
echo -e "${YELLOW}Checking Supabase status in: $PROJECT_DIR${NC}"
84+
# Extract project name from project.json (matches project_id in supabase/config.toml)
85+
if [ ! -f "$PROJECT_DIR/project.json" ]; then
86+
echo -e "${RED}Error: project.json not found in $PROJECT_DIR${NC}" >&2
87+
exit 1
88+
fi
89+
90+
PROJECT_NAME=$(jq -r '.name' "$PROJECT_DIR/project.json")
91+
if [ -z "$PROJECT_NAME" ] || [ "$PROJECT_NAME" = "null" ]; then
92+
echo -e "${RED}Error: Could not read 'name' from project.json${NC}" >&2
93+
exit 1
94+
fi
95+
96+
echo -e "${YELLOW}Checking Supabase status for project '$PROJECT_NAME' in: $PROJECT_DIR${NC}"
7997

8098
# Fast path: Check if all required Supabase services are running via docker ps
8199
# This is more reliable than `supabase status` which returns 0 even with stopped services
82-
if check_required_services_running; then
83-
echo -e "${GREEN}✓ Supabase is already running (all required services up)${NC}"
100+
if check_required_services_running "$PROJECT_NAME"; then
101+
echo -e "${GREEN}✓ Supabase is already running for project '$PROJECT_NAME' (all required services up)${NC}"
84102
exit 0
85103
fi
86104

0 commit comments

Comments
 (0)