From cc0fa092196f2f5af0d42c0ecec5aed88973e8cc Mon Sep 17 00:00:00 2001 From: dadaloop82 Date: Sat, 23 May 2026 11:45:26 +0000 Subject: [PATCH] fix: recipe errors now show specific cause instead of generic 'connection error' MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - PHP generateRecipeStream: wrap entire body in try/catch(\Throwable) to catch any PHP fatal/exception mid-stream and send it as a proper SSE error event - PHP: curl timeout raised 60sβ†’90s; capture curl errno/errmsg on failure - PHP: HTTP error messages now include a human-readable status label (e.g. 'Quota API esaurita (429)', 'Nessuna risposta da Gemini (cURL: ...)') - JS catch block: show err.message alongside error.connection so the actual JS network error (NetworkError, AbortError, etc.) is visible - JS no-recipe+no-error path: show recipes.stream_interrupted instead of generic error.connection - Translation: added recipes.stream_interrupted in it/en/de --- api/index.php | 32 ++++++++++++++++++++++++++++---- assets/js/app.js | 7 +++++-- translations/de.json | 1 + translations/en.json | 1 + translations/it.json | 1 + 5 files changed, 36 insertions(+), 6 deletions(-) diff --git a/api/index.php b/api/index.php index e529690..fec784a 100644 --- a/api/index.php +++ b/api/index.php @@ -5162,6 +5162,8 @@ function generateRecipeStream(PDO $db): void { flush(); }; + try { + $apiKey = env('GEMINI_API_KEY'); if (empty($apiKey)) { $send('error', ['error' => 'no_api_key']); return; } @@ -5477,13 +5479,15 @@ PROMPT; for ($attempt = 1; $attempt <= $maxRetries; $attempt++) { $retryAfterHeader = null; + $curlErrno = 0; + $curlErrMsg = ''; $ch = curl_init($url); curl_setopt_array($ch, [ CURLOPT_POST => true, CURLOPT_POSTFIELDS => json_encode($payload), CURLOPT_HTTPHEADER => ['Content-Type: application/json'], CURLOPT_RETURNTRANSFER => true, - CURLOPT_TIMEOUT => 60, + CURLOPT_TIMEOUT => 90, CURLOPT_HEADERFUNCTION => function ($ch, $header) use (&$retryAfterHeader) { if (stripos($header, 'retry-after:') === 0) { $val = intval(trim(substr($header, strlen('retry-after:')))); @@ -5495,8 +5499,12 @@ PROMPT; $body = curl_exec($ch); $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); + if ($body === false) { + $curlErrno = curl_errno($ch); + $curlErrMsg = curl_error($ch); + $body = ''; + } curl_close($ch); - if ($body === false) $body = ''; $result = [ 'http_code' => $httpCode, @@ -5539,8 +5547,16 @@ PROMPT; } if ($httpCode !== 200) { - $errDetail = $result['data']['error']['message'] ?? substr($result['body'], 0, 300); - $send('error', ['error' => recipeText($lang, 'error_gemini_api'), 'http_code' => $httpCode, 'detail' => $errDetail]); + if ($httpCode === 0) { + // cURL-level failure: timeout, DNS, network down + $curlLabel = $curlErrMsg ?: "cURL errno {$curlErrno}"; + $send('error', ['error' => recipeText($lang, 'error_gemini_api'), 'http_code' => 0, 'detail' => "Nessuna risposta da Gemini ({$curlLabel}) β€” verifica la connessione del server o riprova tra qualche istante."]); + } else { + $errDetail = $result['data']['error']['message'] ?? substr($result['body'], 0, 300); + $statusLabels = [429 => 'Quota API esaurita (429)', 503 => 'Servizio Gemini non disponibile (503)', 401 => 'API key non valida (401)', 403 => 'API key non autorizzata (403)', 500 => 'Errore interno Gemini (500)']; + $statusLabel = $statusLabels[$httpCode] ?? "HTTP {$httpCode}"; + $send('error', ['error' => recipeText($lang, 'error_gemini_api'), 'http_code' => $httpCode, 'detail' => "{$statusLabel}" . ($errDetail ? ": {$errDetail}" : '')]); + } return; } @@ -5683,6 +5699,14 @@ PROMPT; $send('status', ['step' => 4, 'message' => 'βœ… Ricetta pronta!']); $send('recipe', ['recipe' => $recipe]); + + } catch (\Throwable $e) { + EverLog::error('generateRecipeStream fatal: ' . $e->getMessage() . ' in ' . $e->getFile() . ':' . $e->getLine()); + $send('error', [ + 'error' => 'Errore interno del server', + 'detail' => $e->getMessage() . ' (' . basename($e->getFile()) . ':' . $e->getLine() . ')', + ]); + } } // ===== GEMINI AI PRODUCT IDENTIFICATION ===== diff --git a/assets/js/app.js b/assets/js/app.js index b4ae793..58de99b 100644 --- a/assets/js/app.js +++ b/assets/js/app.js @@ -14443,7 +14443,8 @@ async function generateRecipe() { showToast((errorEvent.error || t('recipes.generate_error')) + detail, 'error'); } } else { - showToast(t('error.connection'), 'error'); + // Stream closed without recipe or error event β€” likely a server crash mid-stream + showToast(t('recipes.stream_interrupted'), 'error'); } } @@ -14451,7 +14452,9 @@ async function generateRecipe() { console.error('Recipe error:', err); document.getElementById('recipe-loading').style.display = 'none'; document.getElementById('recipe-ask').style.display = ''; - showToast(t('error.connection'), 'error'); + // Show the actual JS error (e.g. NetworkError, AbortError, TypeError) + const errMsg = err?.message || String(err); + showToast(`${t('error.connection')}: ${errMsg}`, 'error'); } } diff --git a/translations/de.json b/translations/de.json index e65cc23..9722038 100644 --- a/translations/de.json +++ b/translations/de.json @@ -367,6 +367,7 @@ "steps_title": "πŸ‘¨β€πŸ³ Zubereitung", "no_steps": "Keine Zubereitungsschritte verfΓΌgbar", "generate_error": "Fehler bei der Generierung", + "stream_interrupted": "Generierung unterbrochen (unvollstaendige Antwort vom Server). Protokolle pruefen oder erneut versuchen.", "persons_short": "Pers.", "use_ingredient_title": "Zutat verwenden", "recipe_qty_label": "Rezept", diff --git a/translations/en.json b/translations/en.json index e2f7004..cedcf46 100644 --- a/translations/en.json +++ b/translations/en.json @@ -367,6 +367,7 @@ "steps_title": "πŸ‘¨β€πŸ³ Steps", "no_steps": "No steps available", "generate_error": "Generation error", + "stream_interrupted": "Generation interrupted (incomplete server response). Check logs or try again.", "persons_short": "serv.", "use_ingredient_title": "Use ingredient", "recipe_qty_label": "Recipe", diff --git a/translations/it.json b/translations/it.json index 90eb339..d9b1cd3 100644 --- a/translations/it.json +++ b/translations/it.json @@ -367,6 +367,7 @@ "steps_title": "πŸ‘¨β€πŸ³ Procedimento", "no_steps": "Nessun procedimento disponibile", "generate_error": "Errore nella generazione", + "stream_interrupted": "Generazione interrotta (risposta incompleta dal server). Controlla i log o riprova.", "persons_short": "pers.", "use_ingredient_title": "Usa ingrediente", "recipe_qty_label": "Ricetta",