The Response Is Not a Valid JSON Response: Fix It

The Response Is Not a Valid JSON Response: Fix It

“Updating failed. The response is not a valid JSON response” means the block editor sent your save to the REST API and got back something that was not JSON — an HTML error page, a PHP warning, a firewall block page, or an empty body. Your post is not corrupt and the problem is usually not in WordPress itself: it is in whatever sits between the editor and wp-json.

Every list of fixes for this error tells you to resave permalinks, rename .htaccess and disable your plugins. One of those may work by accident. The faster route is that the failing response already names the cause, and reading it takes about thirty seconds.

The thirty-second version

Open DevTools (F12), go to Network, filter to Fetch/XHR, then press Update. Click the failed request to wp-json and open its Response tab. HTML means something intercepted the request. A Warning: line above the JSON means a plugin is printing before the response. An empty body means PHP died. Each of those points at a different fix.

What the response is not a valid JSON response actually means

The block editor does not post a form. It saves over the REST API: a POST to /wp-json/wp/v2/posts/123, sent through the @wordpress/api-fetch package, which parses the reply with response.json(). When that parse throws, api-fetch has no useful detail to show you, so it falls back to the generic string you are reading.

So the message is about the shape of the reply, not the content of your post. That distinction rules things out quickly. A permissions failure returns perfectly valid JSON with an error code in it, and the editor shows that message instead — “Sorry, you are not allowed to edit this post.” An expired login returns JSON too. Seeing the JSON message means the body was not JSON at all.

The same mechanism produces three wordings people report as separate bugs: Updating failed, Publishing failed, and the quieter autosave notice. Image uploads fail the same way, because media goes through wp/v2/media on the same route.

This message is on its way out

A Gutenberg pull request merged on 20 October 2026, in the 24.3 milestone and pinned to the wp/7.2 branch, replaces the generic text with the HTTP status: an HTML error page now reports Error code 502. instead, a REST error reports Error code 403. followed by the server’s own message, and a dropped connection says the autosave failed because there was no response from the server. On WordPress 7.1 you still have to read the response yourself, which is what the rest of this guide does.

Read the response before you change anything

The Network tab is the primary tool, because it shows the exact request that failed. Three shell commands fill in the rest. An unauthenticated GET to the REST index is public on a default install, so you can run these from your own machine:

# 1. Does the REST index answer at all, and with what content type?
curl -sS -o /dev/null -w '%{http_code} %{content_type}\n' \
  https://example.com/wp-json/

# 2. What is actually in the body? The first few hundred bytes decide it.
curl -sS https://example.com/wp-json/wp/v2/types | head -c 600

# 3. Same route, bypassing pretty permalinks entirely
curl -sS -o /dev/null -w '%{http_code} %{content_type}\n' \
  'https://example.com/?rest_route=/wp/v2/types'

Read the content type first. application/json; charset=UTF-8 means WordPress answered. text/html means something else did, and command two tells you what. If command one fails and command three succeeds, the route is not being handed to WordPress — a rewrite problem, not a WordPress problem. The rest_route form is not a hack, incidentally: the REST API handbook documents it as the supported way to call a route when pretty permalinks are off.

A clean curl does not clear the server

Saving is an authenticated POST carrying cookies, a nonce and your post body. A firewall rule that only inspects request bodies, or only trips on POST, will let all three commands above pass and still block the save. If curl looks healthy and the editor still fails, trust the Network tab over curl.

What came back, and where to fix it

What is in the responseWhat it isWhere the fix lives
HTML beginning <!DOCTYPE html>, showing your theme’s 404 pageWordPress received the request but has no route for that URIPermalinks and rewrite rules
<br /><b>Warning</b>: or Notice:, then valid-looking JSON after itSomething printed to the output buffer before the REST responseThe plugin or theme printing it, plus debug display
A branded “Access denied” or “Request blocked” page, status 403A web application firewall, mod_security, or a security pluginHost firewall rules or the plugin’s allowlist
A 502 or 504 gateway page from the hostPHP died or never answered: memory exhausted, or the request timed outMemory limit, execution time, or a slow hook on save
An nginx or Apache default 404, no WordPress stylingThe web server never passed the URI to index.phpServer config, not WordPress
Status 200 with a completely empty bodyA fatal error with display off, or output buffering that swallowed everythingdebug.log
A 301 or 302 to a different host or schemeSite address mismatch, an HTTPS or www redirect catching the APIsiteurl and home, and redirect rules
JSON containing "code":"rest_cookie_invalid_nonce"Not this error. The body is valid JSON; your login expiredReload the editor and sign in again
Match the response body to a row before you touch a setting. The last row is here because people arrive at it from the same search and it needs a different fix entirely.

The causes, in the order they actually turn up

Rewrite rules that no longer route /wp-json/. A migration, a restored .htaccess, or a host that rebuilt a vhost without the WordPress fallback. On Apache the .htaccess block that sends unmatched URIs to index.php is missing or has been overwritten; on nginx the location / block is missing try_files $uri $uri/ /index.php?$args;. The tell is that command three in the section above works and command one does not.
A plugin printing before the response. A PHP notice, a stray echo, a byte-order mark or a blank line after a closing ?> in functions.php. Any of them lands bytes in the buffer ahead of the JSON, and JSON that does not start at byte zero does not parse. A Gutenberg report that narrowed an invalid-JSON failure to a page builder followed exactly this shape: a Trying to access array offset on value of type bool notice from the plugin, visible in debug.log, with display errors still on.
A firewall treating your post content as an attack. Managed hosts run mod_security or an edge WAF, and block editor saves are large POST bodies full of angle brackets, <script> in a Custom HTML block, SQL-looking strings in a code sample. A rule fires, the request never reaches PHP, and the editor gets an HTML denial page. This is the cause most often misdiagnosed as a plugin conflict, because deactivating plugins changes nothing.
The save request running out of memory or time. Saving fires save_post, and everything hooked to it runs inside your request: related-post indexers, sitemap rebuilds, image optimisers, SEO analysis, cache purges. On a long post with many blocks that can cross a 60-second max_execution_time or a 256M limit, and you get a 502 or 504. The signature is that short posts save fine and one long one never does.
A site address or protocol mismatch. If siteurl says http:// and the host forces HTTPS, or says example.com while you are editing on www.example.com, the editor builds API URLs that redirect. Some redirects survive a POST and some do not, which is why this one presents intermittently and feels random.
A CDN or page cache touching /wp-json/. REST responses for logged-in editors must not be cached and POST requests must not be rewritten. An aggressive edge rule, an HTML minifier applied to API output, or a “cache everything” setting will corrupt the body or serve a stale page. Suspect this when the failure follows the CDN rather than the browser — same account, same post, works on a connection that bypasses the edge.

If the response body does not obviously match one of these, isolate in the ordinary way: deactivate plugins in halves, reload the editor and try a save between each step. Troubleshooting a WordPress plugin conflict step by step covers doing that on a live site, and disabling a plugin without dashboard access is the version for when the admin is unusable too.

Fixing each cause

Visiting Settings → Permalinks and pressing Save Changes flushes the rules and rewrites the WordPress block in .htaccess when the file is writable. From the shell:

wp rewrite structure '/%postname%/' --hard
wp rewrite flush --hard

# Confirm the API base the editor will use
wp eval 'echo rest_url() . PHP_EOL;'

If .htaccess is not writable, nothing is written and the setting appears to save anyway, so check the file afterwards. The same missing fallback produces front-end 404s on every post but the home page, which is why fixing 404 errors in WordPress and fixing this error are frequently the same job. On nginx, .htaccess is ignored entirely and the try_files directive is the thing to check.

Stray output ahead of the JSON

Two changes, in this order. First stop the warnings reaching the browser, which fixes the symptom immediately; then find what is emitting them. In wp-config.php, above the /* That's all, stop editing! */ line:

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

That sends notices to wp-content/debug.log instead of into your API responses. Save the post again, then read the tail of the log: whatever was printed into the response is now written there with a file and line number. Enabling WordPress debugging and reading error logs safely covers doing this on a production site without exposing paths to visitors.

A production site should never have display_errors on in the first place, so treat this as two findings: a plugin bug to report, and a server configuration to correct.

Firewall and mod_security rules

Confirm it before you ask anyone to change anything. Save a trimmed copy of the post with the suspicious content removed — delete the Custom HTML block, or the code sample containing a SELECT. If the trimmed version saves and the full one does not, a content-inspecting rule is firing.

Then take the failing request’s timestamp and response to your host and ask which rule matched. Managed hosts can read the audit log and whitelist a rule for authenticated POST requests to /wp-json/, which is a targeted change. Turning the firewall off site-wide is not; if a security plugin’s own firewall module is responsible, allowlist your own user or the REST route rather than disabling protection you still need.

Timeouts and memory on save

A 502 or 504 is a server-side death, not an editor problem. Raise the ceiling enough to confirm the diagnosis, then find what is spending the budget:

# What are the limits now?
wp eval 'echo ini_get("memory_limit"), " / ", ini_get("max_execution_time"), PHP_EOL;'

# Which plugins hook save_post, and in what order?
wp eval 'global $wp_filter; foreach ( $wp_filter["save_post"] as $priority => $hooks ) {
  foreach ( $hooks as $h ) {
    $cb = $h["function"];
    echo $priority, "  ", is_array( $cb )
      ? ( is_object( $cb[0] ) ? get_class( $cb[0] ) : $cb[0] ) . "::" . $cb[1]
      : ( is_string( $cb ) ? $cb : "closure" ), PHP_EOL;
  }
}'

That list is usually the answer on its own: a sitemap regenerator or an image optimiser running synchronously on every save is the common culprit, and most have a setting to defer the work. Fixing the allowed memory size exhausted error covers raising the limit properly, and diagnosing a slow WordPress admin dashboard covers finding the plugin that is costing you the seconds.

Site address and HTTPS

wp option get siteurl
wp option get home

Both should match the scheme and host you actually use in the browser, including whether www is present. Fixing a mismatch is not a one-line change — hard-coded URLs in content, in wp-config.php and in a CDN config all have to agree — so follow changing your WordPress site URL safely rather than editing the option and hoping.

Caching and the CDN

Exclude /wp-json/ from page caching and from HTML minification, and make sure “cache everything” rules do not cover it. Most caching plugins already bypass REST requests for logged-in users; edge rules written by hand often do not. Test by loading the editor over a connection that bypasses the CDN — if the save succeeds there, you have your answer without changing a single plugin setting.

What Site Health does and does not tell you

Tools → Site Health → Status runs a REST availability check, and its exact wording distinguishes three different failures. Core sends a GET to wp/v2/types/post with context=edit, an X-WP-Nonce header and a ten-second timeout, then checks for a 200 whose JSON contains a capabilities key:

“The REST API encountered an error” — the request itself failed and core reports the error code and message with it. This is the critical one, and the message underneath is the detail worth reading: a cURL timeout points at the loopback, a DNS failure at the site address.
“The REST API encountered an unexpected result” — a response arrived with a status other than 200, and the status code is printed with it. A 403 here is a firewall; a 404 is routing.
“The REST API did not behave correctly” — a 200 came back but the context parameter was not processed, so the body is missing capabilities. Something is filtering or rewriting REST output rather than blocking it.

Site Health can disagree with your browser, legitimately

The test is a loopback request: the server asking itself. It can fail on internal DNS, HTTP authentication on a staging site, or an outbound firewall while your browser saves posts perfectly well. It can also pass while your browser is blocked, because an edge WAF never sees a request that never leaves the server. A green REST check does not clear the REST API, and a red one does not prove the editor is broken.

The rest of that screen is still worth a look while you are there — finding and fixing common problems with WordPress Site Health goes through the checks that matter and the ones you can ignore.

Getting the post out while you debug

Your content is still in the browser, and nothing has been lost. Before you reload anything, open Options → Code editor (Ctrl+Shift+Alt+M), select everything and paste it into a local file. That is the whole post as block markup, and it will restore cleanly into any editor later.

Then stop pressing Update. Repeated failed attempts queue autosaves and can leave a post lock behind, which turns a save problem into an “another user is editing this post” problem on top of it.

If you must publish before the cause is found, the Classic Editor plugin saves through post.php rather than the REST API, so it can get a post out while /wp-json/ is unreachable. Treat it as a bypass and not a fix: the API is still broken for media uploads, the Site Editor and anything else built on it. A smaller version of the same trick is to split a long post — if a firewall is objecting to one block, publishing without it and adding it afterwards tells you which block is the problem.

Do not paste a stale copy over a good one

If some of your edits did save before the failure, the version in the database may be newer than the copy in a tab you left open. Check the revision history before pasting a rescued copy over the current content, and compare the two rather than assuming. Restoring an earlier version of a post using revisions covers reading that diff.

Keeping it from coming back

Keep display_errors off in production. This single setting converts every third-party PHP notice from a logged nuisance into a broken save. It is the cheapest fix on this page and it prevents a whole category of the error.
Save a real post after every migration. Front-end checks pass with broken rewrite rules more often than you would expect, because the home page and cached pages still render. Open an existing post, make a trivial edit, press Update. Testing your site after a major update covers building that into a routine.
Allowlist /wp-json/ deliberately, once. If your host runs a WAF, get authenticated POST requests to the REST API excluded from content-inspection rules as a standing configuration rather than re-raising a ticket every time an editor pastes a code sample.
Watch what you hook to save_post. Anything that rebuilds an index, resizes an image or calls an external API belongs in a scheduled job, not in the request that is holding your editor open. The same discipline keeps publish times predictable as the site grows.
Test plugin updates in the editor, not only on the front end. A plugin that breaks REST output breaks nothing a visitor can see, so a front-end smoke test misses it entirely. Updating plugins safely covers the staging habit this depends on.

Frequently asked questions

Will I lose the post I was editing?

Not if you copy it out before reloading. Open Options → Code editor, select everything and paste it into a text file. The block markup you get is the complete post and restores cleanly later. Reloading the tab without doing that is what loses work, because the failed save means nothing was written.

Why does it only happen on one particular post?

Because the cause depends on the request, not the site. A firewall rule inspecting the request body fires on the post containing a script tag or a SQL sample. A timeout fires on the longest post. Both look site-wide until you notice the short ones still save. Try saving a new empty draft: if that works, the content of the failing post is part of the cause.

Is resaving permalinks really a fix, or just folklore?

It is a real fix for exactly one cause: rewrite rules that no longer route /wp-json/, which is common after a migration. It does nothing for a firewall, a timeout or a plugin printing warnings. It is harmless to try, which is why it tops every list, but if it does not work the first time it will not work the fifth.

Should I disable the REST API to stop this happening?

No, and doing so causes this error rather than preventing it. The block editor, the Site Editor, media uploads and Site Health all run on the REST API. Plugins that “disable” it generally restrict unauthenticated access, which is a reasonable hardening choice; one that blocks authenticated requests breaks editing entirely. If you installed something like that recently, it is the first thing to test.

How is this different from a block showing invalid content?

They are unrelated. Invalid content is a client-side check comparing stored markup with a block’s current save output, and it never involves the network. This error is the network request failing. Fixing a block that contains unexpected or invalid content covers the other one. Seeing both at once usually means a plugin update changed block markup and broke REST output in the same release.

The response is not a valid JSON response only when I upload images. Why?

Uploads hit wp/v2/media with a much larger body and do real work on arrival: generating sizes, running an optimiser, sometimes shipping the file to object storage. That makes them the first requests to hit a post_max_size or upload_max_filesize ceiling, a memory limit or a timeout, each of which returns an HTML error page rather than JSON. Try a 50 KB image: if that uploads and a 4 MB one does not, it is a size or resource limit, not the API.

Does this error affect visitors?

Often not, which is why it can go unnoticed for weeks. But the underlying cause sometimes does: broken rewrite rules also 404 your posts, and a save-path timeout usually means slow pages too. Check the front end once rather than assuming the problem is confined to the admin.

Sources

  • REST API Key Concepts in the WordPress developer handbook — the /wp-json/ route structure and the documented ?rest_route= form for sites without pretty permalinks
  • WP_Site_Health::get_test_rest_availability() — the request Site Health makes, the capabilities check, and the three result labels quoted above
  • Gutenberg pull request 85157 — the merged change that replaces this message with the HTTP status, in the 24.3 milestone and pinned to the wp/7.2 branch
  • Gutenberg issue 61671 — a real report narrowed to a page builder, with the PHP notice in debug.log that matches the stray-output pattern
  • Core ticket 47416 — Site Health reporting a REST API error from a loopback cURL timeout rather than a genuine API fault

Leave a Reply to This Post