Skip to content

Fix file upload truncation on Safari#2645

Merged
limetech merged 2 commits into
unraid:masterfrom
mgutt:fix-upload-truncation
May 21, 2026
Merged

Fix file upload truncation on Safari#2645
limetech merged 2 commits into
unraid:masterfrom
mgutt:fix-upload-truncation

Conversation

@mgutt
Copy link
Copy Markdown
Contributor

@mgutt mgutt commented May 19, 2026

Fixes: https://product.unraid.net/p/7-3-0-file-manager-large-uploads-truncate-in-web-ui

Problem

On Safari, uploading files via the file manager resulted in only the first 20 MB being written. All subsequent chunks were silently dropped.

Root cause

When xhr.send() is called with a file-backed Blob, Safari sends the data correctly and receives the server's 200 response, but the XHR never transitions to readyState 4 (DONE). As a result, onload never fires, the upload loop never advances to the next chunk, and the connection hangs indefinitely. This is a WebKit bug specific to file-backed Blobs as XHR request bodies.

Fix

Convert each file slice to an ArrayBuffer via blob.arrayBuffer() before passing it to xhr.send(). This uses a different WebKit code path that correctly reaches readyState 4.

To avoid a sequential disk-read bottleneck (read chunk N, then send chunk N, then read chunk N+1, ...), the next chunk's arrayBuffer() call is started immediately when the current chunk begins sending, so the disk read for chunk N+1 overlaps with the network transfer of chunk N.

Testing

  • Chrome: 1 GB test file, ~70 MB/s, no regression
  • Safari: upload completes correctly for files larger than 20 MB

Summary by CodeRabbit

  • Bug Fixes
    • Improved file upload speed through optimized chunk processing
    • Fixed upload stalls on Safari iOS browsers
    • User-cancelled uploads no longer display error dialogs

Review Change Stack

On Safari iOS, XHR with a file-backed Blob as request body sends successfully
but never reaches readyState 4 (DONE), so onload never fires and the upload
loop stalls after the first chunk. Only the first 20 MB of any file was written.

This is a WebKit bug specific to file-backed Blobs as XHR request bodies.

Fix: convert each file slice to an ArrayBuffer via blob.arrayBuffer() before
passing it to xhr.send(). This uses a different WebKit code path that correctly
reaches readyState 4.

To avoid a sequential disk-read bottleneck, the next chunk's arrayBuffer() call
is started immediately when the current chunk begins sending, so the disk read
for chunk N+1 overlaps with the network transfer of chunk N.

Tested: Chrome 1 GB upload ~70 MB/s (no regression), Safari iOS confirmed working.
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 19, 2026

Walkthrough

The uploadFile function in Browse.page gains parallel prefetching of the next chunk's ArrayBuffer while transmitting the current chunk. Upload continuation now waits for the prefetch, transmission format switches to ArrayBuffer to address Safari iOS stalling, and error handling suppresses dialogs for user-cancelled uploads.

Changes

Upload performance and reliability

Layer / File(s) Summary
Parallel prefetch initialization
emhttp/plugins/dynamix/Browse.page
uploadFile now accepts prefetchedBuffer and initiates an asynchronous prefetch (nextBufferPromise) of the next chunk as ArrayBuffer while the current chunk is being sent.
Chunk continuation with prefetch and cancellation
emhttp/plugins/dynamix/Browse.page
The chunk-upload loop waits for nextBufferPromise before proceeding, passes the prefetched buffer to the next uploadFile call, and explicitly handles cancellation and promise failures.
XHR error handling and ArrayBuffer transmission
emhttp/plugins/dynamix/Browse.page
Error handlers suppress UI for user-cancelled uploads; request body now sends ArrayBuffer instead of Blob, using prefetched buffer when available and falling back on conversion failure; Safari iOS workaround prevents XHR payload stalling.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Poem

🐰 A rabbit hops through chunks with care,
Prefetching buffers in the air,
While XHR sends with grace,
No stalling in Safari's race!
Arrays flow where Blobs once were,
Upload performance starts to purr. ✨

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title accurately identifies the main issue being fixed—Safari file upload truncation—which aligns with the primary change in the changeset.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions
Copy link
Copy Markdown

github-actions Bot commented May 19, 2026

🔧 PR Test Plugin Available

A test plugin has been generated for this PR that includes the modified files.

Version: 2026.05.19.0030
Build: View Workflow Run

📥 Installation Instructions:

Install via Unraid Web UI:

  1. Go to Plugins → Install Plugin
  2. Copy and paste this URL:
https://preview.dl.unraid.net/pr-plugins/pr-2645/webgui-pr-2645.plg
  1. Click Install

Alternative: Direct Download

⚠️ Important Notes:

  • Testing only: This plugin is for testing PR changes
  • Backup included: Original files are automatically backed up
  • Easy removal: Files are restored when plugin is removed
  • Conflicts: Remove this plugin before installing production updates
  • Post-merge behavior: This preview stays available after merge until preview storage expires or it is manually cleaned up

📝 Modified Files:

Click to expand file list
emhttp/plugins/dynamix/Browse.page

🔄 To Remove:

Navigate to Plugins → Installed Plugins and remove webgui-pr-2645, or run:

plugin remove webgui-pr-2645

🤖 This comment is automatically generated and will be updated with each new push to this PR.

@mgutt mgutt changed the title Fix file upload truncation on Safari iOS Fix file upload truncation on Safari May 19, 2026
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
emhttp/plugins/dynamix/Browse.page (1)

1284-1317: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Honor cancellation before chaining the next upload.

cancel is only handled on the same-file continuation path. If the user cancels after a chunk finishes but before this callback advances, the prefetch branch can return without stopUpload(), and the next-file branch can still start files[index + 1].

Suggested fix
   xhr.onload = function() {
+    if (cancel === 1) {
+      stopUpload(file.name, false);
+      return;
+    }
+
     if (xhr.status < 200 || xhr.status >= 300) {
       stopUpload(file.name, true, 'http:' + xhr.status);
       return;
@@
       $('`#dfm_uploadStatus`').html("_(Uploading)_: <span class='dfm_percent'>"+percent+"%</span><span class='dfm_speed'>Speed: "+speed+"</span><span class='orange-text'> ["+(index+1)+'/'+files.length+']&nbsp;&nbsp;'+escapeHtml(file.name)+"</span>");
-      if (cancel === 1) return;
+      if (cancel === 1) {
+        stopUpload(file.name, false);
+        return;
+      }
       nextBufferPromise.then(function(nextBuffer) {
-        if (cancel === 1) return;
+        if (cancel === 1) {
+          stopUpload(file.name, false);
+          return;
+        }
         uploadFile(files,index,next,time,nextBuffer);
-      }).catch(function() { stopUpload(file.name, true, 'read'); });
+      }).catch(function() {
+        if (cancel !== 1) stopUpload(file.name, true, 'read');
+      });
     } else if (index < files.length-1) {
       // Clean up temp file for completed upload before starting next file
       $.post('/webGui/include/Control.php',{mode:'stop',file:encodeURIComponent(file.name)});
       uploadFile(files,index+1,0,time);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@emhttp/plugins/dynamix/Browse.page` around lines 1284 - 1317, The callback
must honor the cancel flag before chaining further work: in the
nextBufferPromise.then(...) handler and in the else-if branch that starts the
next file (the branch with uploadFile(files,index+1,0,time)), check if cancel
=== 1 and call stopUpload(file.name, true) (or otherwise abort) before calling
uploadFile or posting the stop temp-file request; likewise, ensure the
nextBufferPromise.catch path still calls stopUpload when cancel is set so no
subsequent upload starts after cancellation (refer to cancel,
nextBufferPromise.then, uploadFile, and stopUpload).
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@emhttp/plugins/dynamix/Browse.page`:
- Around line 1310-1313: The catch handlers on nextBufferPromise call
stopUpload(file.name, true, 'read') but that leaves the UI showing the generic
"File is removed" dialog; update those catch blocks (the ones that call
nextBufferPromise.then(...).catch(...)) to pass a clear, user-facing error
message to stopUpload indicating the read/arrayBuffer failure (e.g., "Failed to
read file data — could not convert to ArrayBuffer") or change stopUpload to
interpret errorType==='read' and display that specific message; apply the same
change to both occurrences that currently call stopUpload(..., 'read').

---

Outside diff comments:
In `@emhttp/plugins/dynamix/Browse.page`:
- Around line 1284-1317: The callback must honor the cancel flag before chaining
further work: in the nextBufferPromise.then(...) handler and in the else-if
branch that starts the next file (the branch with
uploadFile(files,index+1,0,time)), check if cancel === 1 and call
stopUpload(file.name, true) (or otherwise abort) before calling uploadFile or
posting the stop temp-file request; likewise, ensure the nextBufferPromise.catch
path still calls stopUpload when cancel is set so no subsequent upload starts
after cancellation (refer to cancel, nextBufferPromise.then, uploadFile, and
stopUpload).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: d5d4b6fb-fd05-43e4-9555-1ad42c250f4d

📥 Commits

Reviewing files that changed from the base of the PR and between 30231c5 and 9e837fb.

📒 Files selected for processing (1)
  • emhttp/plugins/dynamix/Browse.page

Comment thread emhttp/plugins/dynamix/Browse.page Outdated
@mgutt
Copy link
Copy Markdown
Contributor Author

mgutt commented May 19, 2026

@coderabbitai status

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 19, 2026

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Get PR status and details
gh pr view 2645 --repo unraid/webgui --json state,title,author,reviewDecision,statusCheckRollup,mergeable,mergedAt,closedAt,reviews,comments,additions,deletions,changedFiles 2>/dev/null | jq .

Length of output: 21156


@mgutt Here's the current status of PR #2645:


📋 PR Status — Fix file upload truncation on Safari

Item Status
PR State 🟢 Open
Mergeability ✅ Mergeable
CI (build-plugin) ✅ Passed
CodeRabbit check ✅ Passed
Review approval ⏳ Pending (no approval yet)

🔍 Open Review Comments (2)

  1. 🟠 Major — Cancellation not fully honored before chaining next upload (outside diff, lines ~1284–1317)

    • The else-if branch that starts uploadFile(files, index+1, ...) and the nextBufferPromise.then() handler don't check cancel === 1 and call stopUpload() before proceeding. A user cancelling mid-chunk could still trigger the next file upload. A suggested fix was provided in the review.
  2. 🟡 Minor — Unclear error message on ArrayBuffer read failure (inline, lines ~1310–1313)

    • The .catch() blocks call stopUpload(file.name, true, 'read'), which shows a generic "File is removed" dialog. Suggested to surface a more descriptive error message for the read/ArrayBuffer failure case.

The major cancellation issue is the most important one to address before this is ready to merge. Would you like me to provide a more detailed look at the suggested fix?

@mgutt
Copy link
Copy Markdown
Contributor Author

mgutt commented May 19, 2026

@coderabbitai
yes, check my last commit. it should have fixed the comments.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 19, 2026

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Get the latest commits on the PR
gh pr view 2645 --repo unraid/webgui --json commits | jq '.commits[-3:]'

# Get the diff of the latest commit
git log --oneline -5
git show HEAD --stat

Length of output: 2338


🏁 Script executed:

#!/bin/bash
# Read the relevant section of Browse.page around the upload logic
grep -n "cancel\|nextBufferPromise\|stopUpload\|uploadFile\|prefetch\|ArrayBuffer\|\.catch\|read'" emhttp/plugins/dynamix/Browse.page | head -80

Length of output: 2511


Let me read the complete relevant block to fully verify.


🏁 Script executed:

#!/bin/bash
# Read the full upload section
sed -n '1237,1375p' emhttp/plugins/dynamix/Browse.page

Length of output: 5602


@mgutt Both issues are fully addressed in your latest commit — here's a quick verification:

1. ✅ Cancellation fully honored at every branching point:

  • At the top of xhr.onload
  • Before nextBufferPromise.then() (same-file next chunk)
  • Inside nextBufferPromise.then() callback
  • In the .catch()stopUpload is skipped when cancel !== 1
  • In the else if (index < files.length-1) branch before starting the next file
  • In xhr.onerror — suppresses the network error dialog on user cancel
  • Before xhr.send(buffer) in the ArrayBuffer resolution promise

2. ✅ Descriptive error message on read failure:

  • stopUpload now handles errorType === 'read' with: "Failed to read file. The file may have been removed or is inaccessible."
  • Both .catch() handlers (prefetch and send) correctly pass 'read' as the error type

Everything looks good — I'll resolve both review comments now.

[resolve]

@elibosley elibosley added 7.3 and removed 7.3 labels May 20, 2026
@ljm42
Copy link
Copy Markdown
Member

ljm42 commented May 20, 2026

Note that this fix is for 7.3 only, it does not apply to 7.2

I was unable to repro the initial problem with Safari on macOS, and I don't have iOS to test with. But I can confirm the changes did not cause a regression... after installing the PR plugin on 7.3.0 I was still able to upload 300+MB files using Safari/Chrome on macOS as well as Chrome/Firefox on Windows.

LGTM

@elibosley elibosley added the 7.3 label May 21, 2026
@limetech limetech merged commit 375a4cd into unraid:master May 21, 2026
2 checks passed
elibosley added a commit that referenced this pull request May 22, 2026
## Summary

- Pins `actions/checkout` to the immutable commit currently referenced
by `v6.0.2`.
- Bounds the no-previous-tag path to `TAG^..TAG` when possible so the
workflow does not scan full tag ancestry.
- Restricts PR number extraction to explicit merge commit messages like
`Merge pull request #123`, avoiding accidental issue references like
`(#123)`.
- Adds Linear-side GitHub PR link resolution: discovered PR URLs are
passed to the sync script, which resolves Linear attachments for those
URLs and attaches the linked issues to the release.
- Fixes manual `workflow_dispatch` testing against historical tags by
checking out the workflow branch while still resolving the requested
`tag_name`.
- Carries active QA work across OS prereleases: internal tag sync now
attaches active issues to the exact prerelease, the stable companion
release, and the next planned prerelease. It removes next-prerelease
membership only after the issue reaches an internal/completed state.
- Sweeps existing issues already attached to the exact prerelease
through the same policy, so tickets are carried forward even when they
were not mentioned by the latest WebGUI tag diff.

## Review Notes

- Valid: mutable checkout tag. Fixed by pinning to
`de0fac2e4500dabe0009e67214ff5f5447ce83dd`.
- Valid: empty `PREVIOUS_TAG` range walked too much history. Fixed with
parent-bounded range and a parentless fallback.
- Valid: broad parenthetical PR extraction could treat issue references
as PR numbers. Fixed by restricting extraction to `Merge pull request
#[0-9]+` while preserving numeric extraction, `sort -u`, and `|| true`
behavior.
- Follow-up support: if the GitHub PR only links to Linear from the
Linear side, `attachmentsForURL` now resolves the issue from the PR URL.
- Manual test finding: checking out `tag_name` during
`workflow_dispatch` failed for historical tags because the tag did not
contain the new script. Fixed by checking out `github.ref` for manual
runs.
- Skipped: none. All reported findings applied to the current code.

## Validation

- `git diff --check`
- `node --check .github/scripts/sync-linear-release.mjs`
- `ruby -e 'require "yaml";
YAML.load_file(".github/workflows/linear-release.yml"); puts "yaml ok"'`
- Shell probe confirmed PR extraction returns `2645` from `Merge pull
request #2645 ...` and ignores parenthetical `(#2646)` / `(#123)`
references.
- Shell probe confirmed the first-tag fallback uses a single-commit
style range such as `6.2.2^..6.2.2` when a parent exists.
- Live Linear validation:
`attachmentsForURL(https://github.com/unraid/webgui/pull/2645)` resolved
`OS-239`, and the sync script attached `OS-239` to `7.3.1-rc.0.3`
without changing its `QA Ready` state.
- Manual workflow test passed:
https://github.com/unraid/webgui/actions/runs/26299583415 for
`tag_name=7.3.1-rc.0.3`; it synced the existing Linear Release and
attached `OS-239` idempotently.
- Local Linear validation after carry-forward change: `OS-239` is `QA
Ready` and attached to `7.3.1-rc.0.3`, `7.3.1-rc.0.4`, and `Unraid OS
7.3.1 Stable`.


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

## Chores
* Improved release automation and synchronization between GitHub PRs and
Linear issues, ensuring PR links are properly captured and tracked
throughout the release cycle
* Strengthened issue state management across multiple release versions,
providing better visibility into which changes are included in each
release
* Enhanced overall reliability of the release workflow

<!-- review_stack_entry_start -->

[![Review Change
Stack](https://storage.googleapis.com/coderabbit_public_assets/review-stack-in-coderabbit-ui.svg)](https://app.coderabbit.ai/change-stack/unraid/webgui/pull/2647?utm_source=github_walkthrough&utm_medium=github&utm_campaign=change_stack)

<!-- review_stack_entry_end -->

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants