diff --git a/apps/desktop/src/routes/(window-chrome)/settings/integrations/google-drive-config.tsx b/apps/desktop/src/routes/(window-chrome)/settings/integrations/google-drive-config.tsx index a69ffc316b6..f6d0c522180 100644 --- a/apps/desktop/src/routes/(window-chrome)/settings/integrations/google-drive-config.tsx +++ b/apps/desktop/src/routes/(window-chrome)/settings/integrations/google-drive-config.tsx @@ -6,6 +6,7 @@ import { commands } from "~/utils/tauri"; import { apiClient, protectedHeaders } from "~/utils/web-api"; import { Section, SectionCard, SettingsPageContent } from "../Setting"; import { IntegrationConfigHeader } from "./config-header"; +import { describeS3ConfigError } from "./s3-config-error"; const byteUnits = ["B", "KB", "MB", "GB", "TB", "PB"] as const; const googleDriveConnectionPollIntervalMs = 1500; @@ -71,7 +72,9 @@ const fetchS3Config = async (orgId: string | null) => { headers: await protectedHeaders(), }); - if (response.status !== 200) throw new Error("Failed to fetch S3 config"); + if (response.status !== 200) { + throw new Error(describeS3ConfigError(response)); + } return response.body; }; diff --git a/apps/desktop/src/routes/(window-chrome)/settings/integrations/s3-config-error.ts b/apps/desktop/src/routes/(window-chrome)/settings/integrations/s3-config-error.ts new file mode 100644 index 00000000000..f8a8a0fd234 --- /dev/null +++ b/apps/desktop/src/routes/(window-chrome)/settings/integrations/s3-config-error.ts @@ -0,0 +1,29 @@ +/** + * Turn a non-200 `getS3Config` response into something a user can act on or + * paste into an issue. + * + * The route already returns `{ error, details }` (see + * `apps/web/app/api/desktop/[...route]/s3Config.ts`), but both callers threw a + * fixed "Failed to fetch S3 config" and dropped it. Since there is no local + * ErrorBoundary on these pages, that string reached `CapErrorBoundary` and was + * all the user could see or copy — which is exactly what was reported in #1840. + */ +export function describeS3ConfigError(response: { + status: number; + body: unknown; +}): string { + const body = response.body as + | { error?: unknown; details?: unknown } + | undefined; + const error = typeof body?.error === "string" ? body.error : undefined; + const details = typeof body?.details === "string" ? body.details : undefined; + + if (response.status === 403 || error === "forbidden_org") { + return "You don't have access to this organization's storage settings."; + } + + if (error && details) return `${error}: ${details}`; + if (error) return error; + + return `Failed to fetch S3 config (HTTP ${response.status})`; +} diff --git a/apps/desktop/src/routes/(window-chrome)/settings/integrations/s3-config.tsx b/apps/desktop/src/routes/(window-chrome)/settings/integrations/s3-config.tsx index cba26750e95..1cfb4552987 100644 --- a/apps/desktop/src/routes/(window-chrome)/settings/integrations/s3-config.tsx +++ b/apps/desktop/src/routes/(window-chrome)/settings/integrations/s3-config.tsx @@ -8,6 +8,7 @@ import { commands } from "~/utils/tauri"; import { apiClient, protectedHeaders } from "~/utils/web-api"; import { Section, SectionCard, SettingsPageContent } from "../Setting"; import { IntegrationConfigHeader } from "./config-header"; +import { describeS3ConfigError } from "./s3-config-error"; interface S3Config { provider: string; @@ -37,7 +38,13 @@ export default function S3ConfigPage() { headers: await protectedHeaders(), }); - if (response.status !== 200) throw new Error("Failed to fetch S3 config"); + if (response.status !== 200) { + // The server already explains what went wrong (a stored bucket row + // that fails to decrypt, an org the user is no longer in, ...). + // Throwing a fixed string discarded that and left "Failed to fetch + // S3 config" as the only thing the user could see or report. + throw new Error(describeS3ConfigError(response)); + } return response.body; }, diff --git a/packages/web-api-contract/src/desktop.ts b/packages/web-api-contract/src/desktop.ts index 2a0b96b529b..c3522dcfedd 100644 --- a/packages/web-api-contract/src/desktop.ts +++ b/packages/web-api-contract/src/desktop.ts @@ -175,6 +175,8 @@ const protectedContract = c.router( source: z.enum(["default", "user", "organization"]), managedByOrganization: ManagedOrganizationStorage, }), + 403: z.object({ error: z.string() }), + 500: z.object({ error: z.string(), details: z.string().optional() }), }, }, setS3Config: {