Skip to content

fix(media): force file download via fetch+blob instead of window.open (EVO-999)#44

Open
marcelogorutuba wants to merge 1 commit into
developfrom
fix/EVO-999
Open

fix(media): force file download via fetch+blob instead of window.open (EVO-999)#44
marcelogorutuba wants to merge 1 commit into
developfrom
fix/EVO-999

Conversation

@marcelogorutuba
Copy link
Copy Markdown
Member

@marcelogorutuba marcelogorutuba commented May 6, 2026

Summary

  • MessageFile.tsx: replaced openAttachmentInNewTab (which used window.open) with fetch → blob → <a download> so that clicking the download button triggers a real file download instead of opening the file in a new browser tab
  • Falls back to openAttachmentInNewTab only when fetch fails (e.g. CORS restriction)
  • Removes unused toast import

Validation

  • evo-ai-frontend-community: pnpm exec tsc -b --noEmit → no errors

Changed Files

  • src/components/chat/messages/MessageFile.tsx

Related PRs

Linked Issue

  • EVO-999

Summary by Sourcery

Ensure chat message file attachments are downloaded as files instead of being opened in a new browser tab, with a graceful fallback when direct download is not possible.

Bug Fixes:

  • Fix file attachment downloads that previously opened in a new browser tab instead of triggering a direct download.

Enhancements:

  • Use a fetch-to-blob download flow for chat message attachments with a fallback to the previous window-based behavior when fetch fails.

… (EVO-999)

Replace openAttachmentInNewTab with fetch → blob → <a download> so that
clicking the download button on a file attachment triggers a real download
instead of opening the file in a new browser tab. Falls back to
openAttachmentInNewTab only when fetch fails (e.g. CORS).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@sourcery-ai
Copy link
Copy Markdown

sourcery-ai Bot commented May 6, 2026

Reviewer's guide (collapsed on small PRs)

Reviewer's Guide

Implements a more robust file download mechanism for chat message attachments by replacing window.open-based behavior with a fetch-to-blob download flow, with a graceful fallback to the previous implementation when fetch fails, and cleans up an unused import.

Sequence diagram for the new attachment download flow

sequenceDiagram
  actor User
  participant MessageFileComponent
  participant BrowserFetchAPI as BrowserFetchAPI
  participant Server
  participant BrowserDownload as BrowserDownload
  participant FallbackWindowOpen as FallbackWindowOpen

  User->>MessageFileComponent: click download button
  MessageFileComponent->>MessageFileComponent: downloadFile(attachment)
  MessageFileComponent->>MessageFileComponent: validate and trim url
  alt url is empty
    MessageFileComponent-->>User: no action
  else url is valid
    MessageFileComponent->>MessageFileComponent: resolve filename
    MessageFileComponent->>BrowserFetchAPI: fetch(url, mode: cors)
    alt fetch succeeds
      BrowserFetchAPI->>Server: HTTP GET url
      Server-->>BrowserFetchAPI: file response
      BrowserFetchAPI-->>MessageFileComponent: Response
      MessageFileComponent->>BrowserFetchAPI: response.blob()
      BrowserFetchAPI-->>MessageFileComponent: Blob
      MessageFileComponent->>BrowserDownload: URL.createObjectURL(blob)
      MessageFileComponent->>BrowserDownload: create <a> with href and download
      MessageFileComponent->>BrowserDownload: link.click()
      BrowserDownload-->>User: file is downloaded
      MessageFileComponent->>BrowserDownload: URL.revokeObjectURL(blobUrl)
    else fetch throws error
      MessageFileComponent->>FallbackWindowOpen: openAttachmentInNewTab(url, filename)
      FallbackWindowOpen-->>User: open file in new tab or previous behavior
    end
  end
Loading

Flow diagram for the updated downloadFile logic

flowchart TD
  A[Start downloadFile with attachment] --> B[Extract and trim url from attachment.data_url]
  B --> C{Is url non-empty?}
  C -- No --> D[Return without action]
  C -- Yes --> E[Determine filename from attachment.fallback_title or translation]
  E --> F[Call fetch with url and mode cors]
  F --> G{Did fetch succeed?}
  G -- Yes --> H[Get blob from response]
  H --> I[Create blobUrl with URL.createObjectURL]
  I --> J[Create anchor element]
  J --> K[Set anchor.href to blobUrl and anchor.download to filename]
  K --> L[Trigger anchor.click to start download]
  L --> M[Revoke blobUrl with URL.revokeObjectURL]
  M --> N[End]
  G -- No (throws) --> O[Call openAttachmentInNewTab with url and filename]
  O --> N[End]
Loading

File-Level Changes

Change Details Files
Replace window.open-based attachment handling with a fetch → blob → flow that triggers a direct file download, with a fallback to the previous behavior when fetch fails.
  • Make downloadFile async and guard against missing or whitespace-only attachment URLs
  • Derive a filename from the attachment fallback title or translation fallback key
  • Use fetch with CORS mode to retrieve the file, convert the response to a blob, and create an object URL
  • Programmatically create and click an element with the download attribute to trigger a download, then revoke the object URL
  • On fetch errors, call the existing openAttachmentInNewTab helper with the URL and filename to preserve previous behavior
src/components/chat/messages/MessageFile.tsx

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

Copy link
Copy Markdown

@sourcery-ai sourcery-ai Bot left a comment

Choose a reason for hiding this comment

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

Hey - I've found 1 issue, and left some high level feedback:

  • In the new downloadFile implementation, consider checking response.ok (and possibly status codes) before calling response.blob() so that HTTP errors don’t result in attempting to download an error page as a file.
  • You might want to move the URL.revokeObjectURL(blobUrl) call into a finally block (after the link.click()) to ensure the object URL is always cleaned up even if an error occurs after it is created.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- In the new `downloadFile` implementation, consider checking `response.ok` (and possibly status codes) before calling `response.blob()` so that HTTP errors don’t result in attempting to download an error page as a file.
- You might want to move the `URL.revokeObjectURL(blobUrl)` call into a `finally` block (after the `link.click()`) to ensure the object URL is always cleaned up even if an error occurs after it is created.

## Individual Comments

### Comment 1
<location path="src/components/chat/messages/MessageFile.tsx" line_range="30-31" />
<code_context>
+      link.download = filename;
+      link.click();
+      URL.revokeObjectURL(blobUrl);
+    } catch {
+      openAttachmentInNewTab({ url, filename });
     }
   };
</code_context>
<issue_to_address>
**question:** Consider whether to restore user feedback for the download fallback path.

Previously, when `openAttachmentInNewTab` fell back to a download flow, we showed a `downloadStarted` toast. With this change, that feedback is lost if the direct fetch fails and we call `openAttachmentInNewTab`. If you want to preserve that behavior, consider either propagating a signal from `openAttachmentInNewTab` or showing a generic info toast in this catch branch.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment on lines +30 to +31
} catch {
openAttachmentInNewTab({ url, filename });
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

question: Consider whether to restore user feedback for the download fallback path.

Previously, when openAttachmentInNewTab fell back to a download flow, we showed a downloadStarted toast. With this change, that feedback is lost if the direct fetch fails and we call openAttachmentInNewTab. If you want to preserve that behavior, consider either propagating a signal from openAttachmentInNewTab or showing a generic info toast in this catch branch.

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