The problem
An integration with an external system “sort of works”: the form submits, the user sees a thank-you, and yet the data never reaches the other end. The code looks fine. The external system replies “200 OK”. And the lead is still nowhere to be found.
On your own machine you’d drop in a few log lines and catch the culprit. But the problem only shows in production – because production is what connects outward, and localhost doesn’t reach the world. That’s where the gymnastics begin: shared hosting, the PHP error log redirected somewhere you can’t find in the panel, and no console access to the server.
Why “200 OK” can lie
The key lesson here: the HTTP status code only tells you the server received the request – not that it accepted it. An external Form Handler can return “200 OK” and, in the response body, politely say “this field is required” while saving nothing. If you only look at the status code, you see success. The truth sits in the body, which you have to pull out and read separately.
The fix: a switch and a preview file
Instead of fighting the hosting logs, I do two things. First: my own log driven by a constant, off by default. It writes to a file next to the module, so I know where to find it.
// Off by default - I turn it on only for testing.
if (!defined('MY_DEBUG')) {
define('MY_DEBUG', false);
}
private function debug_log($msg) {
if (!MY_DEBUG) return; // silent when off
$file = __DIR__ . '/_debug.log';
@file_put_contents($file, '[' . date('Y-m-d H:i:s') . "] $msg\n", FILE_APPEND);
}
I log what I actually want to know: where it went, what went, and – crucially – the response body, not just its code:
$this->debug_log('URL: ' . $url);
$this->debug_log('FIELDS: ' . wp_json_encode($fields, JSON_UNESCAPED_UNICODE));
$this->debug_log('Response: ' . wp_remote_retrieve_response_code($response)
. ' | body: ' . substr(wp_remote_retrieve_body($response), 0, 300));
Second: a small file that, when opened in a browser, prints the last lines of that log to the screen. So I don’t have to hunt for logs in the hosting panel at all – I open a URL, read, done.
<?php
// _preview.php - I delete this from the server along with the log.
$key = 'put-something-random-here';
$given = is_string($_GET['k'] ?? null) ? $_GET['k'] : '';
if (!hash_equals($key, $given)) {
http_response_code(404); // 404, not 403 - don't confirm the file exists
exit;
}
$path = __DIR__ . '/_debug.log';
if (!is_readable($path)) {
exit('No log yet - did the action run?');
}
// Read only the tail, not the whole file - logs grow.
$f = new SplFileObject($path);
$f->seek(PHP_INT_MAX);
$from = max(0, $f->key() - 100);
header('Content-Type: text/plain; charset=utf-8');
foreach (new LimitIterator($f, $from) as $line) {
echo $line;
}
The key in the URL isn’t hard security – it’s a barrier for those ten minutes, so nobody stumbles onto the file or guesses the address. That’s why 404 rather than 403: with no key, the file pretends it isn’t there. But since the log holds other people’s emails and phone numbers, the key alone isn’t an excuse – the real protection is that the file lives briefly and leaves the server the moment testing ends (more on that below). With text/plain I deliberately skip escaping: the browser renders nothing anyway, and htmlspecialchars() would turn characters in the API response into entities, making the log harder to read.
1. Flip the switch (constant to true), upload the file. 2. Trigger the action you're testing (e.g. submit the form). 3. Open the preview file in a browser - read what went out. 4. When done: switch back to false, delete from the server BOTH the preview file AND the log file.
It was exactly this loop – turn on, trigger, preview, turn off – that pinned down a whole series of small causes invisible from the outside: that the system rejected data despite “200”, that detection didn’t work the way I assumed, that field names didn’t match. Without it I’d be guessing in the dark, shipping fix after fix.
A note more important than the whole trick
Such a log records real form data – emails, phone numbers, names. It’s a temporary diagnostic tool, not part of the site. Once testing is done you must flip the switch off and delete both the preview file and the log file from the server. Left in production, it would be a personal-data leak on request.
What I learned
Not every problem is visible from the outside, and “200 OK” can be misleading. Sometimes the fastest route isn’t another fix by feel, but ten minutes spent building yourself a window through which you can finally see what’s really happening. That window is worth closing afterwards.



