ABJ_404_Solution_Utf8SafeRecord::clip( (string)$requestReader->getPostOrGetSanitize('sessionId', ''), 64), 'retry_parent_id' => self::normalizeId($requestReader->getPostOrGetSanitize('retryParentId', ''), ''), 'client_sent_at' => ABJ_404_Solution_Utf8SafeRecord::clip( (string)$requestReader->getPostOrGetSanitize('clientSentAt', ''), 64), 'header_request_id' => self::readRequestIdHeader(), 'cf_ray' => self::readCfRayHeader(), ); } /** * The client sends X-ABJ404-Request-ID; this validates it. PHP maps that * request header to $_SERVER['HTTP_X_ABJ404_REQUEST_ID']. */ public static function readRequestIdHeader(): string { return self::normalizeId($_SERVER['HTTP_X_ABJ404_REQUEST_ID'] ?? '', ''); } /** Cloudflare's per-request trace ID, captured into the journal when present. */ public static function readCfRayHeader(): string { $header = $_SERVER['HTTP_CF_RAY'] ?? ''; return is_scalar($header) ? ABJ_404_Solution_Utf8SafeRecord::clip((string)$header, 64) : ''; } /** * A header ID that disagrees with the body ID means something between * the browser and PHP rewrote or replayed the request. That is evidence * about the transport, not noise to silently drop. */ public static function recordHeaderMismatchIfAny(string $requestId, string $headerRequestId): void { if ($headerRequestId === '' || $headerRequestId === $requestId) { return; } ABJ_404_Solution_AjaxCheckpointLogger::record($requestId, 'request_id_header_mismatch', array( 'header_request_id' => $headerRequestId, )); } /** * Normalized ID of the in-flight request, or '' when this request has no * ledger entry at all (a handler that never populated request_id). The * ledger is opt-in per endpoint and a fabricated ID would be worse than * none, so '' means "emit nothing", not "emit the sentinel". * * Unlike instrumentedRequestId() this is NOT gated on the action: the * ledger must be echoed on every response it covers, while checkpoint * file writes stay scoped to the endpoint under investigation. */ public static function requestIdFromGlobalContext(): string { $ctx = $GLOBALS['abj404_ajax_context'] ?? null; if (!is_array($ctx) || !array_key_exists('request_id', $ctx)) { return ''; } $raw = $ctx['request_id']; if (!is_scalar($raw) || (string)$raw === '') { return ''; } return self::normalizeId($raw); } /** * Stamp the ledger ID onto an outbound payload that does not already * carry one. * * Applied at the single response choke point rather than at each call * site: the early-response branches (rate-limit 429, auth-failure 403) * are both the ones a stalled request is most likely to hit and the * easiest for a future branch to forget. Stamping centrally makes "an * error response the client cannot join back to its request ID" * impossible by construction instead of by discipline. A payload that * already set its own requestId is left exactly as its handler built it. * * @param mixed $payload * @return mixed */ public static function stampOnPayload($payload, string $requestId) { if ($requestId === '' || !is_array($payload) || array_key_exists('requestId', $payload)) { return $payload; } $payload['requestId'] = $requestId; return $payload; } }