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
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
};
Expand Down
Original file line number Diff line number Diff line change
@@ -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") {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Small thing: treating all 403 as org-access can be misleading if a different 403 ever bubbles up (auth/middleware/future errors). Since the route uses error: "forbidden_org", consider keying off that when available and otherwise falling back to a generic 403 / error string.

Suggested change
if (response.status === 403 || error === "forbidden_org") {
if (response.status === 403) {
if (error === "forbidden_org") {
return "You don't have access to this organization's storage settings.";
}
return error ?? "Request forbidden (HTTP 403)";
}

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})`;
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
},
Expand Down
2 changes: 2 additions & 0 deletions packages/web-api-contract/src/desktop.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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: {
Expand Down