Unverified 提交 a4775c39 authored 作者: wwwillchen-bot's avatar wwwillchen-bot 提交者: GitHub

fix: handle Supabase 403 error when listing branches for free tier users (#2527)

## Summary - Handle 403 Forbidden errors gracefully when listing Supabase branches - Return empty array instead of throwing error for free tier users without branch access - Allows free tier users to continue using other Supabase features without error toasts Fixes #2525 ## Test plan - Verified all 784 unit tests pass - Existing E2E test (`supabase_branch.spec.ts`) continues to work for paid tier (test mode returns mock data) - Free tier users should now see no branches available instead of an error toast 🤖 Generated with [Claude Code](https://claude.com/claude-code) <!-- devin-review-badge-begin --> --- <a href="https://app.devin.ai/review/dyad-sh/dyad/pull/2527" target="_blank"> <picture> <source media="(prefers-color-scheme: dark)" srcset="https://static.devin.ai/assets/gh-open-in-devin-review-dark.svg?v=1"> <img src="https://static.devin.ai/assets/gh-open-in-devin-review-light.svg?v=1" alt="Open with Devin"> </picture> </a> <!-- devin-review-badge-end --> <!-- This is an auto-generated description by cubic. --> --- ## Summary by cubic Show a clear inline message when Supabase branch listing returns 403 for free‑tier projects, and suppress error toasts so users can keep using other Supabase features (addresses #2525). - **Bug Fixes** - Detect 403 from branches API and show “Branches are only supported for Supabase paid customers” in the UI; no toast. - Removed toast meta from the branches query and added getErrorMessage to clean up IPC-wrapped errors. - Other statuses keep existing handling; paid tier behavior unchanged. <sup>Written for commit eb414f84c5c4ff44a4f6f152aae5c395903430b3. Summary will update on new commits.</sup> <!-- End of auto-generated description by cubic. --> --------- Co-authored-by: 's avatarWill Chen <willchen90@gmail.com> Co-authored-by: 's avatarClaude Opus 4.5 <noreply@anthropic.com>
上级 58451425
差异被折叠。
...@@ -37,7 +37,9 @@ import connectSupabaseDark from "../../assets/supabase/connect-supabase-dark.svg ...@@ -37,7 +37,9 @@ import connectSupabaseDark from "../../assets/supabase/connect-supabase-dark.svg
// @ts-ignore // @ts-ignore
import connectSupabaseLight from "../../assets/supabase/connect-supabase-light.svg"; import connectSupabaseLight from "../../assets/supabase/connect-supabase-light.svg";
import { ExternalLink, Plus, RefreshCw, Trash2 } from "lucide-react"; import { ExternalLink, Info, Plus, RefreshCw, Trash2 } from "lucide-react";
import { Alert, AlertDescription } from "@/components/ui/alert";
import { getErrorMessage } from "@/lib/errors";
import { import {
Tooltip, Tooltip,
TooltipTrigger, TooltipTrigger,
...@@ -66,6 +68,7 @@ export function SupabaseConnector({ appId }: { appId: number }) { ...@@ -66,6 +68,7 @@ export function SupabaseConnector({ appId }: { appId: number }) {
isFetchingProjects, isFetchingProjects,
projectsError, projectsError,
isLoadingBranches, isLoadingBranches,
branchesError,
isSettingAppProject, isSettingAppProject,
refetchOrganizations, refetchOrganizations,
refetchProjects, refetchProjects,
...@@ -205,49 +208,58 @@ export function SupabaseConnector({ appId }: { appId: number }) { ...@@ -205,49 +208,58 @@ export function SupabaseConnector({ appId }: { appId: number }) {
<div className="space-y-4"> <div className="space-y-4">
<div className="space-y-2"> <div className="space-y-2">
<Label htmlFor="supabase-branch-select">Database Branch</Label> <Label htmlFor="supabase-branch-select">Database Branch</Label>
<Select {branchesError ? (
value={app.supabaseProjectId || ""} <Alert>
onValueChange={async (supabaseBranchProjectId) => { <Info className="h-4 w-4" />
try { <AlertDescription>
const branch = branches.find( {getErrorMessage(branchesError)}
(b) => b.projectRef === supabaseBranchProjectId, </AlertDescription>
); </Alert>
if (!branch) { ) : (
throw new Error("Branch not found"); <Select
value={app.supabaseProjectId || ""}
onValueChange={async (supabaseBranchProjectId) => {
try {
const branch = branches.find(
(b) => b.projectRef === supabaseBranchProjectId,
);
if (!branch) {
throw new Error("Branch not found");
}
// Keep the same organizationSlug from the app
await setAppProject({
projectId: branch.projectRef,
parentProjectId: branch.parentProjectRef,
appId,
organizationSlug: app.supabaseOrganizationSlug,
});
toast.success("Branch selected");
await refreshApp();
} catch (error) {
toast.error("Failed to set branch: " + error);
} }
// Keep the same organizationSlug from the app }}
await setAppProject({ disabled={isLoadingBranches || isSettingAppProject}
projectId: branch.projectRef,
parentProjectId: branch.parentProjectRef,
appId,
organizationSlug: app.supabaseOrganizationSlug,
});
toast.success("Branch selected");
await refreshApp();
} catch (error) {
toast.error("Failed to set branch: " + error);
}
}}
disabled={isLoadingBranches || isSettingAppProject}
>
<SelectTrigger
id="supabase-branch-select"
data-testid="supabase-branch-select"
> >
<SelectValue placeholder="Select a branch" /> <SelectTrigger
</SelectTrigger> id="supabase-branch-select"
<SelectContent> data-testid="supabase-branch-select"
{branches.map((branch) => ( >
<SelectItem <SelectValue placeholder="Select a branch" />
key={branch.projectRef} </SelectTrigger>
value={branch.projectRef} <SelectContent>
> {branches.map((branch) => (
{branch.name} <SelectItem
{branch.isDefault && " (Default)"} key={branch.projectRef}
</SelectItem> value={branch.projectRef}
))} >
</SelectContent> {branch.name}
</Select> {branch.isDefault && " (Default)"}
</SelectItem>
))}
</SelectContent>
</Select>
)}
</div> </div>
<Button variant="destructive" onClick={handleUnsetProject}> <Button variant="destructive" onClick={handleUnsetProject}>
......
...@@ -103,7 +103,6 @@ export function useSupabase(options: UseSupabaseOptions = {}) { ...@@ -103,7 +103,6 @@ export function useSupabase(options: UseSupabaseOptions = {}) {
return Array.isArray(list) ? list : []; return Array.isArray(list) ? list : [];
}, },
enabled: !!branchesProjectId, enabled: !!branchesProjectId,
meta: { showErrorToast: true },
}); });
// Mutation: Load edge function logs for a Supabase project // Mutation: Load edge function logs for a Supabase project
......
/**
* Extracts a user-friendly error message from an Error object.
*
* Electron IPC errors are wrapped as:
* "Error invoking remote method '<channel>': Error: <actual message>"
*
* This strips the IPC wrapper and returns just the meaningful message.
*/
export function getErrorMessage(error: unknown): string {
const raw =
error instanceof Error ? error.message : String(error ?? "Unknown error");
return raw.replace(/^Error invoking remote method '.*?':\s*Error:\s*/, "");
}
...@@ -704,6 +704,14 @@ export async function listSupabaseBranches({ ...@@ -704,6 +704,14 @@ export async function listSupabaseBranches({
`List Supabase branches for ${supabaseProjectId}`, `List Supabase branches for ${supabaseProjectId}`,
); );
if (response.status === 403) {
// 403 Forbidden means the user doesn't have access to branches (e.g., free tier)
logger.info(
`Branches not available for project ${supabaseProjectId} (403 Forbidden - likely free tier)`,
);
throw new Error("Branches are only supported for Supabase paid customers");
}
if (response.status !== 200) { if (response.status !== 200) {
throw await createResponseError(response, "list branches"); throw await createResponseError(response, "list branches");
} }
......
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论