Repository navigation
fix: Fixes HEIC uploads hanging indefinitely when ImageDecoder stalls - #81211
singhakanshu00 wants to merge 11 commits into
Conversation
|
The following accounts have interacted with this PR and/or linked issues. I will continue to update these lists as activity occurs. You can also manually ask me to refresh this list by adding the If you're merging code through a pull request on GitHub, copy and paste the following into the bottom of the merge commit message. To understand the WordPress project's expectations around crediting contributors, please review the Contributor Attribution page in the Core Handbook. |
|
I tested this PR on my original environment (Windows 11 / Edge, WordPress 7.1-RC1, no HEVC extension). After applying the fix and re-testing, I found that in my environment the client-side decode path in canvas-utils.ts (including this fix) is never reached at all. So this fix doesn't address the hang in my environment — the hang happens one step earlier, in the fallback-to-server-side path (server-side GD also doesn't support HEIC, so the fallback fails silently too). I've posted more detail on the root cause in #81043. Wanted to share this in case it's useful context, since the timeout mechanism itself does look correct for the case it's designed to handle (ImageDecoder claiming support it doesn't have). |
|
Correction to my comment above — the SharedArrayBuffer/COOP-COEP theory was wrong. I dug further and confirmed directly in my environment: The actual reason const isImage = file.type.startsWith('image/');
const isHeic = HEIC_MIME_TYPES.includes(file.type);On my machine (Windows 11, no HEVC Video Extension), The timeout/fallback mechanism in this PR still looks correct for what it's designed to fix ( |
So I believe in this case, we should show an error. if the windows machine does not have the "Windows HEVC Video Extension" extension, it cannot client side convert the image to JPEG. instead, it should inform the user it is unable to handle the format, and help them forward. Because HEIC errors are sometimes related to the platform and sometimes the browser and sometimes an extension (as in your case) we have worked to make the errors more specific to help users. See the recent work in #81130. |
one more thing though, we should break out any string changes to a separate PR if we want to try to land this fix in 7.1 since we are currently in RC and cant add or change strings. cc: @t-hamano I think we could fix this in a point release (7.1.1) because it is somewhat of an edge case. |
|
Makes sense — a clear error is definitely better UX than a silent hang, especially since (as you say) this can come from the platform, browser, or a missing extension, and the user has no way to tell which. Thanks for digging into this and confirming the repro. Happy to test once there's a build to try. Let me know if there's anything else useful I can check on my end in the meantime. |
|
Pushed an additional fix based on @369work's further debugging on the linked issue: on platforms where file.type comes back empty for HEIC files (Windows without the HEVC extension), the file was never being recognized as HEIC at all, skipping this PR's fix entirely and silently falling back to an unhandled upload. Added a fallback to sniff the .heic/.heif extension in that case, so it correctly routes into the HEIC handling below. Tested on macOS by simulating an empty file.type directly via the upload store (details in the issue thread) — before the fix, the block never receives an update; after, it hits the HEIC branch and either converts or fails cleanly. |
This issue always occurs when uploading HEIC images in a default Windows environment where HEVC Video Extension is not enabled, correct? Based on my experience, there are indeed cases where users want to upload HEIC images on Windows machines. For example, this could involve transferring images taken on an iPhone to a PC via USB, or after syncing images to a PC using iCloud or Google Photos, uploading HEIC images from the PC to a WordPress site. If so, it might be worth fixing by 7.1 RC2. I will investigate this on a Windows machine today or tomorrow. |
This is already being discussed in #81132 |
t-hamano
left a comment
There was a problem hiding this comment.
I was able to reproduce the issue, so I will report the details. I am not yet familiar with the architecture of client-side media processing, so please let me know if there is anything incorrect.
Server
I have set up a local environment using Studio. As shown below, HEIC is not supported on the server side.
Details
### wp-core ###
version: 7.1-RC1
site_language: en_US
user_language: en_US
timezone: +00:00
permalink: /%year%/%monthnum%/%day%/%postname%/
https_status: false
multisite: false
user_registration: 0
blog_public: 1
default_comment_status: open
environment_type: local
user_count: 1
dotorg_communication: true
### wp-paths-sizes ###
wordpress_path: D:\_studio\gutenberg
wordpress_size: loading...
uploads_path: D:\_studio\gutenberg/wp-content/uploads
uploads_size: loading...
themes_path: D:\_studio\gutenberg/wp-content/themes
themes_size: loading...
plugins_path: D:\_studio\gutenberg/wp-content/plugins
plugins_size: loading...
fonts_path: D:\_studio\gutenberg/wp-content/uploads/fonts
fonts_size: loading...
database_size: loading...
total_size: loading...
### wp-dropins (1) ###
db.php: true
### wp-active-theme ###
name: Twenty Twenty-Five (twentytwentyfive)
version: 1.5
author: the WordPress team
author_website: https://wordpress.org
parent_theme: none
theme_features: core-block-patterns, post-thumbnails, responsive-embeds, editor-styles, html5, automatic-feed-links, widgets-block-editor, block-templates, post-formats, editor-style
theme_path: D:\_studio\gutenberg/wp-content/themes/twentytwentyfive
auto_update: Disabled
### wp-themes-inactive (2) ###
Twenty Twenty-Four: version: 1.5, author: the WordPress team, Auto-updates disabled
Twenty Twenty-Three: version: 1.6, author: the WordPress team, Auto-updates disabled
### wp-mu-plugins (1) ###
99-studio-loader.php: author: (undefined), version: (undefined)
### wp-plugins-active (1) ###
Gutenberg: version: 23.7.0, author: Gutenberg Team, Auto-updates disabled
### wp-media ###
image_editor: WP_Image_Editor_GD
imagick_module_version: Not available
imagemagick_version: Not available
imagick_version: Not available
file_uploads: 1
post_max_size: 8M
upload_max_filesize: 2M
max_effective_size: 2 MB
max_file_uploads: 20
image_format_transforms: image/heic → image/jpeg, image/heif → image/jpeg, image/heic-sequence → image/jpeg, image/heif-sequence → image/jpeg
gd_version: bundled (2.1.0 compatible)
gd_formats: GIF, JPEG, PNG, WebP, BMP, AVIF, XPM
ghostscript_version: not available
### wp-server ###
server_architecture: Windows NT 10.0 AMD64
httpd_software: PHP/8.4.22 (Development Server)
php_version: 8.4.22 64bit
php_sapi: cli-server
max_input_variables: 1000
time_limit: 30
memory_limit: 128M
admin_memory_limit: 256M
max_input_time: 60
upload_max_filesize: 2M
php_post_max_size: 8M
curl_version: 8.20.0 OpenSSL/3.0.20
suhosin: false
imagick_availability: false
opcode_cache: true
opcode_cache_memory_usage: 41524512 of 132032512
opcode_cache_interned_strings_usage: 72.5% of 8388608 (2306968 free)
opcode_cache_hit_rate: 99.76
opcode_cache_full: false
pretty_permalinks: true
static_robotstxt_file: false
current: 2026-08-06T12:37:56+00:00
utc-time: Thursday, 06-Aug-26 12:37:56 UTC
server-time: 2026-08-06T12:37:53+00:00
### wp-database ###
extension: WP_SQLite_Driver
server_version: 8.0.38
client_version: 3.51.3
max_allowed_packet: undefined
max_connections: undefined
### wp-constants ###
WP_HOME: http://localhost:8881
WP_SITEURL: http://localhost:8881
WP_CONTENT_DIR: D:\_studio\gutenberg/wp-content
WP_PLUGIN_DIR: D:\_studio\gutenberg/wp-content/plugins
WP_MEMORY_LIMIT: 40M
WP_MAX_MEMORY_LIMIT: 256M
WP_DEBUG: true
WP_DEBUG_DISPLAY: false
WP_DEBUG_LOG: true
SCRIPT_DEBUG: false
WP_CACHE: false
CONCATENATE_SCRIPTS: undefined
COMPRESS_SCRIPTS: undefined
COMPRESS_CSS: undefined
WP_ENVIRONMENT_TYPE: local
WP_DEVELOPMENT_MODE: undefined
DB_CHARSET: utf8mb4
DB_COLLATE: undefined
EMPTY_TRASH_DAYS: 30
### wp-filesystem ###
wordpress: writable
wp-content: writable
uploads: writable
plugins: writable
themes: writable
fonts: does not exist
mu-plugins: writable
Browser
The Chrome browser returns the following information regarding the decoder:
await ImageDecoder.isTypeSupported('image/heic');
> falseconst decoder = await VideoDecoder.isConfigSupported({
codec: 'hvc1.1.6.L93.B0',
codedWidth: 4032, codedHeight: 3024,
});
decoder.supported
> trueOS
I have not installed HEVC Video Extensions.
In this environment, when I upload an HEIC image to the editor, file.type indeed becomes an empty string. Therefore, the approach of inferring the image format from sources other than file.type seems correct.
| const extension = getFileExtension( name )?.toLowerCase(); | ||
| return !! extension && extensions.includes( extension ); |
There was a problem hiding this comment.
I believe inferring file extensions from filenames is unreliable. It might be more accurate to determine if it's an HEIC image from the file buffer, similar to how isAnimatedGif might work.
The following code was generated by AI and has not been tested. @adamsilverstein might have more insights into this area.
/**
* ISOBMFF brands that identify a HEIC/HEIF image.
*
* `mif1`/`msf1` are the generic HEIF brands; they are also listed by AVIF
* files, which is why the AVIF brands are excluded first.
*/
const HEIC_BRANDS = [ 'heic', 'heix', 'heim', 'heis', 'hevc', 'hevx', 'hevm', 'hevs', 'mif1', 'msf1' ];
const AVIF_BRANDS = [ 'avif', 'avis' ];
/**
* Detects a HEIC/HEIF file from its ISOBMFF `ftyp` box.
*
* Used when the browser can't report a MIME type at all (`file.type === ''`),
* which happens on platforms lacking the OS-level HEVC codec.
*
* @param buffer First bytes of the file (64 is enough for the `ftyp` box).
* @return Whether the buffer starts with a HEIC/HEIF `ftyp` box.
*/
export function isHeicBuffer( buffer: ArrayBuffer ): boolean {
const view = new Uint8Array( buffer );
// A file-type box is at least: size (4) + 'ftyp' (4) + major brand (4).
if ( view.length < 12 ) {
return false;
}
const readBrand = ( offset: number ) =>
String.fromCharCode( ...view.subarray( offset, offset + 4 ) );
if ( readBrand( 4 ) !== 'ftyp' ) {
return false;
}
// Major brand at 8, minor version at 12, compatible brands from 16 on.
const boxSize = new DataView( buffer ).getUint32( 0 );
const end = Math.min( boxSize || view.length, view.length );
const brands = [ readBrand( 8 ) ];
for ( let offset = 16; offset + 4 <= end; offset += 4 ) {
brands.push( readBrand( offset ) );
}
// AVIF also carries the generic HEIF brands, so rule it out first.
if ( brands.some( ( brand ) => AVIF_BRANDS.includes( brand ) ) ) {
return false;
}
return brands.some( ( brand ) => HEIC_BRANDS.includes( brand ) );
}const isHeicByContent =
! file.type && isHeicBuffer( await file.slice( 0, 64 ).arrayBuffer() );
const isImage = file.type.startsWith( 'image/' ) || isHeicByContent;
const isHeic = HEIC_MIME_TYPES.includes( file.type ) || isHeicByContent;|
Since it's unlikely that many users will encounter this issue, I decided not to backport this PR to 7.1.0 for now. After the 7.1 release, if it turns out that this issue affects a larger number of users than anticipated, we can consider backporting it to a minor release. |
|
Claude did the digging on a neighboring bug, and it lands here: Heads up @singhakanshu00 - #81707 turns out to be the same family of bug as this one, and #81737 overlaps with the header check added here. That issue is a HEIC photo whose extension was changed to The One thing found while testing that may be worth knowing for #81043: there is a second place that routes uploads by Thanks for the work on this one - the header check was the right idea, it just needed a wider gate. |
Test ReportPatch tested: PATCH_URL Environment
Steps taken
Expected result
Screenshots/Screencast with results
|
I believe this is using the media library uploader instead of Gutenberg's and this hasn't been upgraded to use client side media yet. This will be fixed in: I forgot you can do this right inside the editor, I can see how that is confusing. The main flow I was testing is dragging an image directly onto the editor canvas to upload it, this will use the client side path and the image also ends up in your media library.
Thanks for confirming. |
|
@adamsilverstein The situation is a bit complicated, and I'm confused. Should we test and merge this PR or #81043 first? Also, should both PRs be backported to the 7.1 minor release? |
This PR fixes #81043 - did you mean to link to a different PR @t-hamano? @singhakanshu00 can you work on resolving the merge conflicts here and I will give the code a review. |
|
Thanks for sticking with this one @singhakanshu00. Here is the code review I promised - the header sniffing is the right approach and matches what @369work confirmed on Windows, so this is mostly small stuff on top of the conflict resolution. Claude went through the code for this one, findings below:
The test imports are the only thing I'd call blocking. The rest are worth a look but I don't want to hold this up over them, they are very minor. |
Sorry, I meant this PR and #81737. Which PR should we review first? |
@t-hamano Oh, right - these two PRs address separate GitHub Issues, but I suspect the underlying issue is the same. Maybe we can combine them into a single PR. I will review them to see if they can be combined and still fix both reported issues. |
`ImageDecoder.isTypeSupported()` can report HEIC support from the container format alone. When the platform codec is missing, `decode()` then never settles, and the upload hangs with no error. Race the decode against a timeout that closes the decoder, which rejects the pending call, and catch the rejection so the conversion falls through to the next strategy instead of stalling the upload. Folded in from #81211. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016wRcDnK65wbQKCjKoie65o
|
Hey @singhakanshu00, thanks for sticking with this one and for the timeout idea - it turned out to be the right guard, and you caught it before anyone had the environment to observe it. Claude did the comparison of the two branches, here is where it landed:
I'm proposing we close this one in favor of #81737 - please shout if you'd rather carry it yourself, happy to go that way instead. Either way, review eyes on #81737 would be welcome. |
|
@t-hamano I have rolled all of the changes into a single PR #81737 which could use a review. @singhakanshu00 I'm closing this one as it is superseded by #81737. Appreciate your review there if possible and I'll make sure to credit you when the fix lands. |
|
Hey 👋 I wanted to give you a heads-up since this pull request is affected by recent validation changes for changelog files. #83043 adds additional validation for changelog files. You'll note that this pull request is currently failing a "Required changes from trunk" check. What you'll need to do: You will need to either rebase or merge the latest code from |
|
Closing this one in favor of #81737 |


What?
Closes #81043
Fixes HEIC image uploads hanging indefinitely in the block editor. The upload spinner would spin forever with no console error, the original
.heicfile would be saved to the uploads folder, but no resized sub-sizes (150/300/768/1024) would ever be generated.Why?
As described in #81043, WordPress 7.1's client-side media processing is supposed to fall back to server-side processing transparently when the browser/OS can't decode HEIC client-side. Instead, the upload just hung silently.
Root cause:
canvasConvertToJpeg()inpackages/upload-media/src/canvas-utils.tstries three decoding strategies in order —createImageBitmap, WebCodecsImageDecoder, and HEIC-container-parsing + WebCodecsVideoDecoder. Strategies 1 and 3 are wrapped intry/catchso a failure falls through to the next strategy, but strategy 2 was not.On platforms where the underlying HEVC codec isn't actually available (e.g. Windows without the HEVC Video Extensions),
ImageDecoder.isTypeSupported()can optimistically report support based on container-format recognition alone. When that happens,decoder.decode()doesn't reject — it simply never settles. Since nothing throws, the surroundingtry/finallynever resolves either, and theawait canvasConvertToJpeg(...)call inprepareItem()(private-actions.ts) hangs indefinitely, sodispatch.cancelItem()is never reached. This matches the reported symptom exactly.How?
In
canvas-utils.ts, strategy 2 now:decoder.decode()against a 3-second timeout.decode()calls, turning the stuck promise into a real rejection.try/catch, matching strategies 1 and 3, so a timeout or rejection now falls through to strategy 3 (or the final "browser can't decode HEIC" error) instead of hanging the whole upload.Testing Instructions
I don't have a Windows machine without the HEVC extension, so I verified this on macOS (Chrome) by forcing the exact failure condition via DevTools, rather than relying on real hardware lacking the codec:
ImageDecoder; Safari doesn't expose it, so it's unaffected either way)..heicfile via the Image block or Media Library.trunk: the spinner hangs indefinitely — reproduces the reported bug..jpgand uploads successfully, with all sub-sizes (150/300/768/1024) generated correctly in the Media Library.Important caveat: because my Mac has real, working HEVC decode support, my test confirms the timeout/fallback mechanism works, but strategy 3 succeeding is specific to macOS. On a real Windows machine without the HEVC extension (the actual environment in the bug report),
VideoDecoder.isConfigSupported()in strategy 3 should correctly report unsupported and fall through to a clean, visible error message instead — rather than a silent hang. I have not been able to confirm this on real Windows hardware, so it would be great if someone could test this PR on a real Windows machine without the HEVC extension installed, to confirm the expected end-to-end behavior (clean error after ~3s, not a spinner).Testing Instructions for Keyboard
Not applicable — this PR only changes internal upload/decode logic that runs automatically during file upload. It doesn't introduce or modify any UI elements or keyboard interactions.
Screenshots or screencast
faulty.mov
fixed.mov
Use of AI Tools
Assisted by Claude