Skip to content

fix(view,text-editor): give the blob read path a failure state - #11015

Closed
clayrisser wants to merge 1 commit into
hcengineering:developfrom
clayrisser:fix/read-path-viewer-error-states
Closed

fix(view,text-editor): give the blob read path a failure state#11015
clayrisser wants to merge 1 commit into
hcengineering:developfrom
clayrisser:fix/read-path-viewer-error-states

Conversation

@clayrisser

Copy link
Copy Markdown

Problem

Three places on the blob read path have no failure state at all. In each case a failed fetch leaves the UI stuck rather than showing the "Failed to preview" state the codebase already has.

1. TextViewer.svelte:24-36 awaits fetch with no try/catch:

async function fetchFile (value: Ref<Blob>, name: string): Promise<void> {
  loading = true

  const src = getFileUrl(value, name)
  const res = await fetch(src)
  text = await res.text()

  loading = false
}

If the fetch rejects, loading = false is never reached — so the spinner runs forever — and the rejection is unhandled. A non-2xx response is worse than that: it does not reject, so res.text() succeeds and the error body is rendered as the file's contents.

2. ImageViewer.svelte has on:load but no on:error. Same eternal spinner: _setLoading(false) only ever runs from on:load, so a broken image leaves the loading state set and the <img> pinned at height: 0.

3. embed.ts:122 has .then and .finally but no .catch:

void providerPromise
  .then((view) => {
    view = view ?? StubEmbedNodeView

A rejected provider promise is an unhandled rejection, and the nodeview never falls back to StubEmbedNodeView — so the embed renders as nothing at all, rather than as the stub link it is designed to degrade to.

Fix

Each path now settles into a state that already exists in the codebase — presentation.string.FailedToPreview for the two viewers (already defined at packages/presentation/src/plugin.ts:138 and translated in lang/en.json, so no new i18n), and the existing stub view for the embed.

  • TextViewer — wrap in try/catch/finally, treat !res.ok as an error rather than rendering the body, and add an {:else if error} branch showing FailedToPreview.
  • ImageViewer — add error state, an on:error handler on the <img>, and an error branch. The error flag resets alongside _setLoading(true) when value changes, so navigating from a broken image to a good one recovers.
  • embed.ts — add a .catch that warns and returns undefined, which the existing view = view ?? StubEmbedNodeView line then turns into the stub.

Scope and residual risk

  • No behaviour change on the success path in any of the three.
  • The ImageViewer diff looks larger than it is: adding the {#if error} branch re-indents the existing DrawingBoard block into the {:else}. The only substantive changes are the error variable, the on:error handler, and the reset.
  • TextViewer now treats a non-2xx response as a failure rather than rendering the response body as file contents. That is a deliberate behaviour change and the one thing here worth a second opinion — it is the right call for a file viewer, but it does mean a server that returns a 404 page with useful text will now show "Failed to preview" instead of that text.
  • The two console.warns are the only new output. If you would rather they went through a measure context, say so and I will change them.

Verification

Verified against develop @ 1be6047c8: TextViewer.svelte:33 is still a bare await fetch, ImageViewer.svelte still has zero on:error handlers, and embed.ts:122 still has no .catch on providerPromise.

No automated tests. plugins/view-resources and plugins/text-editor-resources have no jest projects, and all three behaviours are DOM-level failure paths in Svelte components. Manual check for each: point the viewer at a blob id that does not resolve and confirm the "Failed to preview" label appears instead of a permanent spinner; give the embed node a URL whose provider match rejects and confirm the stub link renders.

Note on scope

This was originally prepared together with an absolute-URL guard for getPreviewThumbnail on the same read path. I have split that into a separate PR — it is a different concern, and it ships with unit tests whereas this one cannot.

Three read-path sites had no failure handling. TextViewer awaited fetch
with no try/catch, so a rejection left an unhandled promise and a spinner
that never resolves, and a non-2xx response was rendered as the file's
contents. ImageViewer had on:load but no on:error, so a broken image kept
the loading state set forever. The embed nodeview's provider promise had
.then/.finally but no .catch, so a rejection left an unhandled rejection
and no fallback to StubEmbedNodeView.

Each now settles into a state that already exists: the FailedToPreview
label for the two viewers, the stub view for the embed. No new i18n.

Co-authored-by: Cursor <cursoragent@cursor.com>
Signed-off-by: Clay Risser <clayrisser@gmail.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
@clayrisser
clayrisser force-pushed the fix/read-path-viewer-error-states branch from bb2b33c to b33ffec Compare August 13, 2026 06:47
@clayrisser

Copy link
Copy Markdown
Author

Closing — this was opened by an automated agent without my intent. Apologies for the noise.

@clayrisser clayrisser closed this Aug 14, 2026
@clayrisser
clayrisser deleted the fix/read-path-viewer-error-states branch August 14, 2026 20:43
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.

1 participant