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.

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.