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:
+27
-3
@@ -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) {
|
||||||
|
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);
|
$errDetail = $result['data']['error']['message'] ?? substr($result['body'], 0, 300);
|
||||||
$send('error', ['error' => recipeText($lang, 'error_gemini_api'), 'http_code' => $httpCode, 'detail' => $errDetail]);
|
$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
@@ -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');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
Reference in New Issue
Block a user