You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
[$250] [Due for payment 2026-04-28] Reports - Web - Downloaded video has corrupted format #82894
Version Number: 9.3.22-1-staging Reproducible in staging?: Yes Reproducible in production?: Yes If this was caught during regression testing, add the test name, ID and link from BrowserStack:https://test-management.browserstack.com/projects/2219752/folder/13176922/test-cases/41237524 Email or phone of affected tester (no customers): N/A Issue reported by: Applause Internal Team Bug source: Regression TC Execution Device used: Windows 11, Chrome App Component: Search
Please re-state the problem that we are trying to solve in this issue.
When downloading a video file (e.g., MP4) from a report on web, the downloaded file has a corrupted format that cannot be played. This is because the downloaded blob loses its MIME type during the download process.
What is the root cause of that problem?
The root cause is in src/libs/fileDownload/DownloadUtils.ts around line 75. When a file is fetched, response.blob() correctly returns a Blob with the proper MIME type from the server's Content-Type header (e.g., video/mp4). However, this blob is then unnecessarily re-wrapped with new Blob([blob]) without passing the {type: blob.type} option.
According to the MDN Blob API documentation, the type property is not inherited from source content — it must be explicitly passed to the Blob constructor. As a result, the re-wrapped blob has an empty MIME type (""), and when URL.createObjectURL() is called on it, the resulting object URL lacks the format metadata needed for proper file recognition.
While this affects all downloaded file types, it is most apparent with video files because image formats (JPEG, PNG) rely on magic bytes for format self-detection, while video containers like MP4 are more dependent on MIME type metadata for proper recognition by media players and OS file handlers.
What changes do you think we should make in order to solve the problem?
The simplest and cleanest fix is to remove the unnecessary new Blob([blob]) wrapper entirely in src/libs/fileDownload/DownloadUtils.ts and pass the original blob directly to URL.createObjectURL():
Current (buggy):
consthref=URL.createObjectURL(newBlob([blob]));
Fixed:
consthref=URL.createObjectURL(blob);
Since response.blob() already returns a Blob object that is directly usable with URL.createObjectURL(), the re-wrapping step serves no purpose and only introduces the MIME type stripping bug.
What alternative solutions did you explore?
An alternative fix (instead of removing the wrapper) would be to preserve the MIME type when re-wrapping: new Blob([blob], {type: blob.type}). This also resolves the bug, but is less clean since the re-wrap is unnecessary in the first place. The simpler approach of using blob directly is preferred.
Summary: Downloaded video attachments on web are corrupted because the download utility strips the MIME type when re-wrapping the fetched blob, making the file unrecognizable to video players.
Root Cause: In src/libs/fileDownload/DownloadUtils.ts:75, the response blob is unnecessarily re-wrapped with new Blob([blob]) without preserving the content type. Per the MDN Blob API, the type property is not inherited from source content — it must be explicitly passed. The resulting blob has an empty MIME type (""), causing video files to lose format metadata needed for playback.
Recommended Fix: Remove the unnecessary new Blob([blob]) wrapper on line 75 of src/libs/fileDownload/DownloadUtils.ts and pass the original blob directly to URL.createObjectURL():
Since response.blob() already returns a Blob object directly usable with URL.createObjectURL(), the re-wrapping serves no purpose and only strips the MIME type.
Detailed Investigation
Root Cause Analysis
The download flow on web works as follows:
User clicks Download in the expanded attachment modal header (AttachmentModalBaseContent calls onDownloadAttachment)
useDownloadAttachment hook adds the encrypted auth token to the URL and calls fileDownload()
fileDownload() delegates to fetchFileDownload() in DownloadUtils.ts
fetch(url) retrieves the file → response.blob() returns a Blob with the correct MIME type (e.g., video/mp4) from the server's Content-Type header
BUG: new Blob([blob]) creates a new Blob that defaults type to "" — the original MIME type is lost
URL.createObjectURL() creates a blob URL for the now-typeless blob
Browser saves the file without proper MIME type metadata
Images (JPEG, PNG) survive this bug because they rely on magic bytes for format self-detection. Video containers like MP4, while they do have magic bytes (ftyp box), are more dependent on MIME type metadata for proper recognition by media players and OS file handlers — especially on Windows Chrome.
Not a Recent Regression
The new Blob([blob]) pattern has been present since at least September 2023 (the TypeScript migration, commit 0989000c). It was carried forward when DownloadUtils.ts was extracted as a separate file (commit b073f0e7, July 2024). This bug was likely never noticed because:
Video attachment downloads may not have been covered by automated tests
Image downloads, which are more common, are not visibly affected
Both Download Paths Are Affected
There are two entry points for downloading attachments on web, and both converge at the same buggy function:
This can lead to a situation where, when we have source = '/staging/chat-attachments/...' through this function (getValidatedImageSource, specifically in the function tryResolveUrlFromApiRoot) is added to the beginning of /staging, which leads to us having an incorrect source (it is equal to ‘/staging/staging/chat-attachments/...’)
But in the chat (report), and not in the search, this does not happen, because another component (Carousel) is rendered, which leads to this function being executed on the first render (or when switching between attachments):
Therefore, this bug does not occur, although if you add console.log, this bug will appear on the first render, but on the next one, the source changes to the correct one, which is why it is not noticeable in the report.
PS: Issue is reproducible only with Use Staging Server
What changes do you think we should make in order to solve the problem?
constonDownloadAttachment=useDownloadAttachment({
isAuthTokenRequired,
type
});
And we need to pass the correct source in this case, but in order not to break too much, I suggest changing this function so that it checks whether apiRoot and url both starts with the same value, and if so, then don't add the prefix. This should fix the problem:
// Avoid "/staging/staging/..." when apiRoot already includes "/staging"functiondedupeProxyPrefix(url: string,apiRoot: string){// Iterate only over proxy path values like "/staging/", "/staging-secure/"for(constprefixofObject.values(proxyConfig)){// normalize to ensure trailing slash consistencyconstnormalizedPrefix=prefix.endsWith('/') ? prefix : `${prefix}/`;constapiHasPrefix=apiRoot.endsWith(normalizedPrefix);consturlHasPrefix=url.startsWith(normalizedPrefix);if(apiHasPrefix&&urlHasPrefix){// remove only the leading prefix from urlreturnurl.slice(normalizedPrefix.length-1);// -1 because prefix starts with "/" and we want to keep leading slash}}returnurl;}/** * When possible this function resolve URLs relative to API ROOT * - Absolute URLs like `/{path}`, become: `https://{API_ROOT}/{path}` * - Similarly for prod or staging URLs we replace the `https://www.expensify` * or `https://staging.expensify` part, with `https://{API_ROOT}` * - Unmatched URLs (non expensify) are returned with no modifications */functiontryResolveUrlFromApiRoot(url: string): string;functiontryResolveUrlFromApiRoot(url: ImageSourcePropType): number;functiontryResolveUrlFromApiRoot(url: ReceiptSource): ReceiptSource;functiontryResolveUrlFromApiRoot(url: string|ImageSourcePropType|ReceiptSource): string|ImageSourcePropType|ReceiptSource;functiontryResolveUrlFromApiRoot(url: string|ImageSourcePropType|ReceiptSource): string|ImageSourcePropType|ReceiptSource{// in native, when we import an image asset, it will have a number representation which can be used in `source` of Image// in this case we can skip the url resolvingif(typeofurl!=='string'){returnurl;}constapiRoot=getApiRoot({shouldUseSecure: false});constnormalizedUrl=dedupeProxyPrefix(url,apiRoot);returnnormalizedUrl.replace(ORIGIN_PATTERN,apiRoot);}
PS: This is the main idea, we can improve it during the PR stage if necessary.
What alternative solutions did you explore? (Optional)
For second bug we can do that:
Remove tryResolveUrlFromApiRoot, and it will look like this:
returndecodeURIComponent(source);
But I think this parameter should be made dynamic so as not to break existing logic elsewhere. Reminder: Please use plain English, be brief and avoid jargon. Feel free to use images, charts or pseudo-code if necessary. Do not post large multi-line diffs or write walls of text. Do not create PRs unless you have been hired for this job.
Please re-state the problem that we are trying to solve in this issue.
Video attachments downloaded from reports on web have corrupted format and cannot be played, even though the file downloads successfully.
What is the root cause of that problem?
The fetchFileDownload function wraps the blob in new Blob([blob]) before passing to URL.createObjectURL(). This creates a new Blob object that loses the MIME type information from the original response.
When response.blob() is called, it returns a Blob with the correct type property (e.g., video/mp4) from the Content-Type header. However, new Blob([blob]) creates a generic Blob with an empty type string. Video players rely on this MIME type to recognize and decode the file format.
This also works but is unnecessarily verbose. Direct use of the blob is cleaner and matches the existing pattern in Sound/index.ts.
Why images work but videos don't: Image formats like JPEG and PNG have "magic bytes" at the start of the file that allow format self-detection. Video containers like MP4 are more dependent on MIME type metadata for proper recognition. This is why the bug manifests primarily with video files.
Files Modified:
src/libs/fileDownload/DownloadUtils.ts - Remove unnecessary Blob wrapper to preserve MIME type
Testing Notes:
Reproduce by downloading a video attachment from a report on web
Verify downloaded file plays correctly after the fix
Test with different file types (MP4, MOV, PDF) to ensure no regression
The fix aligns with the working pattern in Sound/index.ts
changed the title [-][Due for payment 2026-04-28] Reports - Web - Downloaded video has corrupted format[/-][+][$250] [Due for payment 2026-04-28] Reports - Web - Downloaded video has corrupted format[/+]on Apr 28, 2026
I’m not insisting on raising the price here, since I’m not sure, so I want to hear his opinion. Since we discussed this in part when reviewing the proposal. Fixing just the first bug would have been enough to resolve this issue, but we also fixed another bug as part of this task, even though it only occurs during local development (which is why QA couldn’t catch it).
@Eskalifer1 I don't think it should be treated as a separate issue. It seems like the same issue with two different causes. So, bounty increase does't seem necessary.
Passing the type alone would not have resolved the issue on the staging server, which would have made the solution incomplete. Thanks for testing and covering that as well.
Activity
melvin-bot commentedon Feb 19, 2026
Triggered auto assignment to @garrettmknight (
Bug), see https://stackoverflow.com/c/expensify/questions/14418 for more details. Please add this bug to a GH project, as outlined in the SO.twilight2294 commentedon Feb 19, 2026
Proposal
Please re-state the problem that we are trying to solve in this issue.
When downloading a video file (e.g., MP4) from a report on web, the downloaded file has a corrupted format that cannot be played. This is because the downloaded blob loses its MIME type during the download process.
What is the root cause of that problem?
The root cause is in
src/libs/fileDownload/DownloadUtils.tsaround line 75. When a file is fetched,response.blob()correctly returns aBlobwith the proper MIME type from the server'sContent-Typeheader (e.g.,video/mp4). However, this blob is then unnecessarily re-wrapped withnew Blob([blob])without passing the{type: blob.type}option.According to the MDN Blob API documentation, the
typeproperty is not inherited from source content — it must be explicitly passed to theBlobconstructor. As a result, the re-wrapped blob has an empty MIME type (""), and whenURL.createObjectURL()is called on it, the resulting object URL lacks the format metadata needed for proper file recognition.While this affects all downloaded file types, it is most apparent with video files because image formats (JPEG, PNG) rely on magic bytes for format self-detection, while video containers like MP4 are more dependent on MIME type metadata for proper recognition by media players and OS file handlers.
What changes do you think we should make in order to solve the problem?
The simplest and cleanest fix is to remove the unnecessary
new Blob([blob])wrapper entirely insrc/libs/fileDownload/DownloadUtils.tsand pass the original blob directly toURL.createObjectURL():Current (buggy):
Fixed:
Since
response.blob()already returns aBlobobject that is directly usable withURL.createObjectURL(), the re-wrapping step serves no purpose and only introduces the MIME type stripping bug.What alternative solutions did you explore?
An alternative fix (instead of removing the wrapper) would be to preserve the MIME type when re-wrapping:
new Blob([blob], {type: blob.type}). This also resolves the bug, but is less clean since the re-wrap is unnecessary in the first place. The simpler approach of usingblobdirectly is preferred.melvin-bot commentedon Feb 19, 2026
Triggered auto assignment to Contributor-plus team member for initial proposal review - @sobitneupane (
External)melvin-bot commentedon Feb 19, 2026
Unable to auto-create job on Upwork. The BZ team member should create it manually for this issue.
MelvinBot commentedon Feb 19, 2026
🔍 Issue Analysis
Summary: Downloaded video attachments on web are corrupted because the download utility strips the MIME type when re-wrapping the fetched blob, making the file unrecognizable to video players.
Root Cause: In
src/libs/fileDownload/DownloadUtils.ts:75, the response blob is unnecessarily re-wrapped withnew Blob([blob])without preserving the content type. Per the MDN Blob API, thetypeproperty is not inherited from source content — it must be explicitly passed. The resulting blob has an empty MIME type (""), causing video files to lose format metadata needed for playback.Recommended Fix: Remove the unnecessary
new Blob([blob])wrapper on line 75 ofsrc/libs/fileDownload/DownloadUtils.tsand pass the original blob directly toURL.createObjectURL():Since
response.blob()already returns aBlobobject directly usable withURL.createObjectURL(), the re-wrapping serves no purpose and only strips the MIME type.Detailed Investigation
Root Cause Analysis
The download flow on web works as follows:
AttachmentModalBaseContentcallsonDownloadAttachment)useDownloadAttachmenthook adds the encrypted auth token to the URL and callsfileDownload()fileDownload()delegates tofetchFileDownload()inDownloadUtils.tsfetch(url)retrieves the file →response.blob()returns a Blob with the correct MIME type (e.g.,video/mp4) from the server'sContent-Typeheadernew Blob([blob])creates a new Blob that defaultstypeto""— the original MIME type is lostURL.createObjectURL()creates a blob URL for the now-typeless blobImages (JPEG, PNG) survive this bug because they rely on magic bytes for format self-detection. Video containers like MP4, while they do have magic bytes (
ftypbox), are more dependent on MIME type metadata for proper recognition by media players and OS file handlers — especially on Windows Chrome.Not a Recent Regression
The
new Blob([blob])pattern has been present since at least September 2023 (the TypeScript migration, commit0989000c). It was carried forward whenDownloadUtils.tswas extracted as a separate file (commitb073f0e7, July 2024). This bug was likely never noticed because:Both Download Paths Are Affected
There are two entry points for downloading attachments on web, and both converge at the same buggy function:
useDownloadAttachment→fileDownload→fetchFileDownloadVideoPopoverMenuContext.downloadAttachment→fileDownload→fetchFileDownloadEvidence Chain
response.blob()at line 71 returns a Blob with correcttypefrom server Content-Type header ✓new Blob([blob])at line 75 creates new Blob without{type: blob.type}option → type defaults to""✓URL.createObjectURL(typelessBlob)creates blob URL without MIME metadata ✓.mp4) but empty MIME type → video player cannot play it ✓Relevant Code
src/libs/fileDownload/DownloadUtils.ts:75- The buggy line that strips MIME typesrc/libs/fileDownload/DownloadUtils.ts:8-27-createDownloadLinkfunctionsrc/pages/media/AttachmentModalScreen/routes/hooks/useDownloadAttachment.ts- Download trigger from attachment modalsrc/components/VideoPlayerContexts/VideoPopoverMenuContext.tsx:41-46- Download trigger from video popover menuImplementation Notes
new Blob([])wrapperNext Steps for Contributor+ team: Reply with
@MelvinBot implement thisto create a draft PR, or@MelvinBot <your feedback>to refine this analysis.Eskalifer1 commentedon Feb 19, 2026
🚨 Edited by proposal-police: This proposal was edited at 2026-02-23 13:31:39 UTC.
Proposal
Please re-state the problem that we are trying to solve in this issue.
Reports - Web - Downloaded video has corrupted format
What is the root cause of that problem?
We are missing file name for search, so we get very strange name for file, here is this part:
App/src/pages/media/AttachmentModalScreen/routes/hooks/useDownloadAttachment.ts
Line 29 in a6d885b
But we don't pass type here:
App/src/pages/media/AttachmentModalScreen/routes/report/ReportAttachmentModalContent.tsx
Lines 91 to 93 in a6d885b
So on Search page we get
fileName === '', which leads to unexpected behaviourThere is also another problem. When we open an attachment on the Search page, another component is rendered:
App/src/pages/media/AttachmentModalScreen/AttachmentModalBaseContent/index.tsx
Lines 242 to 276 in db4fb46
This leads to the use of this source:
App/src/pages/media/AttachmentModalScreen/routes/report/ReportAttachmentModalContent.tsx
Line 95 in db4fb46
App/src/libs/AvatarUtils.ts
Lines 113 to 125 in db4fb46
App/src/libs/tryResolveUrlFromApiRoot.ts
Lines 14 to 33 in db4fb46
This can lead to a situation where, when we have
source = '/staging/chat-attachments/...'through this function (getValidatedImageSource, specifically in the functiontryResolveUrlFromApiRoot) is added to the beginning of/staging, which leads to us having an incorrect source (it is equal to‘/staging/staging/chat-attachments/...’)But in the chat (report), and not in the search, this does not happen, because another component (Carousel) is rendered, which leads to this function being executed on the first render (or when switching between attachments):
App/src/pages/media/AttachmentModalScreen/AttachmentModalBaseContent/index.tsx
Lines 126 to 138 in db4fb46
This changes the source to the correct one:
App/src/pages/media/AttachmentModalScreen/AttachmentModalBaseContent/index.tsx
Line 131 in db4fb46
Therefore, this bug does not occur, although if you add console.log, this bug will appear on the first render, but on the next one, the source changes to the correct one, which is why it is not noticeable in the report.
PS: Issue is reproducible only with
Use Staging ServerWhat changes do you think we should make in order to solve the problem?
We need to pass here
type:App/src/pages/media/AttachmentModalScreen/routes/report/ReportAttachmentModalContent.tsx
Lines 91 to 93 in a6d885b
And we need to pass the correct source in this case, but in order not to break too much, I suggest changing this function so that it checks whether
apiRootandurlboth starts with the same value, and if so, then don't add the prefix. This should fix the problem:App/src/libs/tryResolveUrlFromApiRoot.ts
Lines 14 to 33 in cfcd45a
PS: This is the main idea, we can improve it during the PR stage if necessary.
What alternative solutions did you explore? (Optional)
App/src/libs/AvatarUtils.ts
Lines 120 to 122 in 67b752a
For second bug we can do that:
Remove
tryResolveUrlFromApiRoot, and it will look like this:But I think this parameter should be made dynamic so as not to break existing logic elsewhere.
Reminder: Please use plain English, be brief and avoid jargon. Feel free to use images, charts or pseudo-code if necessary. Do not post large multi-line diffs or write walls of text. Do not create PRs unless you have been hired for this job.
HiepPP commentedon Feb 19, 2026
Proposal
Please re-state the problem that we are trying to solve in this issue.
Video attachments downloaded from reports on web have corrupted format and cannot be played, even though the file downloads successfully.
What is the root cause of that problem?
The
fetchFileDownloadfunction wraps the blob innew Blob([blob])before passing toURL.createObjectURL(). This creates a new Blob object that loses the MIME type information from the original response.When
response.blob()is called, it returns a Blob with the correcttypeproperty (e.g.,video/mp4) from the Content-Type header. However,new Blob([blob])creates a generic Blob with an empty type string. Video players rely on this MIME type to recognize and decode the file format.The buggy code is at line 75:
App/src/libs/fileDownload/DownloadUtils.ts
Line 75 in 62b6b1b
What changes do you think we should make in order to solve the problem?
Pass the blob directly to
URL.createObjectURL()without wrapping:src/libs/fileDownload/DownloadUtils.tsThis follows the existing pattern in
src/libs/Sound/index.ts:62which correctly usesURL.createObjectURL(soundBlob)without unnecessary wrapping:App/src/libs/Sound/index.ts
Lines 61 to 63 in 62b6b1b
What alternative solutions did you explore? (Optional)
Alternative 1: Preserve the MIME type when wrapping:
This also works but is unnecessarily verbose. Direct use of the blob is cleaner and matches the existing pattern in Sound/index.ts.
Why images work but videos don't: Image formats like JPEG and PNG have "magic bytes" at the start of the file that allow format self-detection. Video containers like MP4 are more dependent on MIME type metadata for proper recognition. This is why the bug manifests primarily with video files.
Files Modified:
src/libs/fileDownload/DownloadUtils.ts- Remove unnecessary Blob wrapper to preserve MIME typeTesting Notes:
annaweber830 commentedon Feb 19, 2026
🚫 Duplicated proposal withdrawn by 🤖 ProposalPolice.
72 remaining items
melvin-bot commentedon Apr 28, 2026
Current assignees @sobitneupane and @Eskalifer1 are eligible for the External assigner, not assigning anyone new.
melvin-bot commentedon Apr 28, 2026
Job added to Upwork: https://www.upwork.com/jobs/~022049246091609971560
[-][Due for payment 2026-04-28] Reports - Web - Downloaded video has corrupted format[/-][+][$250] [Due for payment 2026-04-28] Reports - Web - Downloaded video has corrupted format[/+]mallenexpensify commentedon Apr 28, 2026
@sobitneupane 👀 above plz.
If you think the work warrants an increase, plz follow the steps in the C+ doc.
If not, please provide reasoning.
@Eskalifer1 can you please accept the job below? Please reply here and tag me once you have.
https://www.upwork.com/jobs/~022049246091609971560
Eskalifer1 commentedon Apr 29, 2026
Hi @mallenexpensify, i have accepted offer, thank you!
Let's wait for a response from @sobitneupane.
I’m not insisting on raising the price here, since I’m not sure, so I want to hear his opinion. Since we discussed this in part when reviewing the proposal. Fixing just the first bug would have been enough to resolve this issue, but we also fixed another bug as part of this task, even though it only occurs during local development (which is why QA couldn’t catch it).
sobitneupane commentedon Apr 30, 2026
@Eskalifer1 I don't think it should be treated as a separate issue. It seems like the same issue with two different causes. So, bounty increase does't seem necessary.
Passing the type alone would not have resolved the issue on the staging server, which would have made the solution incomplete. Thanks for testing and covering that as well.
Eskalifer1 commentedon Apr 30, 2026
Sure! It works for me
Eskalifer1 commentedon Apr 30, 2026
@mallenexpensify we can procees with payment here :)
mallenexpensify commentedon Apr 30, 2026
Payment Summary
Contributor: @Eskalifer1 paid $250 via Upwork
Contributor+: @sobitneupane due $250 via NewDot
^ Test case created
JmillsExpensify commentedon May 24, 2026
$250 approved for @sobitneupane