Refactor HTTP request handling and add smoke test for server status checks
This commit is contained in:
parent
05a0c7bb78
commit
25abf317cb
@ -84,21 +84,59 @@ function is_allowed_http_url(string $url, array $allowedHosts): bool
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* HTTP HEAD check (no redirects).
|
* Extract first HTTP status code from a response header array.
|
||||||
|
* @param array<int,string> $headers
|
||||||
|
*/
|
||||||
|
function extract_http_status_code(array $headers): ?int
|
||||||
|
{
|
||||||
|
foreach ($headers as $h) {
|
||||||
|
if (preg_match('#^HTTP/\S+\s+(\d{3})#i', (string)$h, $m)) {
|
||||||
|
return (int)$m[1];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decide if a HTTP status code means the service is reachable.
|
||||||
|
* We treat 2xx/3xx as up and also 401/403 as up (service is there, but needs auth).
|
||||||
|
*/
|
||||||
|
function is_reachable_http_code(?int $code): bool
|
||||||
|
{
|
||||||
|
if ($code === null) return false;
|
||||||
|
if ($code >= 200 && $code < 400) return true;
|
||||||
|
if ($code === 401 || $code === 403) return true;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Perform a lightweight HTTP request (HEAD/GET) without following redirects.
|
||||||
* @return array{ok:bool, code:int|null, ms:int|null, error:string|null}
|
* @return array{ok:bool, code:int|null, ms:int|null, error:string|null}
|
||||||
*/
|
*/
|
||||||
function check_http_head(string $url, float $timeoutSeconds = 1.6): array
|
function check_http_request(string $url, string $method, float $timeoutSeconds = 1.6, ?string $extraHeaders = null): array
|
||||||
{
|
{
|
||||||
$start = microtime(true);
|
$start = microtime(true);
|
||||||
|
|
||||||
|
$http_response_header = null; // ensure local scope, avoid stale values
|
||||||
|
$code = null;
|
||||||
|
$error = null;
|
||||||
|
|
||||||
|
$headers = "User-Agent: ServerStatus/1.1\r\n";
|
||||||
|
if ($extraHeaders) {
|
||||||
|
$headers .= $extraHeaders;
|
||||||
|
if (!str_ends_with($headers, "\r\n")) {
|
||||||
|
$headers .= "\r\n";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
$ctx = stream_context_create([
|
$ctx = stream_context_create([
|
||||||
'http' => [
|
'http' => [
|
||||||
'method' => 'HEAD',
|
'method' => $method,
|
||||||
'timeout' => $timeoutSeconds,
|
'timeout' => $timeoutSeconds,
|
||||||
'ignore_errors' => true,
|
'ignore_errors' => true,
|
||||||
'follow_location' => 0,
|
'follow_location' => 0,
|
||||||
'max_redirects' => 0,
|
'max_redirects' => 0,
|
||||||
'header' => "User-Agent: ServerStatus/1.0\r\n",
|
'header' => $headers,
|
||||||
],
|
],
|
||||||
'ssl' => [
|
'ssl' => [
|
||||||
'verify_peer' => true,
|
'verify_peer' => true,
|
||||||
@ -106,10 +144,8 @@ function check_http_head(string $url, float $timeoutSeconds = 1.6): array
|
|||||||
],
|
],
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$code = null;
|
|
||||||
$error = null;
|
|
||||||
|
|
||||||
set_error_handler(static function (int $severity, string $message) use (&$error): bool {
|
set_error_handler(static function (int $severity, string $message) use (&$error): bool {
|
||||||
|
// capture warning as error string (e.g. connection refused, timeout, DNS fail)
|
||||||
$error = $message;
|
$error = $message;
|
||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
@ -117,22 +153,26 @@ function check_http_head(string $url, float $timeoutSeconds = 1.6): array
|
|||||||
restore_error_handler();
|
restore_error_handler();
|
||||||
|
|
||||||
if ($fp !== false) {
|
if ($fp !== false) {
|
||||||
|
// For GET we don't want to download; the Range header should keep it tiny.
|
||||||
fclose($fp);
|
fclose($fp);
|
||||||
}
|
}
|
||||||
|
|
||||||
$ms = (int)round((microtime(true) - $start) * 1000);
|
$ms = (int)round((microtime(true) - $start) * 1000);
|
||||||
|
|
||||||
// Parse status code from $http_response_header
|
|
||||||
if (isset($http_response_header) && is_array($http_response_header)) {
|
if (isset($http_response_header) && is_array($http_response_header)) {
|
||||||
foreach ($http_response_header as $h) {
|
$code = extract_http_status_code($http_response_header);
|
||||||
if (preg_match('#^HTTP/\S+\s+(\d{3})#i', (string)$h, $m)) {
|
|
||||||
$code = (int)$m[1];
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$ok = is_reachable_http_code($code);
|
||||||
|
|
||||||
|
// If we couldn't even open the stream, treat as hard failure, regardless of any weird headers.
|
||||||
|
if ($fp === false) {
|
||||||
|
$ok = false;
|
||||||
|
if ($error === null) {
|
||||||
|
$error = 'Verbindung fehlgeschlagen';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$ok = ($code !== null) && ($code >= 200 && $code < 400);
|
|
||||||
if ($code === null && $error === null) {
|
if ($code === null && $error === null) {
|
||||||
$error = 'Unbekannter Fehler';
|
$error = 'Unbekannter Fehler';
|
||||||
}
|
}
|
||||||
@ -140,6 +180,29 @@ function check_http_head(string $url, float $timeoutSeconds = 1.6): array
|
|||||||
return ['ok' => $ok, 'code' => $code, 'ms' => $ms, 'error' => $error];
|
return ['ok' => $ok, 'code' => $code, 'ms' => $ms, 'error' => $error];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* HTTP check (no redirects). Try HEAD first, then a minimal GET fallback (some servers lie on HEAD).
|
||||||
|
* @return array{ok:bool, code:int|null, ms:int|null, error:string|null}
|
||||||
|
*/
|
||||||
|
function check_http_head(string $url, float $timeoutSeconds = 1.6): array
|
||||||
|
{
|
||||||
|
$head = check_http_request($url, 'HEAD', $timeoutSeconds);
|
||||||
|
if ($head['ok']) {
|
||||||
|
return $head;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If HEAD is not supported/misconfigured, a GET with Range is a cheap reachability check.
|
||||||
|
$get = check_http_request($url, 'GET', $timeoutSeconds, "Range: bytes=0-0\r\n");
|
||||||
|
|
||||||
|
// prefer the GET result if it produced a code (more reliable)
|
||||||
|
if ($get['code'] !== null) {
|
||||||
|
return $get;
|
||||||
|
}
|
||||||
|
|
||||||
|
// otherwise fall back to the HEAD info
|
||||||
|
return $head;
|
||||||
|
}
|
||||||
|
|
||||||
function build_server_status(array $targets): array
|
function build_server_status(array $targets): array
|
||||||
{
|
{
|
||||||
$allowedHosts = [];
|
$allowedHosts = [];
|
||||||
|
|||||||
@ -16,7 +16,7 @@ require_once __DIR__ . '/includes/lib/view_helpers.php';
|
|||||||
|
|
||||||
// ── Serverstatus (mit Cache) ──────────────────────────────────────────────
|
// ── Serverstatus (mit Cache) ──────────────────────────────────────────────
|
||||||
$cacheTtlSeconds = 30;
|
$cacheTtlSeconds = 30;
|
||||||
$cacheKey = 'server_status_v1';
|
$cacheKey = 'server_status_v2';
|
||||||
$cacheFile = rtrim(sys_get_temp_dir(), DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR . $cacheKey . '.json';
|
$cacheFile = rtrim(sys_get_temp_dir(), DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR . $cacheKey . '.json';
|
||||||
|
|
||||||
$serverStatus = null;
|
$serverStatus = null;
|
||||||
|
|||||||
19
scripts/smoke_server_status.php
Normal file
19
scripts/smoke_server_status.php
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
<?php
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
require __DIR__ . '/../includes/lib/server_status.php';
|
||||||
|
|
||||||
|
$targets = [
|
||||||
|
['url' => 'https://example.com'],
|
||||||
|
['url' => 'https://example.com/does-not-exist-hopefully'],
|
||||||
|
];
|
||||||
|
|
||||||
|
foreach ($targets as $t) {
|
||||||
|
$r = check_http_head($t['url']);
|
||||||
|
echo $t['url'], " => ", ($r['ok'] ? 'UP' : 'DOWN'), " code=", ($r['code'] ?? 'null'), " ms=", ($r['ms'] ?? 'null');
|
||||||
|
if (!empty($r['error'])) {
|
||||||
|
echo " error=", $r['error'];
|
||||||
|
}
|
||||||
|
echo PHP_EOL;
|
||||||
|
}
|
||||||
|
|
||||||
Loading…
Reference in New Issue
Block a user