The timing clue in this scenario (a WAF update landing right before the break) is the kind of signal a non-expert glosses over in favor of re-reading their own PHP handler for the tenth time. Always correlate “when did this start” against recent changes outside your own codebase first.
Code
Debug an admin-ajax.php request returning a 400 or 500 error
A triage scenario for a broken AJAX handler, reasoning through nonce mismatches, missing hook prefixes, and security-plugin interference before touching application code.
Works with Claude / GPT1,600 uses★ 4.4
The prompt
code-debug-admin-ajax-error
A "Add to Wishlist" button on the front end fires an AJAX call to `admin-ajax.php`
that's now returning a 400 status, and the response body in the network tab is empty.
It worked yesterday.
CONTEXT: [the JS sends `action: 'add_to_wishlist'` plus a nonce created with
`wp_create_nonce('wishlist_nonce')`, the PHP handler is hooked on both
`wp_ajax_add_to_wishlist` and `wp_ajax_nopriv_add_to_wishlist`, and a security plugin
(Wordfence) was updated on this site two days ago]
Triage this the way a senior dev debugs a broken AJAX handler, in order:
1. Narrow down what a 400 (vs 500 vs 0) actually tells you: a 400 from `admin-ajax.php`
itself usually means the request never reached WordPress's action dispatch at all,
which points to something intercepting it before `admin-ajax.php` runs, rather than
a bug in the handler function. Check server logs and Wordfence's own blocked-request
log first, since a WAF rule update is a common cause of exactly this symptom timing.
2. If it's not being blocked, verify the nonce is actually being read and verified
correctly: check that the handler calls `check_ajax_referer('wishlist_nonce', 'nonce')`
with the same string used in `wp_create_nonce()`, and that the JS is sending the
nonce under the key PHP expects to check (`'nonce'` vs `'security'` vs a custom key
mismatch is one of the most common causes of AJAX failures that look like network
errors but are actually silent `check_ajax_referer()` deaths).
3. Confirm the hook names match exactly: `wp_ajax_{action}` and `wp_ajax_nopriv_{action}`
must match the JS `action` value character-for-character, and if the button is used
by logged-out visitors, confirm the `nopriv` hook actually exists, since a handler
registered only for logged-in users will 400/0 for guests with no obvious PHP error.
4. Check for output before the AJAX response: if any code (a stray `echo`, a PHP notice
from an unrelated plugin, a BOM character in a file) prints before `wp_die()` or
`wp_send_json_success()` is called, it can corrupt the response and some setups will
report that as a malformed/400 response rather than showing the extra output.
5. Edge case specific to this timing: since a WAF update coincided exactly with this
breaking, treat that as the leading hypothesis, not a coincidence, and check
Wordfence's live traffic log for a blocked or rate-limited entry matching this
request before spending time in the plugin's own PHP.
Replace the action name, nonce key, and security plugin with your actual setup.