Your REST API response starts with “Notice: Undefined index…” or “Warning: Cannot modify header information…” instead of valid JSON. Your client throws a parse error. The block editor says “The response is not a valid JSON response.” Your integration script fails silently. Something is printing to stdout before WordPress sends the JSON body, and you need to find and stop it right now.
What Does PHP Output Before the JSON Body Mean?
PHP output before the JSON body means a plugin, theme, or server configuration is writing text to the output buffer before WP_REST_Server::serve_request() calls echo $result to send the JSON. That text, whether a PHP notice, a warning, an HTML fragment, or invisible whitespace, gets prepended to the response. The client receives Notice: Undefined variable...{"id":1,...} instead of {"id":1,...}, and JSON parsing fails at the first character.
The most common cause is WP_DEBUG_DISPLAY set to true (or left undefined, which defaults to true) in wp-config.php on a site with WP_DEBUG enabled. WordPress does attempt to disable display_errors for REST and JSON requests through the wp_debug_mode() function in wp-includes/load.php. The relevant code checks for REST_REQUEST, XMLRPC_REQUEST, wp_doing_ajax(), and wp_is_json_request(), and if any match, calls ini_set( 'display_errors', 0 ). But this protection has a timing gap that the WordPress source code itself acknowledges: the comment reads “The ‘REST_REQUEST’ check here is optimistic as the constant is most likely not set at this point even if it is in fact a REST request.” The REST_REQUEST constant is defined later, during parse_request, well after wp_debug_mode() has already run. The fallback check, wp_is_json_request(), inspects the Accept and Content-Type headers for the string “json.” If your client does not send Accept: application/json, the protection does not trigger, and PHP notices print straight into the response body.
If you are also debugging broader connectivity issues, our REST API disabled troubleshooting guide covers the server-level causes that produce 404, 401, and 403 responses. This guide focuses on a different failure: the REST API is reachable and returns HTTP 200, but the response body is not valid JSON because something printed to stdout first.
How to Reproduce the Error With curl

curl shows you the raw response, including any text before the JSON body. Browser dev tools and most HTTP client libraries try to parse the response as JSON and throw a generic parse error without showing you what preceded the JSON. Run this command:
curl -i https://example.com/wp-json/wp/v2/posts
The -i flag includes HTTP headers in the output. Look at the first line after the blank line that separates headers from the body. If the response body starts with anything other than [ or {, you have output before the JSON.
A healthy response looks like this:
HTTP/2 200
content-type: application/json; charset=UTF-8
[{"id":1,"date":"2026-01-15T10:00:00",...}]
A corrupted response looks like this:
HTTP/2 200
content-type: application/json; charset=UTF-8
Notice: Undefined index: custom_key in /var/www/html/wp-content/plugins/bad-plugin/bad-plugin.php on line 42
[{"id":1,"date":"2026-01-15T10:00:00",...}]
Or with HTML formatting (when display_errors includes HTML, which is the PHP default):
<br />
<b>Notice</b>: Undefined index: custom_key in <b>/var/www/html/wp-content/plugins/bad-plugin/bad-plugin.php</b> on line <b>42</b><br />
[{"id":1,"date":"2026-01-15T10:00:00",...}]
The text before the [ tells you the file path and line number of the offending output. That is your starting point. If you see a file path, skip to the section on finding the offending plugin or theme. If you see no file path, only whitespace or invisible characters, skip to Cause 3 (BOM or whitespace before the opening PHP tag).
For authenticated requests, add credentials:
curl -i -u "username:app_password" https://example.com/wp-json/wp/v2/posts
To confirm whether the wp_is_json_request() protection gap is involved, send a request with an explicit Accept header and one without:
curl -i -H "Accept: application/json" https://example.com/wp-json/wp/v2/posts
curl -i -H "Accept: text/html" https://example.com/wp-json/wp/v2/posts
If the notice appears with Accept: text/html but disappears with Accept: application/json, the issue is that wp_is_json_request() did not detect the first request as JSON, so the display_errors = 0 line never executed. This is common with custom integration scripts that do not set a proper Accept header. If you are troubleshooting authentication separately, see our guide on WordPress application passwords not working for the auth-layer diagnostic chain.
Which Cause Matches Your Response?
Read the text before the JSON body. The format of that text tells you which cause is responsible and where to jump in this guide.
| Symptom in Raw Response | Likely Cause | Jump To |
|---|---|---|
| Starts with “Notice:”, “Warning:”, or “Deprecated:” | WP_DEBUG_DISPLAY true or server display_errors on | Cause 1 or 4 |
| Starts with HTML, plain text, or debug output (no PHP error format) | Plugin or theme echoing on init or another hook | Cause 2 |
| Starts with whitespace, blank lines, or invisible characters | BOM or whitespace before opening PHP tag | Cause 3 |
| Notice appears without Accept: application/json but not with it | wp_is_json_request() not detecting the request | Cause 1 |
| Notice appears even with WP_DEBUG_DISPLAY false | Plugin re-enabling display_errors or server override | Cause 4 or 5 |
| Works on one environment, fails on another | Different php.ini display_errors setting between environments | Cause 4 |
How to Fix Each Cause
Cause 1: WP_DEBUG_DISPLAY Is True in wp-config.php
This is the single most common cause. Open wp-config.php and look for the debug constants. If WP_DEBUG is true and WP_DEBUG_DISPLAY is either set to true or not defined at all (it defaults to true per wp-includes/default-constants.php), PHP notices and warnings are printed to the output buffer. On a REST API request where the wp_is_json_request() check does not fire, those notices land in the response body before the JSON.
The fix is to set WP_DEBUG_DISPLAY to false while keeping WP_DEBUG and WP_DEBUG_LOG true so errors are still logged to wp-content/debug.log but not printed to the screen. The WordPress debugging documentation recommends exactly this configuration. Add or modify these lines in wp-config.php, above the /* That's all, stop editing! */ comment:
define( 'WP_DEBUG', true );
define( 'WP_DEBUG_LOG', true );
define( 'WP_DEBUG_DISPLAY', false );
@ini_set( 'display_errors', 0 );
The @ini_set( 'display_errors', 0 ) line is belt-and-suspenders. WordPress’s wp_debug_mode() calls ini_set( 'display_errors', 0 ) when WP_DEBUG_DISPLAY is false, but the explicit call in wp-config.php ensures the setting is applied before any plugin or theme code runs. The @ suppresses the warning that some hosts throw when ini_set is disabled.
After making this change, retest with curl. If the notice is gone, this was the cause. Check wp-content/debug.log to see the notices that were previously being printed to the screen. Fix the underlying PHP notice in the plugin or theme that generated it, but the immediate integration blocker is resolved.
Important: if you set WP_DEBUG to false entirely, WP_DEBUG_DISPLAY and WP_DEBUG_LOG stop doing anything. The wp_debug_mode() function only applies the display and log settings when WP_DEBUG is true. If WP_DEBUG is false, WordPress falls back to a reduced error_reporting level that excludes notices, and display_errors is not explicitly set by WordPress at all. In that case, the server’s php.ini setting for display_errors takes over. See Cause 4 if you have WP_DEBUG set to false and are still seeing notices.
Cause 2: A Plugin or Theme Echoes Output on init
Some plugins and themes call echo, print, var_dump, print_r, or var_export during the WordPress loading sequence, typically on the init hook or earlier on plugins_loaded. This output goes to stdout before the REST API sends the JSON body. Unlike PHP notices, this output is not controlled by display_errors. Setting WP_DEBUG_DISPLAY to false will not stop it because the plugin is explicitly printing, not triggering a PHP error.
The symptom is distinctive: the response body starts with HTML, plain text, or debug output that does not follow the PHP notice format. You might see a stray <div>, a tracking pixel, a debug string like Plugin loaded successfully, or output from a forgotten var_dump() left in production code.
To confirm: the curl response shows non-JSON text before the body, and the text does not start with “Notice:”, “Warning:”, or “Deprecated:”. Search your active plugins and theme for direct output calls:
grep -rn "echo|print|var_dump|print_r|var_export" wp-content/plugins/ wp-content/themes/
This will return a lot of results because echo and print are used legitimately in template files. Filter for calls that are not inside a function body or that are in files loaded during the init phase. The most common offenders are plugins that echo tracking codes, social sharing buttons, or debug output on init or wp_head without checking whether the current request is a REST request.
The fix: deactivate plugins one by one and retest with curl after each deactivation. When the output disappears, the last-deactivated plugin is the source. Then either fix the plugin code (wrap the echo in a ! defined( 'REST_REQUEST' ) || ! REST_REQUEST check) or contact the plugin author. A correctly written plugin should not echo output during init without checking for REST context:
add_action( 'init', function() {
if ( defined( 'REST_REQUEST' ) && REST_REQUEST ) {
return;
}
// Output that should only appear on front-end page loads
echo '<!-- analytics tag -->';
} );
If the plugin is one you cannot modify (a third-party plugin from the repository), check for an update. Many plugins fixed this exact issue after the block editor introduced stricter JSON response validation. If no fix is available, a temporary workaround is to add an output buffer in a must-use plugin that strips non-JSON output from REST responses, but this is a bandage, not a fix. The real fix is in the plugin code.
Cause 3: Whitespace or BOM Before the Opening PHP Tag
If the curl response shows blank lines, whitespace, or invisible characters before the JSON body, the cause is a PHP file with whitespace or a Byte Order Mark (BOM) before the opening <?php tag. PHP sends anything outside of <?php ... ?> tags directly to the output buffer. A single space or newline before <?php in any loaded plugin or theme file becomes output that precedes the JSON.
The symptom: the response body starts with one or more blank lines, spaces, or the characters xefxbbxbf (the UTF-8 BOM). In curl, this looks like an empty line before the [ or {. Your JSON parser fails because the response does not start with a valid JSON character.
To find the offending file, use WP-CLI to scan plugin and theme files for whitespace or BOM before the opening PHP tag:
find wp-content/plugins wp-content/themes -name "*.php" -exec grep -Pl "^s+$|^[x{FEFF}]" {} ;
Or check for BOM specifically:
grep -rl $'xefxbbxbf' wp-content/plugins/ wp-content/themes/
The fix: open each file found by the scan in a text editor, remove any whitespace or BOM characters before the <?php tag, and save. The opening <?php must be the very first characters in the file, with no spaces, newlines, or BOM preceding it. If your text editor saved the file as UTF-8 with BOM, re-save it as UTF-8 without BOM.
This cause is harder to diagnose than the others because the output is invisible. If you suspect whitespace but the grep commands return nothing, pipe the curl output through xxd to see the raw bytes:
curl -s https://example.com/wp-json/wp/v2/posts | head -c 100 | xxd
If the first bytes are 20 (space), 0a (newline), 0d (carriage return), or ef bb bf (UTF-8 BOM), you have whitespace or BOM output. The byte offset tells you how many bytes of garbage precede the JSON.
Cause 4: Server-Level display_errors Overrides WordPress

If you have set WP_DEBUG_DISPLAY to false and even set WP_DEBUG to false entirely, but PHP notices still appear in the REST API response, the server’s php.ini or server-level configuration is overriding WordPress’s ini_set calls. This happens on hosts that disable ini_set for certain PHP settings or that set display_errors = On in a configuration file that loads after WordPress’s settings.
Check the current PHP display_errors value from within WordPress. Add this to a must-use plugin or functions.php temporarily:
add_action( 'init', function() {
error_log( 'display_errors: ' . ini_get( 'display_errors' ) );
error_log( 'error_reporting: ' . ini_get( 'error_reporting' ) );
} );
Check wp-content/debug.log after loading a page. If display_errors is 1 or On despite WordPress setting it to 0, the server is overriding the ini_set call.
The fix depends on your access level. If you have server access, edit php.ini directly:
display_errors = Off
error_reporting = E_ALL & ~E_NOTICE & ~E_DEPRECATED
log_errors = On
If you do not have access to php.ini, try adding the directive to .htaccess (Apache only):
php_value display_errors Off
php_flag display_errors Off
If neither works, your host is blocking runtime PHP configuration changes. Contact the hosting provider and ask them to set display_errors to Off for your account. If you are on a managed WordPress host, this is often a setting in their control panel.
Another variant: some hosts set display_errors in a per-directory php.ini or .user.ini file that takes precedence over WordPress’s ini_set calls. Check for a .user.ini file in your site root:
cat .user.ini
If it contains display_errors = On or display_errors = 1, change it to Off or remove the line entirely.
Cause 5: A Plugin Re-enables display_errors After WordPress Disables It
Some plugins, particularly debugging and development tools, call ini_set( 'display_errors', 1 ) or error_reporting( E_ALL ) during their initialization. If this happens after WordPress’s wp_debug_mode() has already set display_errors to 0, the plugin’s call overrides the WordPress setting, and PHP notices start printing to the output buffer again.
The symptom: you have WP_DEBUG_DISPLAY set to false and have confirmed that the server is not overriding it, but notices still appear. The notices start appearing after activating a specific plugin, often a development, debugging, or logging tool.
To confirm, search the codebase for plugins that modify error display settings:
grep -rn "display_errors|error_reporting" wp-content/plugins/ wp-content/mu-plugins/
Look for ini_set( 'display_errors', 1 ) or ini_set( 'display_errors', 'On' ) or ini_set( 'display_errors', 'on' ). If you find a match in an active plugin, that plugin is re-enabling error display after WordPress disabled it.
The fix: deactivate the plugin, or if you need the plugin, add a must-use plugin that runs after all plugins have loaded and re-sets display_errors to 0. Use the plugins_loaded hook with a high priority to ensure it runs after the offending plugin:
// wp-content/mu-plugins/force-display-errors-off.php
add_action( 'plugins_loaded', function() {
@ini_set( 'display_errors', 0 );
}, 999 );
This runs after all plugins have loaded (priority 999 ensures it is one of the last callbacks on plugins_loaded) and re-disables display_errors. It is a workaround, not a fix. The real fix is in the plugin that is re-enabling display_errors, but this workaround keeps your REST API functional until the plugin is updated.
How to Find Which Plugin or Theme Is Generating the Output

If the curl response shows a file path in the PHP notice, you already know the source. If it shows only output without a file path, or if the output is from Cause 2 (explicit echo), you need to isolate the source. Three methods, in order of speed:
Method 1: Binary Deactivation
Deactivate half of your plugins at once and retest with curl. If the output disappears, the source is in the deactivated half. Reactivate half of the deactivated half and retest. Repeat until you narrow it down to a single plugin. This is the fastest method when you have many plugins. Use WP-CLI to speed it up:
# Deactivate all plugins
wp plugin deactivate --all
# Test
curl -i https://example.com/wp-json/wp/v2/posts
# If clean, reactivate half and retest
wp plugin activate plugin-one plugin-two plugin-three
curl -i https://example.com/wp-json/wp/v2/posts
Method 2: Grep for Output Calls
Search all plugin and theme files for functions that produce output:
grep -rn "echo|print|var_dump|print_r|var_export|printf" wp-content/plugins/ wp-content/themes/ --include="*.php"
Filter the results for calls that are not inside function definitions (top-level echo statements) and calls on init, plugins_loaded, or wp_loaded hooks. These are the ones most likely to fire during REST API requests.
Method 3: Query Monitor Plugin
Install the free Query Monitor plugin. It captures PHP notices, warnings, and deprecated function calls, and shows the exact file path, line number, and hook that triggered them. After activating Query Monitor, load the REST API endpoint in the browser admin bar. Click the Query Monitor admin bar item and navigate to the PHP Errors panel. The component column shows which plugin or theme generated the notice.
Query Monitor also adds an X-QM-php-errors-error-N HTTP response header for each PHP error, which you can see in the curl response headers with curl -i. This is useful when you cannot load the browser-based Query Monitor panel, such as when debugging a REST API endpoint that returns invalid JSON.
How to Confirm the Fix Is Working
After applying the fix, run the same curl command you used to reproduce the error:
curl -i https://example.com/wp-json/wp/v2/posts
The response body must start with [ or { with no text, whitespace, or blank lines before it. Verify with xxd to check for invisible characters:
curl -s https://example.com/wp-json/wp/v2/posts | head -c 20 | xxd
The first byte should be 5b (ASCII [) for a list response or 7b (ASCII {) for a single-object response. If you see any other bytes first, there is still output before the JSON.
For authenticated requests, test with credentials:
curl -i -u "username:app_password" -H "Content-Type: application/json" -X POST -d '{"title":"Test Post","status":"draft"}' https://example.com/wp-json/wp/v2/posts
If this returns HTTP 201 with a clean JSON body, the full pipeline is working: routing, authentication, JSON integrity. If the block editor was throwing “The response is not a valid JSON response,” try saving a post. If the save succeeds, the fix is confirmed.
If you are using a content automation tool like ClearPost to publish via the REST API, this error blocks the publishing pipeline until the WordPress side is fixed. The plugin cannot work around corrupted JSON responses because the corruption happens before the JSON body reaches any HTTP client. The fix is always on the WordPress site, not in the publishing tool.
Where These Fixes Do NOT Apply
These fixes address exactly one problem: text appearing in the output buffer before the REST API sends its JSON response. They do not apply to the following situations:
- JSON parse errors caused by a malformed request body, not a malformed response. If your curl request sends invalid JSON in the POST body, WordPress returns a 400 error with a valid JSON error object. That is a client-side issue, not an output-before-JSON issue.
- REST API returning 404, 401, or 403 status codes. Those are routing, authentication, or permission failures, not JSON corruption. Our REST API disabled guide covers those causes.
- CORS errors in the browser. If curl returns clean JSON but the browser throws a CORS error, the issue is browser-side header enforcement, not output corruption. Our CORS errors guide covers that layer.
- SSL or mixed-content errors. If the REST API is unreachable because the site is not on HTTPS, or if
is_ssl()returns false behind a reverse proxy, see our is_ssl() fix guide. - PHP fatal errors that produce a 500 response with an HTML error page. A fatal error kills the process before the JSON is generated, so there is no JSON body at all. The response is a full HTML error page, not JSON with text prepended. The fix is resolving the fatal error, not suppressing output.
Frequently Asked Questions
What does PHP output before the JSON body mean?
It means a plugin, theme, or server configuration is writing text to the PHP output buffer before WP_REST_Server::serve_request() sends the JSON response. That text gets prepended to the JSON body, making the response invalid JSON. The client receives something like Notice: ...{"id":1} instead of {"id":1}, and parsing fails at the first character.
How do I reproduce a PHP notice before JSON error with curl?
Run curl -i https://example.com/wp-json/wp/v2/posts. The -i flag shows HTTP headers and the raw response body. If the body starts with anything other than [ or {, there is output before the JSON. Use curl -s ... | head -c 100 | xxd to inspect invisible characters like whitespace or BOM.
Why does WP_DEBUG_DISPLAY cause PHP notices in REST API responses?
When WP_DEBUG is true and WP_DEBUG_DISPLAY is true (or undefined, which defaults to true), WordPress sets display_errors to 1. WordPress tries to disable display_errors for REST requests in wp_debug_mode(), but the REST_REQUEST constant is not defined yet when that function runs. The fallback check, wp_is_json_request(), depends on the client sending an Accept: application/json header. If that header is missing, the protection does not trigger and notices print into the response body. Setting WP_DEBUG_DISPLAY to false and adding @ini_set( 'display_errors', 0 ) in wp-config.php fixes this.
How do I find which plugin or theme is generating output before the JSON body?
Three methods: binary deactivation (deactivate half your plugins, retest with curl, narrow down), grep for output calls (grep -rn "echo|print|var_dump" wp-content/plugins/), or install the Query Monitor plugin which shows the exact file, line number, and hook for every PHP notice. The curl response itself often includes the file path and line number in the PHP notice text.
Where do these fixes not apply?
These fixes do not apply to JSON parse errors caused by a malformed request body (client-side issue), 404/401/403 status codes (routing, auth, or permission failures), CORS errors in the browser (header enforcement, not output corruption), SSL or mixed-content errors, or PHP fatal errors that produce a 500 response with an HTML error page instead of a JSON body with text prepended.
Try ClearPost free for 7 days. AI does the heavy lifting, you approve every post before it goes live. No long onboarding, no agency overhead, cancel anytime. See what 30 SEO-optimized posts a month looks like compared to the 4 you are getting now.
Frequently Asked Questions
What does PHP output before the JSON body mean?
It means a plugin, theme, or server configuration is writing text to the PHP output buffer before WP_REST_Server::serve_request() sends the JSON response. That text gets prepended to the JSON body, making the response invalid JSON. The client receives something like ‘Notice: …{“id”:1}’ instead of ‘{“id”:1}’, and parsing fails at the first character.
How do I reproduce a PHP notice before JSON error with curl?
Run ‘curl -i https://example.com/wp-json/wp/v2/posts’. The -i flag shows HTTP headers and the raw response body. If the body starts with anything other than [ or {, there is output before the JSON. Use ‘curl -s … | head -c 100 | xxd’ to inspect invisible characters like whitespace or BOM.
Why does WP_DEBUG_DISPLAY cause PHP notices in REST API responses?
When WP_DEBUG is true and WP_DEBUG_DISPLAY is true (or undefined, which defaults to true), WordPress sets display_errors to 1. WordPress tries to disable display_errors for REST requests in wp_debug_mode(), but the REST_REQUEST constant is not defined yet when that function runs. The fallback check, wp_is_json_request(), depends on the client sending an Accept: application/json header. If that header is missing, the protection does not trigger and notices print into the response body. Setting WP_DEBUG_DISPLAY to false and adding @ini_set( ‘display_errors’, 0 ) in wp-config.php fixes this.
How do I find which plugin or theme is generating output before the JSON body?
Three methods: binary deactivation (deactivate half your plugins, retest with curl, narrow down), grep for output calls (‘grep -rn echo|print|var_dump wp-content/plugins/’), or install the Query Monitor plugin which shows the exact file, line number, and hook for every PHP notice. The curl response itself often includes the file path and line number in the PHP notice text.
Where do these fixes not apply?
These fixes do not apply to JSON parse errors caused by a malformed request body (client-side issue), 404/401/403 status codes (routing, auth, or permission failures), CORS errors in the browser (header enforcement, not output corruption), SSL or mixed-content errors, or PHP fatal errors that produce a 500 response with an HTML error page instead of a JSON body with text prepended.
