"The response is not a valid JSON response" means the block editor sent a request to the REST API, the server answered, and the body of that answer could not be parsed as JSON. Something other than WordPress's REST server wrote that body, or wrote something in front of it, and the body itself tells you what.
So the fix is not a checklist. Open the failed request, read the first few hundred bytes, and the cause is usually printed right there: a firewall's block page, a PHP warning, your theme's 404 template, or a login form.
We read the guides that rank for this error before writing. WPShout (2024-01-02, about 2,000 words), Hostinger (updated 2026-09-15, about 2,000) and BlogVault (updated 2025-02-27, about 1,800) all lead with re-saving permalinks, checking the site URL and deactivating plugins.
MalCare's guide (updated 2026-07-16) is the best of them and does tell you to open the Network panel. None of the four explains where the sentence comes from, why a PHP fatal usually does not produce it, why Site Health can say the REST API is fine while saving fails, or that with one common cause your post saved anyway.
Where the sentence comes from
The editor talks to WordPress through a small library called api-fetch, which ships in wp-includes/js/dist/api-fetch.js. In WordPress 7.1.4 it reads every response with response.json(), and if that throws, it replaces the error with its own:
throw {
code: "invalid_json",
message: __("The response is not a valid JSON response.")
};
It does this for successful responses and for failed ones alike. A 403 page from a firewall, a 404 page from your theme and a 200 with a PHP warning in front of the JSON all end up as the same sentence.
The editor then prefixes its own words. Click Publish on a draft and you see "Publishing failed." in front of it; save anything else and you see "Updating failed.", which is why people search for both phrasings (verified in wp-includes/js/dist/editor.js, 7.1.4).
Three errors that look alike and are not
api-fetch has three separate failure messages, and telling them apart halves the search:
| Message after "Updating failed." | What happened | Where to look |
|---|---|---|
| The response is not a valid JSON response. | The server answered, and the body was not JSON | The response body |
| Could not get a valid response from the server. | No usable response at all: connection reset, blocked cross-origin request, TLS failure | The browser console and the request's status |
| Updating failed because you were offline. | navigator.onLine was false |
Your connection |
There is a fourth case worth knowing, because it rules out a cause. When a plugin throws a PHP fatal error during a save, WordPress's fatal error handler sees the Accept: application/json header the editor sends and answers with a JSON error and status 500.
That message contains HTML (a link to the troubleshooting docs), and the 7.1 editor drops server messages that contain tags. So a plugin fatal during a save shows "Updating failed." on its own, with no JSON sentence after it.
If you see the JSON sentence, something other than WordPress's error handling wrote the body. The rest of this post is about finding out what.
Read the response: the Network panel in two minutes
- Open the post, then open developer tools (F12) and the Network tab.
- Filter by Fetch/XHR and click Update in the editor.
- Find the request to
/wp-json/wp/v2/posts/<id>(orindex.php?rest_route=/wp/v2/posts/<id>with plain permalinks). It shows a red status or a status you did not expect. - Open Response and read the first lines. Note the status code.
One detail confuses people here. Updating an existing post is a PUT, but api-fetch sends it as a POST with an X-HTTP-Method-Override: PUT header, so that hosts which block PUT still work (httpV1Middleware, which does this for PATCH, PUT and DELETE).
If you want the raw bytes, right-click the request, choose Copy as cURL, and run it with the output sent to a file:
# paste the copied command, then add these two options at the end
... -o body.txt -w '%{http_code}\n'
head -c 400 body.txt
That copied command contains your login cookie and a REST nonce. Treat it like a password: do not paste it into a support ticket or a chat.
What the first bytes tell you
| Status | Body starts with | Cause | Fix |
|---|---|---|---|
| 200 | <br /> then <b>Warning</b>:, or stray text, then {"id": |
Output was printed before the JSON | Find the file that printed it |
| 403 | An HTML block page, often with the host's, Cloudflare's or a security plugin's branding | A firewall rule matched the request | Find the rule in that layer's log and allow the request |
| 404 | Your theme's 404 page | Requests to /wp-json/ are not reaching WordPress |
Re-save permalinks; on nginx, check try_files |
| 200 | The wp-login.php form or your home page |
A redirect sent the request somewhere else | Compare home and siteurl, then your redirect rules |
| 413 | An nginx or Apache "Request Entity Too Large" page | Upload larger than the web server allows | Raise the web server limit, not only PHP's |
| 200 or 500 | Nothing at all | Something called exit or the request died with no output |
debug.log |
Something printed in front of the JSON: your post probably saved
This is the case the guides miss, and it changes what you do next.
A status of 200 with {"id":123,... after the stray text means the REST server ran the update to the end. The editor just could not read the answer.
Reload the post before you try again. Very often your changes are already there, and repeated clicking only adds revisions.
Most guides blame this on WP_DEBUG showing PHP warnings. That is usually wrong: wp_debug_mode() in wp-includes/load.php sets display_errors to 0 for any request where wp_is_json_request() is true, and the editor's Accept: application/json header makes every save one of those (verified in 7.1.4).
So a <b>Warning</b> in a REST response came from somewhere WordPress could not reach:
- Before WordPress switched display off.
wp-config.phpruns beforewp_debug_mode(). A constant defined twice there prints "Constant WP_DEBUG already defined" on PHP 8 if the server's ownphp.inihasdisplay_errorson. - After, because something switched it back on. A plugin or theme calling
ini_set( 'display_errors', 1 ). - Not an error at all. A leftover
echo,print_r()orvar_dump(), or text outside the<?phptags of a file that loads on every request.
The text itself is the clue. A PHP warning names its file and line, and echoed output can be found with a grep across wp-content for the string you see.
debug.log will not show echoed output, because it is not an error. It will show warnings, which is why debug mode on a SandyWP sandbox puts debug.log next to the toggles: reproduce the save, then read what was logged during it.
A 403 that depends on what you wrote
If one post fails and others save, the firewall is probably reacting to the content. Web application firewalls (ModSecurity rules on shared hosts, Cloudflare's managed rules, security plugins) inspect the request body, and a post body is a large blob of HTML that can contain anything.
A pasted <script> or <iframe> embed, a code sample containing SQL, or a block of shell commands can all look like an attack. To find the trigger, copy half the post's blocks into a new draft and save it, then halve again until one block is left.
Once you know the block, ask the host to allowlist that rule for /wp-json/wp/v2/ requests from logged-in users, rather than switching the firewall off. MalCare makes the same recommendation.
Why Site Health says the REST API is fine
Site Health's "The REST API is available" test (get_test_rest_availability() in class-wp-site-health.php) makes one request from the server to itself: a GET to /wp/v2/types/post?context=edit, with your cookies and a 10 second timeout.
That request has no body, goes from the server to itself, and is a GET. The failing save is a POST from your browser, through any CDN in front of the site, carrying the whole post.
A firewall rule on request bodies, a CDN rule on POST, or a block on the X-HTTP-Method-Override header can break saving while the Site Health test stays green.
A green Site Health result rules out rewrite problems and a REST API disabled in PHP. It does not rule out anything between the browser and the server.
More on what the test does and does not check is in our Site Health post.
Is it the server or the code? Test it on a copy
This is the question every guide answers with "deactivate your plugins one by one", on the live site, while the editor is broken for everyone. There is a faster split.
Clone the site and save the same post on the copy. The copy has the same database, plugins, theme and post content, but a different server and none of the production firewall or CDN in front of it.
- It saves on the copy. The problem is in the layer that did not come with the copy: the host's firewall, the CDN, server rewrite rules, or a redirect at the edge. Take the 403 or the redirect from the production Network panel to the host.
- It fails on the copy too. The problem travelled with the site: a plugin printing notices, a plugin hooking
rest_pre_dispatchorrest_authentication_errors, or something in the post content that a plugin chokes on. Now the one-by-one plugin test is safe, because nobody else is using the copy.
With SandyWP Cloner, the copy comes from a plugin that only reads the live site, and the sandbox gets its own URL with URLs rewritten. WP-CLI is installed on every sandbox, so the plugin bisection can be done over SSH in a few commands:
wp plugin list --status=active --field=name
wp plugin deactivate --all
wp plugin activate classic-editor # or whichever half you are testing
There is an honest limit. A copy on another server cannot reproduce a production firewall, and it cannot tell you which of the host's rules fired.
It tells you which side the fault is on, which is the question that decides whether you open a ticket with the host or start reading plugin code.
Fixes that do not fix anything
Several popular steps are workarounds, and it helps to know what they hide.
Installing the Classic Editor. The classic editor saves through a normal form post to post.php, not the REST API, so it avoids the broken path. That is fine for publishing today, but every plugin that uses the REST API from the admin is still broken.
Uploading through Media, Add New instead of the editor. The Media screen uploads through async-upload.php, again bypassing the REST route. If the cause is a 413, the larger file will still fail there; see raising the upload limit, including the web server limit that PHP settings do not change.
Re-saving permalinks. This flushes rewrite rules and rewrites .htaccess on Apache, which fixes the 404 case and nothing else. If the failing request returned a 403 or a 200 with a warning, re-saving changes nothing.
Our posts on permalinks and the default .htaccess cover that case properly.
What changes in the next version
The editor in Gutenberg trunk (checked on 2026-10-30) rewrites the save failure notice to "Updating failed. Keep this page open and try updating again."
In the same change, invalid_json is listed as a client-generated error whose message is no longer shown. Instead, the notice gets details such as "Error code 403." and, for a fatal, the PHP error message.
That is unreleased and could change before it reaches a WordPress release. Until it does, the status code is in the Network panel, not on the screen.
The short version
- The sentence means the server answered with something that is not JSON. Read that answer before changing anything.
- A 200 with a warning or stray text in front: your post probably saved. The text names the file that printed it.
- A 403 on one post: a firewall rule matched the content. Halve the post to find the block.
- A 404: rewrite rules. Re-save permalinks, or check nginx.
- Site Health being green does not clear the browser-to-server path.
- Not sure which side it is on? Save the same post on a copy of the site. If it works there, take it to the host.
