Fixing WordPress media upload error: “HTTP error” without debug logs in wp-config

Root Cause Diagnosis

The “HTTP error” message is the Media Uploader’s generic fallback. It appears when the request to wp-admin/async-upload.php returns anything other than the expected JSON response: a non-200 status, a dropped connection, or a body corrupted by PHP warnings. Failures that occur before WordPress finishes loading are never written to wp-content/debug.log. These include a request rejected by the web server for body size, a PHP-FPM worker killed by the OOM killer or a timeout, and a WAF block. When debug.log stays empty, the failing layer is the web server, PHP-FPM, the kernel or a proxy, not WordPress core.

Quick Fix

The most common cause is an Imagick crash or memory spike during thumbnail generation. Force the GD image editor with a must-use plugin:

<?php
// wp-content/mu-plugins/force-gd.php
add_filter( 'wp_image_editors', function () {
    return array( 'WP_Image_Editor_GD' );
} );

If the upload now succeeds, keep Imagick off or apply the thread-limit fix in Step 5. If it still fails, continue with the steps below.

Step-by-Step Resolution

Step 1: Identify the failing layer from the HTTP response.

Open browser DevTools, go to the Network tab, upload a file, and select the async-upload.php request. The status code decides the branch:

StatusLikely layerGo to
413Web server body limitStep 4
500 / 502 / 504PHP fatal, FPM crash or timeoutSteps 3, 4, 5
403ModSecurity / WAFStep 7
200 with non-JSON bodyPHP warnings printed into the responseStep 2
Cancelled / (failed)Connection reset, proxy cutoffSteps 3, 7

Step 2: Enable logging correctly in wp-config.php.

Add these constants above the line /* That's all, stop editing! Happy publishing. */:

define( 'WP_DEBUG', true );
define( 'WP_DEBUG_LOG', true );
define( 'WP_DEBUG_DISPLAY', false );
@ini_set( 'display_errors', 0 );

WP_DEBUG_DISPLAY must be false. If it is true, any warning is printed into the AJAX response and itself triggers “HTTP error”. To log to an explicit path, use define( 'WP_DEBUG_LOG', '/var/log/wp-debug.log' ); and make sure the PHP user can write to it.

See also  Rank Math Primary Category Not Persisting on Bulk WooCommerce Product Import

The log is written to wp-content/debug.log. If the constants are ignored, check that they are not defined twice (the first definition wins), that no mu-plugin or host-level config overrides them, and that wp-content/ is writable by the PHP user.

Step 3: Read the server-side logs when debug.log is empty.

# Nginx
tail -n 100 /var/log/nginx/error.log

# Apache
tail -n 100 /var/log/apache2/error.log

# PHP-FPM (adjust version)
tail -n 100 /var/log/php8.2-fpm.log
journalctl -u php8.2-fpm --since "15 min ago"

# Kernel OOM killer
dmesg -T | grep -i -E "killed process|out of memory"

Patterns to look for:

  • client intended to send too large body indicates an Nginx body limit (Step 4).
  • upstream timed out or request_terminate_timeout indicates a timeout (Step 4).
  • Allowed memory size of ... exhausted indicates memory_limit (Step 4).
  • child exited on signal 11 or signal 9 indicates a segfault or OOM kill, often Imagick (Step 5).
  • ModSecurity: Access denied indicates a WAF rule (Step 7).

Step 4: Raise upload, memory and timeout limits at every layer.

PHP (/etc/php/8.2/fpm/php.ini, or .user.ini in the WordPress root on shared hosting):

upload_max_filesize = 64M
post_max_size = 64M
memory_limit = 256M
max_execution_time = 300
max_input_time = 300

post_max_size must be greater than or equal to upload_max_filesize. Restart PHP-FPM afterwards:

systemctl restart php8.2-fpm

Nginx (/etc/nginx/nginx.conf or the site vhost):

client_max_body_size 64M;

location ~ \.php$ {
    fastcgi_read_timeout 300;
    fastcgi_send_timeout 300;
}
nginx -t && systemctl reload nginx

Apache with mod_fcgid (/etc/apache2/mods-enabled/fcgid.conf):

FcgidMaxRequestLen 67108864

WordPress memory (wp-config.php):

define( 'WP_MEMORY_LIMIT', '256M' );
define( 'WP_MAX_MEMORY_LIMIT', '512M' );

Step 5: Fix Imagick threading and memory crashes.

Imagick can spawn multiple threads that exceed per-process limits on constrained hosts. Limit it at the top of wp-config.php:

putenv( 'MAGICK_THREAD_LIMIT=1' );

Alternatively, in .htaccess (Apache with mod_env):

SetEnv MAGICK_THREAD_LIMIT 1

Memory use during resizing scales with pixel dimensions, not file size. A 6000×4000 JPEG needs roughly 100 MB or more for decoding alone, regardless of its size on disk. Reduce the threshold for downscaling large originals:

add_filter( 'big_image_size_threshold', function () { return 2560; } );

Step 6: Verify filesystem permissions, space and temp directory.

# Ownership and mode
stat -c '%U:%G %a' wp-content/uploads

# Standard modes
find wp-content/uploads -type d -exec chmod 755 {} \;
find wp-content/uploads -type f -exec chmod 644 {} \;

# Free space and inodes
df -h /var/www /tmp
df -i /var/www /tmp

# PHP temp upload directory
php -r 'echo ini_get("upload_tmp_dir") ?: sys_get_temp_dir(), PHP_EOL;'

The directory must be owned by the PHP-FPM user (commonly www-data). A full /tmp or exhausted inodes produces the same “HTTP error”.

See also  WooCommerce order stock status not reducing on completed COD orders

Step 7: Isolate the layer with WP-CLI, then eliminate plugins and WAF.

Bypass the browser and web server entirely:

wp media import /path/to/test.jpg --allow-root
  • If this succeeds, PHP and image processing are fine. The fault is in the web server, proxy, WAF or a plugin hooking the admin request.
  • If this fails, the error output points directly at the PHP, GD/Imagick or filesystem problem.

Then rule out plugins and the theme:

wp plugin deactivate --all
wp theme activate twentytwentyfour

Re-enable plugins one at a time to find the culprit. Security, image-optimization and CDN-offload plugins are the most common offenders. For ModSecurity, check /var/log/modsec_audit.log for the rule ID, then whitelist that rule for /wp-admin/async-upload.php rather than disabling ModSecurity globally.

Why This Fails for Some Users

  • Cloudflare or reverse proxy limits. Cloudflare Free and Pro plans cap request bodies at 100 MB, and a response that takes more than about 100 seconds returns 524. Uploads larger than the cap fail before reaching the origin, so no origin log entry exists. Test by uploading directly against the origin IP or with the proxy disabled.
  • php_value in .htaccess under PHP-FPM. php_value and php_flag directives are only valid for mod_php. Under FPM or CGI they cause a 500 error. Use .user.ini instead.
  • .user.ini caching. PHP re-reads .user.ini only every user_ini.cache_ttl seconds (default 300). Changes may appear to have no effect for up to five minutes.
  • Control panels overriding PHP settings. Hosting panels often keep per-site PHP overrides separate from php.ini. Editing the global file does nothing if a per-site setting overrides it. Change the value in the panel’s PHP settings for that site, or in its generated pool config under /etc/php/8.2/fpm/pool.d/.
  • open_basedir restrictions. If open_basedir excludes the temp directory or wp-content/uploads, the move from the temp upload path fails. Verify with php -i | grep open_basedir.
  • SELinux. On RHEL-family systems, upload directories need the correct context. Run ls -Z wp-content/uploads and apply chcon -R -t httpd_sys_rw_content_t wp-content/uploads (or a persistent semanage fcontext rule). Denials appear in /var/log/audit/audit.log, not in PHP logs.
  • Unsupported image formats or color profiles. HEIC, CMYK JPEGs, and WebP or AVIF files on a build without the matching codec can crash Imagick or GD. Test with a plain sRGB JPEG to confirm.
  • Low pm.max_children or pm.max_requests recycling. Under FPM, a worker recycled mid-upload or a pool with too few workers drops the request. Check /etc/php/8.2/fpm/pool.d/www.conf and the FPM log for server reached pm.max_children.
  • Filenames with special characters. Non-ASCII or very long filenames can trip WAF rules or filesystem limits. Rename the file and retry.
  • Persistent object cache or CDN-offload plugins. A misconfigured offload plugin can fail after the local save, which still surfaces as “HTTP error”. The WP-CLI test in Step 7 exposes this when wp media import fails only with the plugin active.
See also  Fixing duplicate slug append (-2) on published WordPress pages with empty trash

Leave a Comment