Skip to content

fix(integrations): surface why the S3 config request failed - #2042

Open
DPS0340 wants to merge 1 commit into
CapSoftware:mainfrom
DPS0340:fix/s3-config-error-detail
Open

fix(integrations): surface why the S3 config request failed#2042
DPS0340 wants to merge 1 commit into
CapSoftware:mainfrom
DPS0340:fix/s3-config-error-detail

Conversation

@DPS0340

@DPS0340 DPS0340 commented Jul 26, 2026

Copy link
Copy Markdown

Closes #1840.

What @pulgueta saw

A full-page "An Error Occured" when opening Integrations → S3 Config, and the only copyable text was:

Error: Failed to fetch S3 config
@tauri://localhost/_build/assets/s3-config-RlCqiTlF.js:1:3123

That string is the whole diagnosis available to them — and to anyone triaging it.

Why it says nothing

Both integration pages collapse every failure into one fixed message:

if (response.status !== 200) throw new Error("Failed to fetch S3 config");

The route is more informative than that. It already distinguishes 403 forbidden_org from 500 and returns { error, details }, where details is the caught error's message — a decrypt failure on a stored bucket row, a DB error, and so on. All of it was thrown away at the call site.

Neither page has a local ErrorBoundary, so the thrown string propagates to CapErrorBoundary, which renders the generic page and offers "Copy Error to Clipboard" — that's why the report contains a bundled JS stack frame instead of a cause. The reporter did everything right; the app had nothing to tell them.

Fix

  • Contract: getS3Config declared only a 200, so the error body was untyped at the call site. Added 403 and 500 shapes, matching the existing style of createGoogleDriveAuthUrl (403: z.object({ error: ... })).
  • describeS3ConfigError, shared by both pages: 403 → a plain sentence about organization access; otherwise error: details from the server; falling back to Failed to fetch S3 config (HTTP <status>) when the body isn't shaped as expected.
  • google-drive-config.tsx made the identical call with the identical fixed string — it would have produced the same opaque page. Both now go through the helper, which is why this touches two pages rather than one.

On showing server messages to users

I checked before piping details through: it is error instanceof Error ? error.message : String(error) from the route's catch, and the handler doesn't put bucket credentials into it. So this surfaces failure causes, not secrets.

Verification

  • npx tsc --noEmit -p apps/desktop/tsconfig.json0 errors.
  • biome check clean on all four files.
  • packages/web-api-contract reports 1 error, TS4023 in util.ts — I stashed and re-ran on unmodified main to confirm it is pre-existing and unrelated (a @ts-rest/core name-visibility issue in a file this PR doesn't touch).

What I have not done: reproduce the original 500 on a live instance. I don't have R2 credentials or a Cap server to point at, so I traced the path statically — route → contract → call site → CapErrorBoundary — rather than observing it. The change is presentational and the failing path is unchanged, but the exact wording is worth a glance from someone who can trigger it.

I'd also gently suggest #1840 stays open until someone confirms why @pulgueta's request 500'd; this makes the cause visible, it doesn't fix whatever the underlying cause was.

Both integration pages threw a fixed string on any non-200:

    if (response.status !== 200) throw new Error("Failed to fetch S3 config");

The route already returns `{ error, details }` and distinguishes 403
(`forbidden_org`) from 500 (decrypt failure, DB error), but all of that was
discarded. Neither page has a local ErrorBoundary, so the thrown string
reached CapErrorBoundary and became the entire failure UI -- which is what
CapSoftware#1840 reports: a full-page "An Error Occured" whose copyable text is

    Error: Failed to fetch S3 config
    @tauri://localhost/_build/assets/s3-config-*.js

with no indication of the cause, so neither the user nor a maintainer can
tell a permissions problem from a corrupt stored bucket row.

- Model the 403 and 500 bodies in the contract; only 200 was declared, so the
  error shape was untyped at the call site.
- Add `describeS3ConfigError`, shared by both pages: 403 becomes a plain
  sentence about organization access, otherwise `error: details` from the
  server, falling back to the status code.
- google-drive-config.tsx made the identical call and had the identical
  fixed string; both now go through the helper.

Server-side messages were already safe to show -- `details` is the caught
error's message, and the route does not put credentials in it.

Closes CapSoftware#1840
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)";
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Generic error on S3 integration (Desktop)

1 participant