api.php: fault-isolated dispatch — ob_start+catch(Throwable) per endpoint

A ParseError or fatal in any endpoint file now returns JSON 500 for that
endpoint only. switch replaced with data-driven map. All other endpoints
continue working normally when one is broken.
This commit is contained in:
2026-06-11 20:49:52 +00:00
parent 6abff8dd56
commit 2767a858dd
+69 -83
View File
@@ -1,6 +1,8 @@
<?php <?php
/** /**
* JARVIS API Router * JARVIS API Router — fault-isolated per endpoint
* A ParseError or fatal in any endpoint file returns JSON 500 for that
* endpoint only; all other endpoints continue to work normally.
*/ */
require_once __DIR__ . '/../api/config.php'; require_once __DIR__ . '/../api/config.php';
require_once __DIR__ . '/../api/lib/db.php'; require_once __DIR__ . '/../api/lib/db.php';
@@ -15,22 +17,22 @@ header('Access-Control-Allow-Headers: Content-Type, X-Session-Token');
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') { http_response_code(204); exit; } if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') { http_response_code(204); exit; }
$uri = $_SERVER['REQUEST_URI'] ?? '/'; $uri = $_SERVER['REQUEST_URI'] ?? '/';
$method = $_SERVER['REQUEST_METHOD']; $method = $_SERVER['REQUEST_METHOD'];
$path = trim(parse_url($uri, PHP_URL_PATH), '/'); $path = trim(parse_url($uri, PHP_URL_PATH), '/');
$parts = explode('/', $path); $parts = explode('/', $path);
// Remove 'api' prefix if present
if (($parts[0] ?? '') === 'api') array_shift($parts); if (($parts[0] ?? '') === 'api') array_shift($parts);
$endpoint = $parts[0] ?? ''; $endpoint = $parts[0] ?? '';
$action = $parts[1] ?? ''; $action = $parts[1] ?? '';
// Auth check (except login and ping) // ── Auth check (skip for auth / agent / netscan) ──────────────────────
if ($endpoint !== 'auth' && $endpoint !== 'agent' && $endpoint !== 'netscan') { if (!\in_array($endpoint, ['auth', 'agent', 'netscan'], true)) {
$token = $_SESSION['jarvis_token'] ?? ($_SERVER['HTTP_X_SESSION_TOKEN'] ?? ''); $token = $_SESSION['jarvis_token'] ?? ($_SERVER['HTTP_X_SESSION_TOKEN'] ?? '');
if (empty($token) || $token !== ($_SESSION['jarvis_token'] ?? '')) { $isValid = !empty($token) && $token === ($_SESSION['jarvis_token'] ?? '');
$localIP = $_SERVER['REMOTE_ADDR'] ?? ''; if (!$isValid) {
$isLocal = in_array($localIP, ['127.0.0.1', '::1', JARVIS_IP]); $ip = $_SERVER['REMOTE_ADDR'] ?? '';
$isLocal = \in_array($ip, ['127.0.0.1', '::1', JARVIS_IP], true);
if (!$isLocal && $endpoint !== 'ping') { if (!$isLocal && $endpoint !== 'ping') {
http_response_code(401); http_response_code(401);
echo json_encode(['error' => 'Unauthorized', 'code' => 401]); echo json_encode(['error' => 'Unauthorized', 'code' => 401]);
@@ -39,79 +41,63 @@ if ($endpoint !== 'auth' && $endpoint !== 'agent' && $endpoint !== 'netscan') {
} }
} }
if ($endpoint !== 'auth') session_write_close(); // Skip for auth so login can write session token if ($endpoint !== 'auth') session_write_close();
$body = file_get_contents('php://input'); $body = file_get_contents('php://input');
$data = json_decode($body, true) ?? []; $data = json_decode($body, true) ?? [];
switch ($endpoint) { // ── Fast ping (no file dispatch needed) ──────────────────────────────
case 'ping': if ($endpoint === 'ping') {
echo json_encode(['status' => 'online', 'time' => date('c'), 'codename' => JARVIS_CODENAME]); echo json_encode(['status' => 'online', 'time' => date('c'), 'codename' => JARVIS_CODENAME]);
break; exit;
case 'auth': }
require __DIR__ . '/../api/endpoints/auth.php';
break; // ── Endpoint → file map ───────────────────────────────────────────────
case 'chat': $endpoints = [
require __DIR__ . '/../api/endpoints/chat.php'; 'auth' => 'auth.php',
break; 'chat' => 'chat.php',
case 'system': 'system' => 'system.php',
require __DIR__ . '/../api/endpoints/system.php'; 'netscan' => 'netscan.php',
break; 'network' => 'network.php',
case 'netscan': 'proxmox' => 'proxmox.php',
require __DIR__ . '/../api/endpoints/netscan.php'; 'ha' => 'ha.php',
break; 'tts' => 'tts.php',
case 'network': 'email' => 'email.php',
require __DIR__ . '/../api/endpoints/network.php'; 'do' => 'do_server.php',
break; 'alerts' => 'alerts.php',
case 'proxmox': 'facts' => 'facts_collector.php',
require __DIR__ . '/../api/endpoints/proxmox.php'; 'weather' => 'weather.php',
break; 'news' => 'news.php',
case 'ha': 'sites' => 'sites.php',
require __DIR__ . '/../api/endpoints/ha.php'; 'agent' => 'agent.php',
break; 'planner' => 'planner.php',
case 'tts': 'arc' => 'arc.php',
require __DIR__ . '/../api/endpoints/tts.php'; 'directives' => 'directives.php',
break; 'memory' => 'memory.php',
case 'email': 'calendar' => 'calendar_sync.php',
require __DIR__ . '/../api/endpoints/email.php'; ];
break;
case 'do': if (!isset($endpoints[$endpoint])) {
require __DIR__ . '/../api/endpoints/do_server.php'; http_response_code(404);
break; echo json_encode(['error' => 'Unknown endpoint: ' . $endpoint]);
case 'alerts': exit;
require __DIR__ . '/../api/endpoints/alerts.php'; }
break;
case 'facts': $file = __DIR__ . '/../api/endpoints/' . $endpoints[$endpoint];
require __DIR__ . '/../api/endpoints/facts_collector.php';
break; // ── Fault-isolated dispatch ───────────────────────────────────────────
case 'weather': // ob_start() buffers any partial output so a mid-execution fatal doesn't
require __DIR__ . '/../api/endpoints/weather.php'; // send a broken response. catch(Throwable) catches ParseError, TypeError,
break; // and all other Errors + Exceptions in PHP 7+.
case 'news': ob_start();
require __DIR__ . '/../api/endpoints/news.php'; try {
break; require $file;
case 'sites': ob_end_flush();
require __DIR__ . '/../api/endpoints/sites.php'; } catch (\Throwable $e) {
break; ob_end_clean();
case "agent": http_response_code(500);
require __DIR__ . '/../api/endpoints/agent.php'; echo json_encode(['error' => 'Endpoint unavailable', 'endpoint' => $endpoint, 'code' => 500]);
break; error_log(sprintf('JARVIS API [%s] %s: %s in %s:%d',
case "planner": $endpoint, get_class($e), $e->getMessage(), $e->getFile(), $e->getLine()
require __DIR__ . '/../api/endpoints/planner.php'; ));
break;
case "arc":
require __DIR__ . "/../api/endpoints/arc.php";
break;
case "directives":
require __DIR__ . "/../api/endpoints/directives.php";
break;
case "memory":
require __DIR__ . "/../api/endpoints/memory.php";
break;
case "calendar":
require __DIR__ . '/../api/endpoints/calendar_sync.php';
break;
default:
http_response_code(404);
echo json_encode(['error' => 'Unknown endpoint: ' . $endpoint]);
} }