Skip to content

Commit 8c64f76

Browse files
parham tehraniclaude
authored andcommitted
Fix 500 error on REST API and admin-ajax tracking endpoints
Refactored Ajax::handle() to separate exit behavior from processing logic: - Added new Ajax::process() method that returns result instead of calling exit() - Ajax::handle() now wraps process() and calls exit() for admin-ajax.php - Tracker::slimtrack_ajax() now returns result from Ajax::process() - TrackingRestController simplified - no longer needs output buffering - RestApiManager::handleAdblockTracking() now outputs result and exits The previous implementation called exit() inside Ajax::handle(), which terminated the script before the REST API could return a proper response. This caused 500 errors on both /wp-json/slimstat/v1/hit and admin-ajax.php endpoints when the REST handler couldn't complete normally. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 56463d0 commit 8c64f76

File tree

4 files changed

+46
-29
lines changed

4 files changed

+46
-29
lines changed

src/Controllers/Rest/TrackingRestController.php

Lines changed: 2 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -185,16 +185,8 @@ public function handle_tracking(\WP_REST_Request $request)
185185
\SlimStat\Services\Privacy\ConsentHandler::handleBannerConsent(false, $consent_data);
186186
}
187187

188-
// Handle tracking hits
189-
$result = null;
190-
if (function_exists('ob_start')) {
191-
ob_start();
192-
$maybe = Tracker::slimtrack_ajax();
193-
$output = ob_get_clean();
194-
$result = $maybe ?? $output;
195-
} else {
196-
$result = Tracker::slimtrack_ajax();
197-
}
188+
// Handle tracking hits - process() returns result without exit()
189+
$result = Tracker::slimtrack_ajax();
198190

199191
// Normalize to string numeric id if possible
200192
if (is_numeric($result) && (int) $result > 0) {

src/Providers/RestApiManager.php

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -166,7 +166,10 @@ public static function handleAdblockTracking(): void
166166
\SlimStat\Services\Privacy\ConsentHandler::handleBannerConsent(false, $consent_data);
167167
}
168168

169-
Tracker::slimtrack_ajax();
169+
$result = Tracker::slimtrack_ajax();
170+
// Output result and exit for adblock bypass requests
171+
echo $result;
172+
exit;
170173
}
171174
}
172175
}

src/Tracker/Ajax.php

Lines changed: 33 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -6,21 +6,37 @@
66

77
class Ajax
88
{
9+
/**
10+
* Handle AJAX tracking request with exit (for admin-ajax.php).
11+
* This wrapper calls process() and exits with the result.
12+
*/
913
public static function handle()
14+
{
15+
$result = self::process();
16+
exit($result);
17+
}
18+
19+
/**
20+
* Process tracking request and return result (for REST API and other contexts).
21+
* Returns the tracking result without calling exit().
22+
*
23+
* @return string|int The tracking result (record ID with checksum, error code, or 0)
24+
*/
25+
public static function process()
1026
{
1127
$remote_ip = isset($_SERVER['REMOTE_ADDR']) ? sanitize_text_field(wp_unslash($_SERVER['REMOTE_ADDR'])) : '';
1228
if (!empty($remote_ip)) {
1329
$key = 'slimstat_rl_' . md5($remote_ip);
1430
$hits_in_5s = (int) get_transient($key);
1531
if ($hits_in_5s >= 10) {
16-
exit(Utils::logError(429));
32+
return Utils::logError(429);
1733
}
1834

1935
set_transient($key, $hits_in_5s + 1, 5);
2036
}
2137

2238
if ('on' != \wp_slimstat::$settings['is_tracking']) {
23-
exit(Utils::logError(204));
39+
return Utils::logError(204);
2440
}
2541

2642
$id = 0;
@@ -67,7 +83,7 @@ public static function handle()
6783
// Security: Validate referer format
6884
if (false === $parsed_ref) {
6985
// Invalid referer format - reject request
70-
exit(Utils::logError(201));
86+
return Utils::logError(201);
7187
}
7288

7389
// Security: Validate host (if present) - allow external domains for referer
@@ -76,7 +92,7 @@ public static function handle()
7692
// Validate host format (prevent injection)
7793
if (!preg_match('/^[a-zA-Z0-9]([a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])?(\.[a-zA-Z0-9]([a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])?)*$/', $parsed_ref['host'])) {
7894
// Invalid host format - reject request
79-
exit(Utils::logError(201));
95+
return Utils::logError(201);
8096
}
8197
}
8298

@@ -94,13 +110,13 @@ public static function handle()
94110
if (!empty($data_js['id'])) {
95111
$data_js['id'] = Utils::getValueWithoutChecksum($data_js['id']);
96112
if (false === $data_js['id']) {
97-
exit(Utils::logError(101));
113+
return Utils::logError(101);
98114
}
99115

100116
$stat['id'] = intval($data_js['id']);
101117
if ($stat['id'] < 0) {
102118
do_action('slimstat_track_exit_' . abs($stat['id']));
103-
exit(Utils::getValueWithChecksum($stat['id']));
119+
return Utils::getValueWithChecksum($stat['id']);
104120
}
105121

106122
// Process IP according to consent status (cookie set only by consent upgrade handler)
@@ -137,7 +153,7 @@ public static function handle()
137153
// Security: Whitelist validation - only allow current site domain
138154
if (!$is_allowed_host($parsed_resource['host'])) {
139155
// Invalid host - reject request
140-
exit(Utils::logError(203));
156+
return Utils::logError(203);
141157
}
142158

143159
// Security: Validate path format (prevent path traversal attacks)
@@ -147,7 +163,7 @@ public static function handle()
147163
// Validate path contains only safe characters
148164
if (!preg_match('#^[/\w\-\.~!*\'();:@&=+$,?#\[\]%]*$#', $path)) {
149165
// Invalid path format - reject request
150-
exit(Utils::logError(203));
166+
return Utils::logError(203);
151167
}
152168

153169
// Extract path from resource URL
@@ -173,9 +189,9 @@ public static function handle()
173189
$visitIdAssigned = Session::ensureVisitId(true);
174190
$stat = \wp_slimstat::get_stat();
175191

176-
// Security: Validate visit_id exists - exit if generation failed
192+
// Security: Validate visit_id exists - return error if generation failed
177193
if (empty($stat['visit_id']) || $stat['visit_id'] <= 0) {
178-
exit(Utils::logError(500));
194+
return Utils::logError(500);
179195
}
180196

181197
$stat = Utils::getClientInfo($data_js, $stat);
@@ -244,7 +260,7 @@ public static function handle()
244260
$stat['id'] = intval($existing_record->id);
245261
\wp_slimstat::set_stat($stat);
246262
$GLOBALS['wpdb']->query('COMMIT');
247-
exit(Utils::getValueWithChecksum($stat['id']));
263+
return Utils::getValueWithChecksum($stat['id']);
248264
}
249265

250266
$GLOBALS['wpdb']->query('COMMIT');
@@ -290,7 +306,7 @@ public static function handle()
290306
$resource = Utils::base64UrlDecode($data_js['res']);
291307
$parsed_resource = parse_url($resource ?: '');
292308
if (false === $parsed_resource || empty($parsed_resource['host'])) {
293-
exit(Utils::logError(203));
309+
return Utils::logError(203);
294310
}
295311

296312
if (!empty($parsed_resource['path']) && in_array(pathinfo($parsed_resource['path'], PATHINFO_EXTENSION), \wp_slimstat::string_to_array(\wp_slimstat::$settings['extensions_to_track']))) {
@@ -333,22 +349,22 @@ public static function handle()
333349
if (!empty($data_js['res'])) {
334350
$stat['resource'] = Utils::base64UrlDecode($data_js['res']);
335351
if (false === parse_url($stat['resource'] ?: '')) {
336-
exit(Utils::logError(203));
352+
return Utils::logError(203);
337353
}
338354
}
339355

340356
$stat = Utils::getClientInfo($data_js, $stat);
341357
if (!empty($data_js['ci'])) {
342358
$data_js['ci'] = Utils::getValueWithoutChecksum($data_js['ci']);
343359
if (false === $data_js['ci']) {
344-
exit(Utils::logError(102));
360+
return Utils::logError(102);
345361
}
346362

347363
$decoded_ci = Utils::base64UrlDecode($data_js['ci']);
348364
$content_info = json_decode($decoded_ci, true);
349365
// Security: Only accept JSON-encoded content info, reject serialized data
350366
if (empty($content_info) || !is_array($content_info)) {
351-
exit(Utils::logError(103));
367+
return Utils::logError(103);
352368
}
353369

354370
foreach (['content_type', 'category', 'content_id', 'author'] as $a_key) {
@@ -378,10 +394,10 @@ public static function handle()
378394
}
379395

380396
if (empty($id)) {
381-
exit(0);
397+
return 0;
382398
}
383399

384400
do_action('slimstat_track_success');
385-
exit(Utils::getValueWithChecksum($id));
401+
return Utils::getValueWithChecksum($id);
386402
}
387403
}

src/Tracker/Tracker.php

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,15 @@
77

88
class Tracker
99
{
10+
/**
11+
* Process AJAX tracking request.
12+
* Returns the result for REST API and other callers.
13+
*
14+
* @return string|int The tracking result (record ID with checksum, error code, or 0)
15+
*/
1016
public static function slimtrack_ajax()
1117
{
12-
Ajax::handle();
18+
return Ajax::process();
1319
}
1420

1521
public static function rewrite_rule_tracker()

0 commit comments

Comments
 (0)