Skip to content
  • FG-21
  • 14 min read
  • Last checked November 8, 2026

How to Fix “Updating Failed. The Response Is Not a Valid JSON Response” in WordPress

The block editor says “Updating failed” or “Publishing failed” because the REST API returned something other than JSON. Find what answered instead: a firewall, a redirect, a 404 or a PHP notice.

Run the health check

You click Update or Publish in the WordPress block editor and a notice appears at the top: Updating failed. The response is not a valid JSON response. or Publishing failed. The response is not a valid JSON response. Sometimes it only happens on one post, or only when you upload an image, or only for one user. Site Health may also report The REST API encountered an error or The REST API encountered an unexpected result.

The block editor is a JavaScript application running in your browser. It saves your work by sending requests to the WordPress REST API, at addresses that start with /wp-json/, and it expects JSON back. This error means something else answered: an HTML error page, a firewall block page, a login page, a redirect, or JSON with a PHP warning glued to the front. Your job is to see that response and identify who sent it.

What is happening

When you press Update, the editor sends a request like POST /wp-json/wp/v2/posts/POST_ID with your post content as JSON, plus a security token called a nonce in an X-WP-Nonce header. That request passes through every layer between your browser and PHP: your browser extensions, Cloudflare or another CDN, the host’s web application firewall, the web server’s rewrite rules, and finally WordPress and its plugins. Any of them can answer instead of WordPress.

Status of the failed request Response body looks like Usual cause
404 Your theme’s “page not found” or a server 404 page Rewrite rules for /wp-json/ missing: permalinks, .htaccess or Nginx config
403 “Forbidden”, “Not Acceptable”, “Access denied” or a Cloudflare page ModSecurity, a host WAF or a Cloudflare rule blocking the content
403 {"code":"rest_cookie_invalid_nonce","message":"Cookie check failed"} The editor’s nonce expired or a cached admin page served a stale one
401 or 403 JSON mentioning REST access being restricted A security or “disable REST API” plugin
301 or 302 Empty, then a redirect siteurl or home use a different scheme or host from the editor
200 <br /><b>Warning</b>: ... followed by JSON PHP notices displayed on screen
413 “Request Entity Too Large” The post is bigger than the server’s request size limit
500, 502, 504 A server error page A PHP fatal error, crash or timeout during save
(failed) or CORS error Nothing; an error in the Console Mixed content or a cross-origin request

Notice that a 403 can mean two very different things. When the body is JSON, WordPress itself refused. When it is HTML, something in front of WordPress refused. That single distinction saves hours.

Before you start

You need a desktop browser and, for server fixes, SSH, SFTP or your hosting file manager. Copy the post content into a text file first, so nothing is lost if the editor reloads.

How to run the commands in this guide. Browser steps use DevTools: press F12, or Cmd+Option+I on a Mac. curl commands run on your own computer: built in on macOS and Linux, and curl.exe in Windows PowerShell (type curl.exe so you do not get the PowerShell alias) or inside WSL. Server commands run in an SSH terminal from your site folder, the one that contains wp-config.php; WP-CLI is called wp on most hosts and wp-cli on some.

Step 1: Look at the failed request in DevTools

This one step tells you which of the following steps you need.

  1. Open the post in the editor, open DevTools and choose the Network tab.
  2. Type wp-json into the filter box, or rest_route if your site has plain permalinks.
  3. Click Update in the editor and wait for the error.
  4. Click the request shown in red. Note the Status in the Headers panel, then open the Response or Preview panel.
  5. Also check the Console tab for red messages about Mixed Content or CORS.

Match what you see to the table above and jump to the matching step. Note the response headers too: cf-ray means the response passed through Cloudflare, cf-mitigated: challenge means Cloudflare served a challenge page instead of your site, and a server header naming your web server with an HTML body points at the host.

Right-click the failed request and choose Copy as cURL to get a command that repeats it exactly; you will use it in the Advanced section. Treat it like a password, because it contains your login cookie.

Step 2: Test the REST API from outside

curl -sS -i https://example.com/wp-json/ | head -n 20
curl -sS -i "https://example.com/?rest_route=/" | head -n 20
curl -sS -i https://example.com/wp-json/wp/v2/types/post | head -n 20

Run these on your own computer, replacing example.com with your domain. -i includes the response headers in the output and head -n 20 shows the first 20 lines. The first command uses the pretty /wp-json/ address. The second asks for the same thing with a query string, which works even when rewrite rules are broken. The third fetches one public endpoint.

Good output: HTTP/2 200, a content-type: application/json; charset=UTF-8 header, and a body starting with {"name": for the first two. How to read failures:

  • First fails with 404, second works: rewrite rules are the problem. Step 3.
  • Both return 403 with HTML: a firewall blocks the REST API itself. Step 4.
  • Both return 401 or 403 with JSON naming REST restrictions: a plugin limits the API to logged-in users or turns it off. Step 6. Restricting it for visitors is fine, as long as logged-in editors are allowed.
  • A 301 to a different address: Step 5.
  • All three work but saving fails: the problem depends on your session or your content. Steps 4, 6 and 7.

Step 3: Fix permalinks and rewrite rules

The pretty /wp-json/ address only exists because the web server sends unknown addresses to WordPress’s index.php. After a migration, a server change, or an edited .htaccess, that rule can disappear.

The quick fix is to go to Settings, Permalinks and click Save Changes without changing anything. That rebuilds WordPress’s rewrite rules and, on Apache, rewrites .htaccess if it can. From the server:

wp rewrite flush --hard
wp rewrite structure

Run these over SSH in the site folder. wp rewrite flush regenerates the rewrite rules, and --hard also writes them to .htaccess on Apache. The second prints the current permalink structure, such as /%postname%/; blank output means plain permalinks, where WordPress uses the ?rest_route= form automatically. Good outcome: the first curl test in Step 2 now returns 200.

Apache and LiteSpeed

Open .htaccess in the site folder and make sure the standard WordPress block is present and not commented out:

# BEGIN WordPress
<IfModule mod_rewrite.c>
RewriteEngine On
RewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}]
RewriteBase /
RewriteRule ^index\.php$ - [L]
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule . /index.php [L]
</IfModule>
# END WordPress

This is the content of the .htaccess file you edit on the server. If WordPress lives in a subfolder, RewriteBase and the last rule include that folder, for example /blog/ and /blog/index.php. The HTTP_AUTHORIZATION line passes authorization headers through to PHP, which application passwords need. Rules placed above the block, such as security rules from a plugin, run first and can intercept /wp-json/.

Nginx

Nginx ignores .htaccess. The site’s server block needs a location rule that falls back to index.php:

location / {
    try_files $uri $uri/ /index.php?$args;
}

This goes in the Nginx configuration for the site, on a server you manage. try_files serves a real file or folder if one exists and otherwise hands the request to WordPress with the query string attached. Test with nginx -t and reload Nginx. On managed hosting, open a ticket and ask for the standard WordPress rewrite rule.

Step 4: Find firewall false positives (ModSecurity, host WAF, Cloudflare)

Web application firewalls inspect request bodies for patterns that look like attacks. A blog post containing a code sample, an <iframe> embed, SQL keywords, a long base64 image, or certain words in a particular order can match a rule. The firewall answers with its own 403 HTML page, and the editor reports invalid JSON.

Confirm it

  • Create a new draft with one plain sentence and save it. If that works while your real post fails, the content is triggering a rule.
  • Remove blocks from the failing post one at a time (custom HTML, embeds, code blocks first) until it saves. That block contains the trigger.
  • The failed request returns 403 with an HTML body, often with words like “Not Acceptable”, “Forbidden” or “Mod_Security”.

Host firewall or ModSecurity

If you manage the server, the ModSecurity audit log or the web server error log holds lines like this:

ModSecurity: Access denied with code 403 (phase 2). Matched "Operator ..." [file "RULE_FILE"] [id "RULE_ID"] [msg "RULE_DESCRIPTION"] [hostname "example.com"] [uri "/wp-json/wp/v2/posts/POST_ID"]

This is example log output. RULE_ID is the number you need: it identifies the exact rule that fired, and uri confirms it was the save request. Do not switch ModSecurity off for the whole site. The right fix is an exclusion for that rule ID on the editor’s REST paths, which your host can add. Many hosting panels let you toggle ModSecurity per domain, which is useful to confirm the diagnosis for a minute, but turn it back on afterward.

Cloudflare

In the Cloudflare dashboard, open Security Events (currently under Security, Analytics; labels change), and filter by path containing /wp-json/ or by the Ray ID from the failed request’s cf-ray header. Each event shows which product acted: a managed WAF rule, a custom rule, rate limiting, or bot protection. Then adjust that specific rule, for example with a WAF exception scoped to the URI path /wp-json/wp/v2/ and, if possible, to your office IP address. Bot Fight Mode cannot be scoped by path or skipped with custom rules; if it is challenging editor requests, test with it off. Avoid skipping security based only on the presence of the wordpress_logged_in cookie, because anyone can send a cookie with that name.

admin-ajax.php is affected too

Older plugin screens, page builders and the classic editor use /wp-admin/admin-ajax.php rather than the REST API. The same firewall rules can block it, causing spinning loaders or settings that never save. Diagnose it the same way: filter the Network tab for admin-ajax and look for HTML 403 responses.

Step 5: Fix mismatched site addresses, mixed content and CORS

WordPress builds the REST API address from your Site Address (home). If you edit at https://www.example.com/wp-admin/ but home is http://example.com, the editor sends its requests to a different scheme or host. The browser then blocks the request as mixed content (an HTTPS page calling HTTP), or treats it as cross-origin, or the server redirects it and the POST becomes a GET without your content.

wp option get home
wp option get siteurl

Run these over SSH. Good output: both show the same scheme and host you see in the address bar while editing, such as https://example.com. If they differ, update them with wp option update home 'https://example.com' and the same for siteurl, replacing the address with your canonical one. If either is set by WP_HOME or WP_SITEURL in wp-config.php, change it there instead. If HTTPS is terminated by a proxy and WordPress thinks requests are HTTP, see our login loop guide for the X-Forwarded-Proto fix, which also repairs REST URLs.

CORS errors

CORS only applies when the editor and the API are on different origins: a headless front end, a mapped domain, or the www mismatch above. WordPress sends Access-Control-Allow-Origin itself on REST responses. If your web server or CDN adds its own copy, the browser reports that the header “contains multiple values” and refuses the response. Check the response headers in DevTools; if the header appears twice, remove the one added in server or CDN configuration.

Step 6: Check security and REST restriction plugins, and the nonce

Several popular plugins can limit the REST API: dedicated “disable REST API” plugins, performance plugins with a REST option, and security suites. Solid Security, for example, has a REST API setting with Default Access and Restricted Access options; restricted access still allows logged-in users. Problems start when a rule blocks logged-in users too, or blocks specific routes such as /wp/v2/users, which the editor uses for the author dropdown.

wp plugin list --status=active --fields=name,title
wp plugin deactivate PLUGIN_SLUG

Run these over SSH. Replace PLUGIN_SLUG with a security, firewall, performance or REST-related plugin from the name column, and retry saving after each. Good output: “Plugin ‘PLUGIN_SLUG’ deactivated.” and then a working save. Reactivate it and adjust its REST setting rather than leaving it off.

“Cookie check failed”

A JSON 403 with the code rest_cookie_invalid_nonce means the nonce sent by the editor did not match your session. A nonce is valid for up to 24 hours, so an editor tab left open overnight, a login in another tab, or a page cache that stored the admin page and serves an old nonce will all cause it. Reload the editor. If it keeps happening, exclude /wp-admin/ and logged-in users from every cache, as described in the login loop guide.

Step 7: Stop PHP output from corrupting the JSON

If the failed request has status 200 but the Response tab starts with <br /> and a PHP warning or notice, PHP is printing errors into the response. JSON parsers reject anything before the opening brace.

grep -nE "WP_DEBUG|display_errors" wp-config.php
wp eval 'var_dump( ini_get( "display_errors" ) );'

Run these over SSH in the site folder. The grep shows how debugging is configured. The wp eval line prints the command-line PHP value of display_errors; the web value can differ, so also check your hosting panel’s PHP settings. Bad signs: WP_DEBUG true without WP_DEBUG_DISPLAY set to false, or display_errors on.

The fix on a live site: keep logging and hide display, using define( 'WP_DEBUG_DISPLAY', false ); and @ini_set( 'display_errors', 0 ); in wp-config.php, and turn display_errors off in the hosting panel. Then fix the warning itself, which the log now records with a file and line. A related cause is a byte order mark or blank line before <?php in a theme or plugin file; the response then starts with invisible characters, and the PHP log usually reports “headers already sent” with the file name.

Large posts and slow saves

A 413 status means the request body exceeded a server limit, such as Nginx’s client_max_body_size or Apache’s LimitRequestBody; your host can raise it. A 504 after a long wait means saving took too long, often because a plugin does slow work on save, such as pinging an external service. Our white screen guide covers reading PHP-FPM timeout lines, and the fix is usually the plugin, not the timeout.

Advanced: trace a failing save around the CDN and through Site Health

Replay the request to the origin

Paste the command from “Copy as cURL” (Step 1) into your terminal and add -i so you can see headers. If it reproduces the failure, you have a repeatable test. Then send the same request straight to your origin server, skipping the CDN:

curl -sS -i --resolve example.com:443:ORIGIN_IP https://example.com/wp-json/ | head -n 20

Run this on your own computer. ORIGIN_IP is your server’s real IP address, found in your hosting panel or in the DNS records at your DNS provider (the A record behind the Cloudflare proxy). --resolve makes curl connect to that IP while still sending your domain name, so the server’s TLS certificate and virtual host still match. Apply the same --resolve option to your copied save request. If the request works at the origin but fails through the CDN, the CDN’s rules are responsible. If it fails at the origin too, look at the host’s firewall and WordPress itself. Some origins only accept traffic from the CDN, in which case this test times out and tells you nothing.

Read the Site Health REST test correctly

Under Tools, Site Health, the REST API test makes a request from your server to itself at /wp-json/wp/v2/types/post?context=edit, sending your login cookies. Its result shows the endpoint and the response code, such as REST API Response: (403) Forbidden. Because it is a loopback request, it can fail for reasons your browser never hits: the server cannot resolve its own domain, a firewall blocks the server’s own IP, or Cloudflare challenges the server as a bot. So:

  • Site Health fails and the editor fails: a real REST problem; follow Steps 2 to 7.
  • Site Health fails but the editor saves fine: a loopback problem. Ask your host whether the server can reach its own public address, and check your firewall and Cloudflare for blocks against the server’s IP. It still matters, because scheduled tasks and some updates rely on loopback requests.
  • Site Health passes but the editor fails: the trigger is your content, your browser, or your network path. Steps 4 and 5.

When to contact your host

Open a ticket when the failed request returns an HTML 403 that is not from Cloudflare, when you need a ModSecurity rule exclusion, when you cannot change rewrite rules on Nginx, or when request size or timeout limits need raising. Send:

  • The full URL of the failed request, such as /wp-json/wp/v2/posts/POST_ID, and its status code.
  • The time it happened, in UTC, so they can find the firewall log entry.
  • The response body and headers from DevTools (remove the Cookie header and nonce first).
  • Your public IP address, and any Ray ID from the cf-ray header.
  • Whether a plain-text draft saves, which tells them it is content-based.

The Emergency Room has a quick triage path if more of the admin is broken, and our diagnostic tools let you check headers and responses for /wp-json/ from outside your network. If the editor loads but the page builder layout is broken after an update, see our page builder guide instead.

Still stuck?

Print a free one-page checklist to keep, or open the Emergency Room if the symptom does not match.

Get the free checklists

Keep reading

All field guides

Search the field manual

What is going wrong?

Press / to search from any page, Esc to close