$url, CURLOPT_RETURNTRANSFER => true, CURLOPT_TIMEOUT => $timeoutSeconds, CURLOPT_CONNECTTIMEOUT => 10, CURLOPT_FOLLOWLOCATION => true, CURLOPT_MAXREDIRS => 3, ]; // Use bundled CA certs if PHP/system doesn't have them configured $caBundle = Env::get('CURL_CA_BUNDLE'); if ($caBundle === '') { // Common fallback locations foreach ([ dirname(__DIR__, 2) . '/cacert.pem', (getenv('APPDATA') ?: '') . '/php/cacert.pem', ] as $candidate) { if ($candidate !== '' && is_file($candidate)) { $caBundle = $candidate; break; } } } if ($caBundle !== '') { $opts[CURLOPT_CAINFO] = $caBundle; } curl_setopt_array($ch, $opts); $headers[] = 'Accept: application/json'; if ($jsonBody !== null) { $encoded = json_encode($jsonBody, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); curl_setopt($ch, CURLOPT_POSTFIELDS, $encoded); $headers[] = 'Content-Type: application/json'; } $method = strtoupper($method); match ($method) { 'GET' => null, 'POST' => curl_setopt($ch, CURLOPT_POST, true), 'PUT' => curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'PUT'), 'PATCH' => curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'PATCH'), 'DELETE' => curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'DELETE'), default => curl_setopt($ch, CURLOPT_CUSTOMREQUEST, $method), }; curl_setopt($ch, CURLOPT_HTTPHEADER, $headers); $body = curl_exec($ch); $status = (int)curl_getinfo($ch, CURLINFO_HTTP_CODE); $error = curl_error($ch); curl_close($ch); if ($body === false) { throw new HttpException(502, 'UPSTREAM_ERROR', "cURL error: {$error}"); } $json = json_decode((string)$body, true); return [ 'status' => $status, 'body' => (string)$body, 'json' => $json, ]; } }