fix: recipe errors now show specific cause instead of generic 'connection error'

- 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
This commit is contained in:
dadaloop82
2026-05-23 11:45:26 +00:00
parent 6a41b53174
commit cc0fa09219
5 changed files with 36 additions and 6 deletions
+28 -4
View File
@@ -5162,6 +5162,8 @@ function generateRecipeStream(PDO $db): void {
flush(); flush();
}; };
try {
$apiKey = env('GEMINI_API_KEY'); $apiKey = env('GEMINI_API_KEY');
if (empty($apiKey)) { $send('error', ['error' => 'no_api_key']); return; } if (empty($apiKey)) { $send('error', ['error' => 'no_api_key']); return; }
@@ -5477,13 +5479,15 @@ PROMPT;
for ($attempt = 1; $attempt <= $maxRetries; $attempt++) { for ($attempt = 1; $attempt <= $maxRetries; $attempt++) {
$retryAfterHeader = null; $retryAfterHeader = null;
$curlErrno = 0;
$curlErrMsg = '';
$ch = curl_init($url); $ch = curl_init($url);
curl_setopt_array($ch, [ curl_setopt_array($ch, [
CURLOPT_POST => true, CURLOPT_POST => true,
CURLOPT_POSTFIELDS => json_encode($payload), CURLOPT_POSTFIELDS => json_encode($payload),
CURLOPT_HTTPHEADER => ['Content-Type: application/json'], CURLOPT_HTTPHEADER => ['Content-Type: application/json'],
CURLOPT_RETURNTRANSFER => true, CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 60, CURLOPT_TIMEOUT => 90,
CURLOPT_HEADERFUNCTION => function ($ch, $header) use (&$retryAfterHeader) { CURLOPT_HEADERFUNCTION => function ($ch, $header) use (&$retryAfterHeader) {
if (stripos($header, 'retry-after:') === 0) { if (stripos($header, 'retry-after:') === 0) {
$val = intval(trim(substr($header, strlen('retry-after:')))); $val = intval(trim(substr($header, strlen('retry-after:'))));
@@ -5495,8 +5499,12 @@ PROMPT;
$body = curl_exec($ch); $body = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
if ($body === false) {
$curlErrno = curl_errno($ch);
$curlErrMsg = curl_error($ch);
$body = '';
}
curl_close($ch); curl_close($ch);
if ($body === false) $body = '';
$result = [ $result = [
'http_code' => $httpCode, 'http_code' => $httpCode,
@@ -5539,8 +5547,16 @@ PROMPT;
} }
if ($httpCode !== 200) { if ($httpCode !== 200) {
$errDetail = $result['data']['error']['message'] ?? substr($result['body'], 0, 300); if ($httpCode === 0) {
$send('error', ['error' => recipeText($lang, 'error_gemini_api'), 'http_code' => $httpCode, 'detail' => $errDetail]); // 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; return;
} }
@@ -5683,6 +5699,14 @@ PROMPT;
$send('status', ['step' => 4, 'message' => '✅ Ricetta pronta!']); $send('status', ['step' => 4, 'message' => '✅ Ricetta pronta!']);
$send('recipe', ['recipe' => $recipe]); $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 ===== // ===== GEMINI AI PRODUCT IDENTIFICATION =====
+5 -2
View File
@@ -14443,7 +14443,8 @@ async function generateRecipe() {
showToast((errorEvent.error || t('recipes.generate_error')) + detail, 'error'); showToast((errorEvent.error || t('recipes.generate_error')) + detail, 'error');
} }
} else { } 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); console.error('Recipe error:', err);
document.getElementById('recipe-loading').style.display = 'none'; document.getElementById('recipe-loading').style.display = 'none';
document.getElementById('recipe-ask').style.display = ''; 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');
} }
} }
+1
View File
@@ -367,6 +367,7 @@
"steps_title": "👨‍🍳 Zubereitung", "steps_title": "👨‍🍳 Zubereitung",
"no_steps": "Keine Zubereitungsschritte verfügbar", "no_steps": "Keine Zubereitungsschritte verfügbar",
"generate_error": "Fehler bei der Generierung", "generate_error": "Fehler bei der Generierung",
"stream_interrupted": "Generierung unterbrochen (unvollstaendige Antwort vom Server). Protokolle pruefen oder erneut versuchen.",
"persons_short": "Pers.", "persons_short": "Pers.",
"use_ingredient_title": "Zutat verwenden", "use_ingredient_title": "Zutat verwenden",
"recipe_qty_label": "Rezept", "recipe_qty_label": "Rezept",
+1
View File
@@ -367,6 +367,7 @@
"steps_title": "👨‍🍳 Steps", "steps_title": "👨‍🍳 Steps",
"no_steps": "No steps available", "no_steps": "No steps available",
"generate_error": "Generation error", "generate_error": "Generation error",
"stream_interrupted": "Generation interrupted (incomplete server response). Check logs or try again.",
"persons_short": "serv.", "persons_short": "serv.",
"use_ingredient_title": "Use ingredient", "use_ingredient_title": "Use ingredient",
"recipe_qty_label": "Recipe", "recipe_qty_label": "Recipe",
+1
View File
@@ -367,6 +367,7 @@
"steps_title": "👨‍🍳 Procedimento", "steps_title": "👨‍🍳 Procedimento",
"no_steps": "Nessun procedimento disponibile", "no_steps": "Nessun procedimento disponibile",
"generate_error": "Errore nella generazione", "generate_error": "Errore nella generazione",
"stream_interrupted": "Generazione interrotta (risposta incompleta dal server). Controlla i log o riprova.",
"persons_short": "pers.", "persons_short": "pers.",
"use_ingredient_title": "Usa ingrediente", "use_ingredient_title": "Usa ingrediente",
"recipe_qty_label": "Ricetta", "recipe_qty_label": "Ricetta",