WordPress REST API error: what breaks REST and how to troubleshoot

Was this helpful?

Some WordPress failures are loud: white screens, fatal errors, the kind you notice before your coffee cools. REST API failures are quieter and more annoying. Your editor won’t save, your headless frontend goes blank, WooCommerce checks out “sometimes,” and the only clue is a smug message like “The REST API encountered an error.”

Here’s the practical reality: the REST API isn’t one feature. It’s a chain—rewrites, routing, authentication, cookies/nonces, caching layers, security middleware, and your hosting stack—all of which can break independently. We’re going to isolate which link snapped, with commands you can run, outputs you can interpret, and decisions you can make without guessing.

How WordPress REST actually works (and where it fails)

When someone says “the WordPress REST API is broken,” they usually mean one of these things:

  • The route can’t be reached (rewrites, permalinks, server config, reverse proxy, CDN, or WAF is in the way).
  • The route is reached but blocked (auth failures, nonce/cookie issues, capability checks, security plugins, or WAF rules).
  • The route runs and crashes (PHP fatal, DB timeouts, memory limits, plugin code path, serialization errors).
  • The route runs but response gets mangled (caching, compression, content-type changes, unexpected redirects, mixed content, CORS).

On a healthy site, the canonical base endpoint is:

  • /wp-json/ (a JSON index of namespaces and routes)
  • Example route: /wp-json/wp/v2/posts

Under the hood, WordPress uses rewrite rules to route /wp-json/... to index.php, where the REST server dispatches to registered routes. That means even “API” failures often start as boring rewrite failures.

Also: REST requests are just HTTP requests. That’s the power and the curse. Every middlebox that ever “helped” a website (cache, CDN, firewall, bot blocker, gzip filter, minifier, “security hardener”) can “help” your REST API into a grave.

One operational quote to keep you honest:

“Hope is not a strategy.” — Gen. Gordon R. Sullivan

Where failures cluster

In production, REST API issues concentrate around five layers:

  1. DNS/TLS/edge: certificates, SNI mismatches, HTTP→HTTPS redirects, CDN caching, WAF blocks.
  2. Reverse proxy / web server: Nginx/Apache rewrites, path normalization, header stripping, request body limits.
  3. WordPress app layer: permalink settings, route registration, plugin conflicts, auth and nonce logic.
  4. State and storage: DB locks/timeouts, object cache weirdness, filesystem permissions.
  5. Client behavior: JS frontend, Gutenberg editor, CORS, mixed cookies, stale nonces.

Don’t troubleshoot from the WordPress admin UI first. That’s like diagnosing a network outage by staring at your laptop wallpaper. Start by making a request from outside the browser with known inputs and known outputs.

Fast diagnosis playbook

When REST is “down,” you want an answer in minutes: is this edge/proxy, app, or backend? Use this playbook in order. Don’t skip steps because you “already know.” That’s how incidents become memoirs.

First: confirm the endpoint and the HTTP truth

  1. Request /wp-json/ with curl and record: status code, content-type, and any redirects.
  2. Compare from two vantage points: from your laptop and from the server (or a box inside the same network as WordPress).
  3. Check for HTML instead of JSON. HTML often means you’re getting a login page, WAF block page, or a cached error template.

Second: isolate edge/security from WordPress

  1. Bypass CDN if possible (direct origin host, internal load balancer VIP, or host file override in a controlled environment).
  2. Check WAF/security plugin logs for blocks on /wp-json and for common signatures (SQLi/XSS false positives).
  3. Confirm no forced redirect loops (HTTP→HTTPS→HTTP, www→non-www, trailing slash rules).

Third: prove WordPress routing and permalinks

  1. Verify permalinks aren’t set to “Plain.” REST can still work in plain mode, but rewrites frequently end up inconsistent when people toggle settings without flushing rules.
  2. Confirm rewrites exist in server config (Nginx location rules) or .htaccess (Apache).
  3. Check if index.php?rest_route=/ works. If yes, rewrites are broken; if no, app or blocking.

Fourth: authenticate only after the path works

  1. Test public endpoints first (posts list) before private endpoints (users, settings).
  2. If auth is required, test with Application Passwords or Basic Auth (in a safe environment) to remove nonce/cookie complexity.
  3. Then test cookie+nonce flows for editor issues.

Fifth: if it’s slow or 500’ing, treat it like backend saturation

  1. Correlate with PHP-FPM saturation, DB slow queries, and error logs.
  2. Take a sample of failing requests and trace: web server access log → PHP error log → WP debug log → DB logs.
  3. Decide: roll back plugin/theme change, scale, or mitigate with caching exemptions.

Decision rule: If /wp-json/ is not a stable 200 with JSON from two vantage points, do not waste time debugging WordPress features. Fix routing/edge first.

Symptom-to-layer map: decode errors by HTTP behavior

REST errors often show up as a generic WordPress admin notice. Ignore the notice and look at HTTP.

Status codes and what they usually mean

  • 200 but wrong content-type (HTML): you hit a login page, a WAF block page, a cached error page, or a redirect target.
  • 301/302 loops: canonicalization rules fighting (HTTP/HTTPS, www/non-www, trailing slash), or proxy headers mis-set.
  • 401 Unauthorized: missing/invalid credentials, nonce failure, Application Passwords disabled, security plugin intercepting.
  • 403 Forbidden: WAF/CDN rule, mod_security, IP restrictions, “block JSON requests,” or a capability check failing.
  • 404 Not Found: rewrites not working, Nginx location mismatch, multisite path issues, or routes not registered.
  • 405 Method Not Allowed: proxy/server disallows POST/PUT, or caching layer only allows GET.
  • 413 Payload Too Large: proxy body size limits; REST uploads fail, media endpoints fail.
  • 429 Too Many Requests: rate limiter, WAF, bot protection, or aggressive plugin throttle.
  • 500/502/503: PHP fatal, upstream crash, php-fpm exhausted, DB timeouts, origin down, or gateway timeouts.

Joke #1: The REST API is “stateless,” which is adorable—like calling a toddler “quiet” five minutes before the sugar hits.

Practical tasks (commands, outputs, decisions)

These are not “tips.” They’re repeatable tasks. Each one includes a command, what the output means, and what decision you make next.

Task 1: Check the REST index from your workstation

cr0x@server:~$ curl -sS -D- -o /tmp/rest.json https://example.com/wp-json/ | head -n 20
HTTP/2 200
content-type: application/json; charset=UTF-8
x-robots-tag: noindex
x-content-type-options: nosniff

What the output means: You got a 200 and JSON content-type. Good: routing and basic response path work from outside.

Decision: Move on to auth- and plugin-related checks. If you see 301/302, follow redirects (-L) and inspect canonicalization. If content-type is text/html, you are not getting the REST API.

Task 2: Verify whether you’re getting HTML instead of JSON

cr0x@server:~$ curl -sS -D- https://example.com/wp-json/ | sed -n '1,25p'
HTTP/2 403
content-type: text/html; charset=UTF-8
server: cloudflare

<!doctype html>
<html>
<head><title>Access denied</title></head>

What the output means: 403 with an HTML block page and a CDN/server header. That’s not WordPress. That’s edge security.

Decision: Stop touching WordPress. Investigate WAF/CDN/firewall rules and bypass the edge to confirm origin health.

Task 3: Test from the origin host to bypass CDN/WAF (where possible)

cr0x@server:~$ curl -sS -D- -H 'Host: example.com' http://127.0.0.1/wp-json/ | head -n 15
HTTP/1.1 200 OK
Content-Type: application/json; charset=UTF-8
X-Powered-By: PHP/8.2.12

What the output means: Origin serves JSON fine. Edge is the problem.

Decision: Fix edge policy: allow /wp-json/*, allow required methods, adjust bot rules, or add a WAF exception for REST endpoints (tightly scoped).

Task 4: Determine if rewrites are broken by using rest_route

cr0x@server:~$ curl -sS -D- "https://example.com/index.php?rest_route=/" | head -n 15
HTTP/2 200
content-type: application/json; charset=UTF-8

What the output means: The REST API works when you bypass rewrites. So your rewrite layer is broken.

Decision: Fix server rewrite config or permalink flushing. Don’t waste time debugging plugins until /wp-json/ works like index.php?rest_route=/.

Task 5: Check Apache rewrite and .htaccess health

cr0x@server:~$ sudo apachectl -M | egrep 'rewrite|headers'
 rewrite_module (shared)
 headers_module (shared)

What the output means: Required modules exist. If rewrite is missing, permalinks and REST routes often fail.

Decision: If rewrite_module is absent, enable it and reload Apache. If present, inspect the vhost and .htaccess rules next.

Task 6: Validate Nginx routing for wp-json (common pitfall)

cr0x@server:~$ sudo nginx -T 2>/dev/null | sed -n '/server_name example.com/,/}/p' | egrep -n 'location|try_files|rewrite'
12:    location / {
13:        try_files $uri $uri/ /index.php?$args;
14:    }
28:    location ~ \.php$ {
29:        include snippets/fastcgi-php.conf;
30:        fastcgi_pass unix:/run/php/php8.2-fpm.sock;
31:    }

What the output means: A sane try_files fallback routes unknown paths (including /wp-json/) to index.php.

Decision: If you see try_files $uri =404; without index fallback, fix it or REST will 404. After changes, reload Nginx and retest.

Task 7: Confirm WordPress thinks the site URL matches reality

cr0x@server:~$ wp option get siteurl --path=/var/www/html
https://example.com
cr0x@server:~$ wp option get home --path=/var/www/html
https://example.com

What the output means: These URLs drive canonical redirects and cookies. Mismatch (http vs https, www vs non-www) breaks nonce/cookie auth and creates redirect loops.

Decision: If they’re wrong, fix them (carefully) and then validate redirects and REST calls.

Task 8: Inspect WordPress rewrite rules and flush them safely

cr0x@server:~$ wp rewrite list --path=/var/www/html | head -n 12
^wp-json/?$		index.php?rest_route=/	last
^wp-json/(.+)?$	index.php?rest_route=/$matches[1]	last
^wp-sitemap\.xml$	index.php?sitemap=index	last

What the output means: The REST rewrite rules exist at the WordPress level.

Decision: If these are missing, flush rules:

cr0x@server:~$ wp rewrite flush --hard --path=/var/www/html
Success: Rewrite rules flushed.

Decision: If WordPress has the rules but HTTP still 404s, your web server isn’t honoring them (Nginx config, Apache AllowOverride, etc.).

Task 9: Check whether Apache allows .htaccess overrides

cr0x@server:~$ sudo apachectl -S 2>/dev/null | sed -n '1,80p'
VirtualHost configuration:
*:80                   example.com (/etc/apache2/sites-enabled/example.conf:1)

cr0x@server:~$ sudo sed -n '1,120p' /etc/apache2/sites-enabled/example.conf | egrep -n 'DocumentRoot|Directory|AllowOverride'
2: DocumentRoot /var/www/html
5: <Directory /var/www/html>
6:     AllowOverride None
7: </Directory>

What the output means: AllowOverride None means your .htaccess is ignored. Permalinks and REST rewrites fail unless the vhost has equivalent rewrite rules.

Decision: Set AllowOverride All (or explicitly add rewrite rules in vhost), reload Apache, retest /wp-json/.

Task 10: Identify if a plugin is intercepting REST requests

cr0x@server:~$ wp plugin list --status=active --path=/var/www/html
+-------------------------+--------+-----------+---------+
| name                    | status | update    | version |
+-------------------------+--------+-----------+---------+
| wordfence               | active | none      | 7.11.6  |
| w3-total-cache          | active | available | 2.7.5   |
| classic-editor          | active | none      | 1.6.7   |
+-------------------------+--------+-----------+---------+

What the output means: Security and caching plugins are common REST troublemakers. This isn’t blame; it’s probability.

Decision: Temporarily disable suspected plugins (in a controlled window) and retest. Start with WAF/security plugins and aggressive caching plugins.

Task 11: Temporarily disable plugins without killing production blindly

cr0x@server:~$ wp plugin deactivate wordfence w3-total-cache --path=/var/www/html
Plugin 'wordfence' deactivated.
Plugin 'w3-total-cache' deactivated.

What the output means: You’re testing whether REST recovers with the interception removed.

Decision: If REST recovers, re-enable one plugin at a time to isolate. Then configure: allow /wp-json/, disable REST blocking, exclude caching for REST endpoints.

Task 12: Check for REST-related errors in PHP logs

cr0x@server:~$ sudo tail -n 40 /var/log/php8.2-fpm.log
[27-Dec-2025 10:12:41] WARNING: [pool www] child 1842 said into stderr: "PHP Warning:  Cannot modify header information - headers already sent in /var/www/html/wp-content/plugins/some-plugin/plugin.php on line 88"
[27-Dec-2025 10:12:41] WARNING: [pool www] child 1842 said into stderr: "PHP Fatal error:  Uncaught TypeError: json_encode(): Argument #2 ($flags) must be of type int, string given in /var/www/html/wp-content/plugins/some-plugin/api.php:55"

What the output means: That fatal error can turn an API request into a 500 and WordPress will report “REST API encountered an error.”

Decision: Roll back the plugin/theme version, patch the code, or pin PHP version if a compatibility break was introduced. Fixing the fatal is non-negotiable.

Task 13: Inspect Nginx/Apache access logs for status and upstream timing

cr0x@server:~$ sudo tail -n 20 /var/log/nginx/access.log | egrep 'wp-json|rest_route' | tail -n 5
203.0.113.10 - - [27/Dec/2025:10:13:02 +0000] "GET /wp-json/ HTTP/2.0" 200 2534 "-" "curl/8.5.0"
203.0.113.10 - - [27/Dec/2025:10:13:05 +0000] "POST /wp-json/wp/v2/posts HTTP/2.0" 401 214 "-" "Mozilla/5.0"
203.0.113.10 - - [27/Dec/2025:10:13:08 +0000] "GET /index.php?rest_route=/wp/v2/users/me HTTP/2.0" 200 612 "-" "Mozilla/5.0"

What the output means: Public GET works, authenticated POST fails with 401, but an authenticated request via rest_route works (maybe with cookies). This points to auth flow differences, headers stripped, or nonce issues.

Decision: Focus on authentication: cookies, nonces, Application Passwords, reverse proxy headers, or security plugins that treat POSTs to /wp-json differently.

Task 14: Confirm response headers aren’t being stripped by the proxy

cr0x@server:~$ curl -sS -D- -o /dev/null https://example.com/wp-json/ | egrep -i 'content-type|cache-control|vary|www-authenticate|set-cookie'
content-type: application/json; charset=UTF-8
cache-control: no-cache, must-revalidate, max-age=0
vary: Accept-Encoding

What the output means: For many REST endpoints, correct cache-control behavior matters. Missing Set-Cookie on login flows or missing WWW-Authenticate on 401 can also signal header mangling.

Decision: If headers differ between origin and edge, fix proxy config (header pass-through) or CDN behavior. REST and “smart” caching are not friends by default.

Task 15: Check if basic REST auth works with an Application Password

cr0x@server:~$ curl -sS -u "apiuser:abcd efgh ijkl mnop" https://example.com/wp-json/wp/v2/users/me | head
{"id":2,"name":"API User","url":"","description":"","link":"https:\/\/example.com\/author\/apiuser\/"}

What the output means: Authentication works and WordPress can serve authenticated REST requests.

Decision: If the editor still fails but this works, your issue is probably cookie+nonce or CORS, not “REST is down.”

Task 16: Detect redirect loops and canonicalization fights

cr0x@server:~$ curl -sS -I -L -o /dev/null -w '%{url_effective} %{http_code}\n' http://example.com/wp-json/
https://www.example.com/wp-json/ 200

What the output means: HTTP was redirected to HTTPS and www, then succeeded.

Decision: If you see repeated redirects or it ends on a non-JSON location, fix siteurl/home, proxy headers (X-Forwarded-Proto), and canonical rules in the edge and WordPress.

Task 17: Check CORS headers for headless frontends

cr0x@server:~$ curl -sS -D- -o /dev/null -X OPTIONS \
  -H 'Origin: https://app.example.net' \
  -H 'Access-Control-Request-Method: POST' \
  https://example.com/wp-json/wp/v2/posts | egrep -i 'http/|access-control'
HTTP/2 403

What the output means: OPTIONS preflight is being blocked. Many WAFs treat OPTIONS as suspicious. Browsers will refuse the actual request if preflight fails.

Decision: Allow OPTIONS to /wp-json/* at the edge, and configure WordPress/CORS plugin carefully. Also confirm you’re not accidentally requiring auth for preflight.

Task 18: Check PHP-FPM saturation (REST “works” until it doesn’t)

cr0x@server:~$ sudo ss -s | sed -n '1,20p'
Total: 693
TCP:   41 (estab 18, closed 12, orphaned 0, synrecv 0, timewait 12/0), ports 0

Transport Total     IP        IPv6
RAW       0         0         0
UDP       9         7         2
TCP       29        18        11
INET      38        25        13
FRAG      0         0         0

cr0x@server:~$ sudo systemctl status php8.2-fpm --no-pager | sed -n '1,20p'
● php8.2-fpm.service - The PHP 8.2 FastCGI Process Manager
     Loaded: loaded (/lib/systemd/system/php8.2-fpm.service; enabled)
     Active: active (running) since Sat 2025-12-27 09:40:12 UTC; 32min ago
   Main PID: 1021 (php-fpm8.2)
     Status: "Processes active: 28, idle: 0, Requests: 1827, slow: 12"

What the output means: Zero idle workers and slow requests climbing means the REST API may fail under load with 502/504, especially for authenticated endpoints.

Decision: Increase FPM capacity (within RAM limits), reduce expensive plugins, add caching for public endpoints, and stop letting bots hammer /wp-json.

What commonly breaks REST (real failure modes)

1) Rewrites and permalinks: the boring foundation

If /wp-json/ returns 404 but index.php?rest_route=/ works, your rewrites are broken. That’s usually one of:

  • Apache mod_rewrite disabled.
  • AllowOverride None blocks .htaccess.
  • Nginx missing try_files ... /index.php fallback.
  • Multisite path rules not matching actual directory structure.
  • Permalinks toggled and rules not flushed.

Opinionated guidance: if you’re on Nginx and you’re relying on WordPress-style “automatic” rewrites, you’re already negotiating with physics. Put the correct Nginx rules in version control and stop pretending the admin UI controls your web server.

2) Reverse proxies and “helpful” redirects

REST is sensitive to scheme/host correctness because authentication uses cookies and nonces tied to origin. If your proxy terminates TLS but WordPress thinks it’s on HTTP, you’ll see:

  • redirect loops
  • cookies set for the wrong domain
  • nonce validation failures (often showing as 401)

Fixes usually involve setting the right forwarded headers and ensuring WordPress respects them (or setting home/siteurl correctly, plus proxy-aware config).

3) WAF and bot protection that blocks /wp-json

Security layers love to block JSON APIs because attackers love JSON APIs. Unfortunately your editor also loves JSON APIs.

Common WAF triggers:

  • POST bodies that look like HTML or contain <script> in legitimate content.
  • Repeated requests from logged-in users (Gutenberg is chatty).
  • OPTIONS preflight for CORS.
  • Rate limiting that doesn’t distinguish bots from editors.

Fix by narrowing exceptions: allow specific methods and paths, log aggressively, and keep the blast radius small. “Disable WAF” is not a fix; it’s a resignation letter.

4) Caching layers that treat REST like static content

Public endpoints can be cached carefully. Authenticated endpoints should usually not be cached at the edge. When caches get this wrong, you see:

  • users seeing each other’s data (rare but catastrophic)
  • stale nonces and random 401s
  • random HTML responses because the cache stored a block page

Practical rule: never cache requests with cookies at the edge unless you know exactly what you’re doing and you can prove isolation.

5) Plugin conflicts: REST route registration and request filtering

Plugins can break REST by:

  • registering routes incorrectly (namespace conflicts, wrong permission callbacks)
  • filtering rest_authentication_errors and returning errors unconditionally
  • outputting whitespace/notices that corrupt JSON
  • changing content-type or buffering output

If disabling one plugin fixes it, you found your culprit. If disabling all plugins fixes it, you found a plugin, not which one. Isolate systematically.

6) Authentication: cookies, nonces, and the many ways they go stale

Gutenberg and admin screens often use cookie authentication with nonces. That means the browser must:

  • send the right cookies for the right domain/path
  • send a valid nonce header (X-WP-Nonce)
  • not be blocked by SameSite policies or cross-domain mismatches

When this fails, the REST API itself might be fine, but the editor screams. You fix it by aligning domains, schemes, and cookie behavior—not by “reinstalling WordPress.”

7) Backend reliability: PHP fatals, DB timeouts, storage hiccups

REST requests execute WordPress code paths. They hit the database. They read/write options. They may generate thumbnails. They may talk to external APIs. If your storage is slow or your DB is unhappy, REST endpoints will fail first because they’re invoked constantly by modern WordPress features.

As a storage engineer’s note: “random” REST failures under load often correlate with I/O latency spikes, especially on shared hosting or undersized networked disks. Check I/O wait and DB health before you accuse REST of being “flaky.”

Joke #2: If your REST API only fails on Mondays, it’s not haunted—it’s your deploy schedule wearing a mask.

Three corporate mini-stories from the trenches

Mini-story 1: The incident caused by a wrong assumption

A mid-sized company ran WordPress behind a reverse proxy and a CDN. They rolled out a new headless landing page that pulled posts via REST. In staging, it worked. In production, the editor started failing to publish and the headless page occasionally returned 401. Everyone blamed “WordPress being WordPress.”

The wrong assumption was simple: “If the website loads, the API must be fine.” The homepage was cached at the CDN. The REST calls were not—except sometimes they were, because the CDN had a generic rule that cached “all GET requests.” Public REST GETs got cached. Authenticated GETs got cached too, because the cache key ignored cookies for performance.

The result was intermittent chaos: one editor’s authenticated response could be served to another editor (sometimes blocked by capability checks, sometimes not). The team didn’t notice data leakage, but they did notice broken sessions and nonce failures because the cached responses didn’t match the browser’s expectations.

The fix was not heroic. They changed the CDN policy: bypass caching for any request with Cookie and for any path under /wp-json/ unless explicitly whitelisted. Then they whitelisted only a few public endpoints with a safe cache key and short TTL. REST stabilized instantly.

The takeaway: never assume “web works” implies “API works,” especially when caching is involved. Your homepage is a liar; curl is honest.

Mini-story 2: The optimization that backfired

An enterprise marketing team wanted faster TTFB. An engineer enabled aggressive HTML optimization and response compression at the reverse proxy. They also enabled a “security hardening” rule set to block “suspicious query strings.”

Performance improved for the public site. Then Gutenberg started failing on image uploads and post updates. The WordPress Site Health panel reported REST API errors. Developers began chasing phantom plugin conflicts.

The real problem: the proxy optimization included a rule that buffered and rewrote responses and enforced a small client_max_body_size. Media uploads through REST hit 413. Meanwhile, the “suspicious query string” rule flagged rest_route= patterns and blocked fallback requests used by some clients and plugins.

The fix was embarrassingly mundane: raise body size limits on the relevant location blocks, disable rewriting for JSON responses, and adjust WAF rules for known REST routes. They also added monitoring specifically for /wp-json/ status codes to catch regression before marketing noticed.

The takeaway: optimizations are allowed. But they need an allowlist mentality for APIs. If your proxy modifies payloads, you’re doing application work without application tests.

Mini-story 3: The boring but correct practice that saved the day

A global org ran multiple WordPress instances with identical infrastructure-as-code for Nginx and PHP-FPM. Their SRE team enforced one boring policy: every config change had a canary, and every canary had a synthetic check that hit /wp-json/, a public endpoint, and an authenticated endpoint.

One week, a minor Nginx change intended to tighten security accidentally removed the try_files fallback and turned unknown paths into 404s. The public site still worked because most pages were cached and common routes existed. But REST calls began failing immediately on the canary.

The synthetic checks caught it in minutes. They rolled back before full rollout. No customer ticket storm. No frantic Slack archaeology. Just a clean revert and a small postmortem.

The takeaway: the “boring” practices—canaries, synthetic checks, and config in version control—save you from cleverness. REST is a perfect canary target because it exercises routing, PHP, and app logic quickly.

Common mistakes: symptom → root cause → fix

This section is meant to be used under pressure. Find your symptom and do the fix. Don’t debate philosophy with your outage.

1) WordPress says “REST API encountered an error,” and /wp-json returns HTML

  • Symptom: content-type: text/html, maybe a login page or block page.
  • Root cause: WAF/CDN block, redirect to login, or cached error template.
  • Fix: Bypass edge to origin and compare. Disable caching for /wp-json. Add WAF allow rules for REST paths/methods. Confirm no auth redirect is applied to API routes.

2) /wp-json works, but authenticated endpoints return 401 in the editor

  • Symptom: Public endpoints OK; editor actions fail; random “nonce invalid.”
  • Root cause: Cookie domain/scheme mismatch, SameSite cookie issues, proxy not sending correct X-Forwarded-Proto, or caching nonces.
  • Fix: Align home and siteurl. Fix proxy headers and canonical redirects. Ensure authenticated REST requests are not cached anywhere.

3) /wp-json returns 404 but index.php?rest_route works

  • Symptom: Rewrite path fails, direct query works.
  • Root cause: Nginx/Apache rewrite misconfiguration, .htaccess ignored, or missing try_files fallback.
  • Fix: Fix server config. Flush rewrites after. Retest both endpoints until identical behavior.

4) REST requests return 403 only for POST/PUT/DELETE

  • Symptom: GET is fine; writes are blocked.
  • Root cause: WAF rule blocks methods, mod_security CRS false positives, or proxy method restrictions.
  • Fix: Review WAF logs for rule ID and adjust exceptions narrowly for /wp-json. Ensure proxy allows these methods to the origin.

5) REST returns 500 intermittently, logs show PHP fatals

  • Symptom: 500/502 with stack traces in PHP logs.
  • Root cause: Plugin/theme bug or PHP version incompatibility.
  • Fix: Roll back the change that introduced fatals. Patch plugin. Add automated tests or at least synthetic REST checks in deploy pipelines.

6) REST is slow; sometimes 504 Gateway Timeout

  • Symptom: Long response times, then timeouts under load.
  • Root cause: PHP-FPM worker exhaustion, DB contention, slow storage, external API calls in request path.
  • Fix: Profile slow requests; increase FPM capacity carefully; optimize DB and object caching; add timeouts/circuit breakers for external calls; block abusive clients.

7) CORS errors in browser console, curl works

  • Symptom: Browser blocks requests; curl succeeds.
  • Root cause: Missing/blocked CORS headers, OPTIONS blocked, wrong origin headers.
  • Fix: Allow OPTIONS through WAF/proxy and configure correct Access-Control-Allow-Origin and related headers, ideally with an allowlist.

Checklists / step-by-step plan

Checklist A: You have a REST error right now

  1. Run curl to /wp-json/ and capture status, headers, and body type (JSON vs HTML).
  2. Run curl to /index.php?rest_route=/ and compare.
  3. Test from origin host with Host: header to bypass edge.
  4. If edge is implicated: check WAF/CDN logs for blocks, rate limits, and method restrictions.
  5. If rewrites implicated: inspect Nginx try_files or Apache AllowOverride/mod_rewrite.
  6. If auth implicated: test Application Password; then test editor/cookie+nonce flows.
  7. If 500/timeout: check PHP logs, FPM saturation, DB health, and recent deploy/plugin changes.

Checklist B: Prevent the next incident (operational hygiene)

  1. Add synthetic checks for /wp-json/, a public endpoint, and an authenticated endpoint.
  2. Instrument status codes for /wp-json in access logs and alert on 4xx/5xx spikes.
  3. Put Nginx/Apache configs in version control; gate changes with a canary.
  4. Define caching policy explicitly: which REST endpoints are cacheable, and how cache keys are built.
  5. Create a WAF exception process: path/method allowlists, rule ID tracking, and expiry reviews.
  6. Keep plugin updates staged; monitor REST synthetic checks after deploy.

Checklist C: When you suspect a plugin conflict

  1. Snapshot active plugin list.
  2. Disable one high-risk plugin (security/caching) at a time.
  3. Retest /wp-json/ and a failing route.
  4. When it works, re-enable others one-by-one to isolate.
  5. Apply configuration fix or replace the plugin if it can’t coexist with your requirements.

Interesting facts and history (short, useful context)

  • Fact 1: The WordPress REST API began as a feature plugin before merging into WordPress core, which is why older blog posts still reference the “REST API plugin.”
  • Fact 2: The base endpoint /wp-json/ is an index document—an API “table of contents”—so a broken index is often a routing or blocking issue, not “missing posts.”
  • Fact 3: Gutenberg (the block editor) depends heavily on REST. If REST is flaky, the editor is usually the first to complain.
  • Fact 4: WordPress can serve REST via a query parameter (rest_route), which makes it a great tool for distinguishing rewrite problems from application problems.
  • Fact 5: Many security products historically treated JSON endpoints as “API surfaces” and defaulted to stricter inspection, which is why false positives cluster around /wp-json.
  • Fact 6: REST routes are registered by code at runtime. If a plugin fails to load (fatal error), entire route namespaces can disappear, turning expected 200s into 404s.
  • Fact 7: WordPress authentication for browser-based REST calls often uses nonces rather than Basic Auth; nonces are time-sensitive and easy to break with caching or clock skew.
  • Fact 8: Historically, some hosts and plugins blocked /wp-json/wp/v2/users paths to reduce enumeration risk, sometimes blocking too broadly and breaking legitimate admin actions.
  • Fact 9: CDN caching policies that ignore cookies can cause API responses to leak between sessions—rare, but it has happened enough that seasoned SREs get twitchy about it.

FAQ

1) How do I tell if WordPress REST is down or just the editor?

Run curl -D- https://example.com/wp-json/. If it’s 200 with JSON, REST is up. If the editor fails, focus on auth/nonce/cookies and caching.

2) Why does /wp-json work but /wp-json/wp/v2/posts fails?

The index can load while specific routes fail due to plugin/theme code, permission callbacks, or route registration issues. Check PHP logs and disable plugins to isolate.

3) What’s the quickest rewrite test?

Compare /wp-json/ with /index.php?rest_route=/. If only the latter works, your web server rewrites are the issue.

4) Why do I get 401 Unauthorized even when logged into wp-admin?

Typically cookie/nonce mismatch due to domain or scheme inconsistency (www vs non-www, http vs https), SameSite cookie behavior, or a proxy misreporting HTTPS.

5) How do I know if the WAF is blocking REST?

Look for 403 with HTML block pages, CDN/WAF headers, or blocks that happen only on POST/OPTIONS. Bypass the edge to origin; if origin works, it’s the WAF/CDN.

6) Can I cache REST API responses?

Yes, selectively. Cache only truly public, non-personalized endpoints. Never cache requests with cookies at the edge unless you can prove safe cache keys and isolation.

7) Why do I see CORS errors but curl succeeds?

Browsers enforce CORS and often send an OPTIONS preflight. If OPTIONS is blocked or CORS headers aren’t correct, the browser refuses. Curl doesn’t care.

8) Does disabling plugins always prove a plugin issue?

It proves interaction, not guilt. A plugin might only trigger a proxy/WAF rule, or expose a latent config problem. Use it to narrow scope, then validate the real root cause.

9) What’s the safest authentication method for server-to-server REST calls?

Application Passwords are usually the least painful in core WordPress. Use HTTPS, restrict scope, rotate credentials, and monitor for abuse.

10) Why does REST fail only under load?

Because under load you hit limits: PHP-FPM workers, DB connections, I/O latency, or rate limiting. REST endpoints are chatty and amplify bottlenecks quickly.

Conclusion: next steps that actually reduce recurrence

If you take only a few actions from this, make them these:

  1. Make /wp-json/ observable. Add synthetic checks and alerting on status codes and latency. REST is the canary for modern WordPress.
  2. Separate routing from auth from backend health. Use the rest_route test to prove rewrites, then test public endpoints, then authenticated endpoints.
  3. Stop letting middleboxes freestyle. Explicitly define caching and WAF behavior for /wp-json. Default-deny is fine; default-surprise is not.
  4. Version control your web server config. If your REST availability depends on a one-off Nginx snippet pasted three years ago, you don’t have config—you have folklore.

Now do the unglamorous thing: run the first three tasks, write down the observed behavior, and follow the layer where reality points. WordPress REST failures are rarely mysterious. They’re just distributed across enough components that people confuse “complicated” with “unknowable.”

← Previous
Office VPN: Allow Access to One Server/Port Only (Least Privilege)
Next →
WordPress + Cloudflare Cache Settings That Won’t Break wp-admin

Leave a comment