fix(integrations): surface why the S3 config request failed - #2042
Open
DPS0340 wants to merge 1 commit into
Open
fix(integrations): surface why the S3 config request failed#2042DPS0340 wants to merge 1 commit into
DPS0340 wants to merge 1 commit into
Conversation
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") { |
There was a problem hiding this comment.
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)"; | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #1840.
What @pulgueta saw
A full-page "An Error Occured" when opening Integrations → S3 Config, and the only copyable text was:
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:
The route is more informative than that. It already distinguishes 403
forbidden_orgfrom 500 and returns{ error, details }, wheredetailsis 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 toCapErrorBoundary, 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
getS3Configdeclared only a200, so the error body was untyped at the call site. Added403and500shapes, matching the existing style ofcreateGoogleDriveAuthUrl(403: z.object({ error: ... })).describeS3ConfigError, shared by both pages: 403 → a plain sentence about organization access; otherwiseerror: detailsfrom the server; falling back toFailed to fetch S3 config (HTTP <status>)when the body isn't shaped as expected.google-drive-config.tsxmade 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
detailsthrough: it iserror 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.json→ 0 errors.biome checkclean on all four files.packages/web-api-contractreports 1 error,TS4023inutil.ts— I stashed and re-ran on unmodifiedmainto confirm it is pre-existing and unrelated (a@ts-rest/corename-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.