fix sse error handling if shop api error part 3
This commit is contained in:
10
README_ERROR_VISIBILITY_PATCH.md
Normal file
10
README_ERROR_VISIBILITY_PATCH.md
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
# RetrieX error visibility patch
|
||||||
|
|
||||||
|
This patch makes Shopware Store API authentication/configuration failures visible in the chat stream and improves client-side display of SSE/job errors.
|
||||||
|
|
||||||
|
Changed files:
|
||||||
|
- src/Shopware/StoreApiClient.php
|
||||||
|
- src/Shopware/StoreApiException.php
|
||||||
|
- src/Agent/AgentRunner.php
|
||||||
|
- src/Controller/AskSseController.php
|
||||||
|
- public/assets/js/base.js
|
||||||
@@ -69,8 +69,28 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
) !== null;
|
) !== null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isWhitespaceTextNode(node) {
|
||||||
|
return node.nodeType === Node.TEXT_NODE && (node.textContent || '').trim() === '';
|
||||||
|
}
|
||||||
|
|
||||||
|
function isBreakNode(node) {
|
||||||
|
return node.nodeType === Node.ELEMENT_NODE && node.tagName === 'BR';
|
||||||
|
}
|
||||||
|
|
||||||
|
function trimEdgeBreaks(element) {
|
||||||
|
while (element.firstChild && (isWhitespaceTextNode(element.firstChild) || isBreakNode(element.firstChild))) {
|
||||||
|
element.firstChild.remove();
|
||||||
|
}
|
||||||
|
|
||||||
|
while (element.lastChild && (isWhitespaceTextNode(element.lastChild) || isBreakNode(element.lastChild))) {
|
||||||
|
element.lastChild.remove();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function cleanupEmptyBlocks(container) {
|
function cleanupEmptyBlocks(container) {
|
||||||
container.querySelectorAll('p, div, li, blockquote').forEach((el) => {
|
container.querySelectorAll('p, div, li, blockquote').forEach((el) => {
|
||||||
|
trimEdgeBreaks(el);
|
||||||
|
|
||||||
const html = el.innerHTML
|
const html = el.innerHTML
|
||||||
.replace(/<br\s*\/?>/gi, '')
|
.replace(/<br\s*\/?>/gi, '')
|
||||||
.replace(/ /gi, '')
|
.replace(/ /gi, '')
|
||||||
@@ -87,6 +107,8 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
el.remove();
|
el.remove();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
trimEdgeBreaks(container);
|
||||||
}
|
}
|
||||||
|
|
||||||
function removeThinkSpansOnly(container) {
|
function removeThinkSpansOnly(container) {
|
||||||
@@ -165,6 +187,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
const thinkSpans = Array.from(container.querySelectorAll('.think'));
|
const thinkSpans = Array.from(container.querySelectorAll('.think'));
|
||||||
|
|
||||||
if (thinkSpans.length === 0) {
|
if (thinkSpans.length === 0) {
|
||||||
|
cleanupEmptyBlocks(container);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -350,7 +373,8 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
firstChunk = false;
|
firstChunk = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
raw += `\n\n<em>${safeMessage}</em>`;
|
const formattedMessage = `<em>${safeMessage}</em>`;
|
||||||
|
raw += raw.trim() === '' ? formattedMessage : `\n\n${formattedMessage}`;
|
||||||
finalizeStream(bubble, raw);
|
finalizeStream(bubble, raw);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -486,7 +510,8 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
const userMessage = 'Die Verbindung zum Antwort-Stream wurde unterbrochen. Bitte sende die Anfrage erneut, falls die Antwort unvollständig ist.';
|
const userMessage = 'Die Verbindung zum Antwort-Stream wurde unterbrochen. Bitte sende die Anfrage erneut, falls die Antwort unvollständig ist.';
|
||||||
|
|
||||||
if (raw.trim() !== '') {
|
if (raw.trim() !== '') {
|
||||||
raw += `\n\n<em>${userMessage}</em>`;
|
const formattedMessage = `<em>${userMessage}</em>`;
|
||||||
|
raw += raw.trim() === '' ? formattedMessage : `\n\n${formattedMessage}`;
|
||||||
renderBubbleContent(bubble, raw);
|
renderBubbleContent(bubble, raw);
|
||||||
} else {
|
} else {
|
||||||
bubble.innerHTML = `<em>${userMessage}</em>`;
|
bubble.innerHTML = `<em>${userMessage}</em>`;
|
||||||
|
|||||||
@@ -156,7 +156,7 @@ final readonly class AgentRunner
|
|||||||
|
|
||||||
yield $this->systemMsg(
|
yield $this->systemMsg(
|
||||||
$this->buildShopUnavailableMessage($primaryShopSearchFailureReason),
|
$this->buildShopUnavailableMessage($primaryShopSearchFailureReason),
|
||||||
'info'
|
'err'
|
||||||
);
|
);
|
||||||
|
|
||||||
$repairPayload = [
|
$repairPayload = [
|
||||||
@@ -811,12 +811,21 @@ final readonly class AgentRunner
|
|||||||
|
|
||||||
private function buildUserErrorMessage(Throwable $e): string
|
private function buildUserErrorMessage(Throwable $e): string
|
||||||
{
|
{
|
||||||
|
$message = trim($e->getMessage());
|
||||||
|
|
||||||
|
if ($message === '') {
|
||||||
|
$message = $e::class;
|
||||||
|
}
|
||||||
|
|
||||||
|
$safeMessage = htmlspecialchars($message, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
|
||||||
|
|
||||||
if (!$this->debug) {
|
if (!$this->debug) {
|
||||||
return $this->agentRunnerConfig->getGenericInternalErrorMessage();
|
return $this->agentRunnerConfig->getGenericInternalErrorMessage()
|
||||||
|
. '<br><small>Technischer Fehler: ' . $safeMessage . '</small>';
|
||||||
}
|
}
|
||||||
|
|
||||||
return $this->agentRunnerConfig->getDebugInternalErrorPrefix()
|
return $this->agentRunnerConfig->getDebugInternalErrorPrefix()
|
||||||
. htmlspecialchars($e->getMessage(), ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
|
. $safeMessage;
|
||||||
}
|
}
|
||||||
|
|
||||||
private function badge(string $label): string
|
private function badge(string $label): string
|
||||||
|
|||||||
@@ -55,9 +55,9 @@ final readonly class AskSseController
|
|||||||
'includeFullContext' => $includeFullContext,
|
'includeFullContext' => $includeFullContext,
|
||||||
'createdAt' => time(),
|
'createdAt' => time(),
|
||||||
]);
|
]);
|
||||||
} catch (\Throwable) {
|
} catch (\Throwable $e) {
|
||||||
return new JsonResponse(
|
return new JsonResponse(
|
||||||
['error' => 'Stream job could not be created.'],
|
['error' => 'Stream job could not be created: ' . $this->formatThrowableForClient($e)],
|
||||||
Response::HTTP_INTERNAL_SERVER_ERROR
|
Response::HTTP_INTERNAL_SERVER_ERROR
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -139,6 +139,7 @@ final readonly class AskSseController
|
|||||||
?Response $cookieResponse
|
?Response $cookieResponse
|
||||||
): void {
|
): void {
|
||||||
$this->prepareStreamRuntime();
|
$this->prepareStreamRuntime();
|
||||||
|
$this->registerStreamShutdownErrorHandler();
|
||||||
|
|
||||||
if ($cookieResponse !== null) {
|
if ($cookieResponse !== null) {
|
||||||
foreach ($cookieResponse->headers->getCookies() as $cookie) {
|
foreach ($cookieResponse->headers->getCookies() as $cookie) {
|
||||||
@@ -167,13 +168,51 @@ final readonly class AskSseController
|
|||||||
} catch (\Throwable $e) {
|
} catch (\Throwable $e) {
|
||||||
$this->sendEvent(
|
$this->sendEvent(
|
||||||
'error',
|
'error',
|
||||||
'❌ Stream abgebrochen: ' . $e->getMessage()
|
'❌ Stream abgebrochen: ' . $this->formatThrowableForClient($e)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->sendEvent('done', '[DONE]');
|
$this->sendEvent('done', '[DONE]');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function registerStreamShutdownErrorHandler(): void
|
||||||
|
{
|
||||||
|
register_shutdown_function(function (): void {
|
||||||
|
$error = error_get_last();
|
||||||
|
|
||||||
|
if ($error === null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$fatalTypes = [E_ERROR, E_PARSE, E_CORE_ERROR, E_COMPILE_ERROR, E_USER_ERROR];
|
||||||
|
|
||||||
|
if (!in_array((int) ($error['type'] ?? 0), $fatalTypes, true)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$message = sprintf(
|
||||||
|
'❌ Fataler Serverfehler: %s in %s:%s',
|
||||||
|
(string) ($error['message'] ?? 'unknown error'),
|
||||||
|
(string) ($error['file'] ?? 'unknown file'),
|
||||||
|
(string) ($error['line'] ?? '?')
|
||||||
|
);
|
||||||
|
|
||||||
|
$this->sendEvent('error', $message);
|
||||||
|
$this->sendEvent('done', '[DONE]');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private function formatThrowableForClient(\Throwable $e): string
|
||||||
|
{
|
||||||
|
$message = trim($e->getMessage());
|
||||||
|
|
||||||
|
if ($message === '') {
|
||||||
|
$message = $e::class;
|
||||||
|
}
|
||||||
|
|
||||||
|
return preg_replace('/\s+/u', ' ', $message) ?? $message;
|
||||||
|
}
|
||||||
|
|
||||||
private function prepareStreamRuntime(): void
|
private function prepareStreamRuntime(): void
|
||||||
{
|
{
|
||||||
@set_time_limit(0);
|
@set_time_limit(0);
|
||||||
|
|||||||
@@ -83,19 +83,32 @@ final readonly class StoreApiClient
|
|||||||
{
|
{
|
||||||
$preview = mb_substr(trim($content), 0, 1000);
|
$preview = mb_substr(trim($content), 0, 1000);
|
||||||
$utf8Failure = $this->containsUtf8FailureSignal($preview);
|
$utf8Failure = $this->containsUtf8FailureSignal($preview);
|
||||||
$serverFailure = $statusCode >= 500;
|
$normalizedPreview = mb_strtolower($preview, 'UTF-8');
|
||||||
|
$accessKeyFailure = $statusCode === 401
|
||||||
|
|| $statusCode === 403
|
||||||
|
|| str_contains($preview, 'FRAMEWORK__API_INVALID_ACCESS_KEY')
|
||||||
|
|| str_contains($normalizedPreview, 'access key is invalid');
|
||||||
|
$serverFailure = $statusCode >= 500 || $accessKeyFailure;
|
||||||
|
|
||||||
|
$hint = '';
|
||||||
|
|
||||||
|
if ($accessKeyFailure) {
|
||||||
|
$hint = ' Hint: The configured Shopware Sales Channel access key is invalid or does not match the Store API endpoint.';
|
||||||
|
} elseif ($utf8Failure) {
|
||||||
|
$hint = ' Hint: The request body was valid JSON; this Shopware error usually means Shopware failed while encoding response/product data as UTF-8.';
|
||||||
|
}
|
||||||
|
|
||||||
return new StoreApiException(
|
return new StoreApiException(
|
||||||
sprintf(
|
sprintf(
|
||||||
'Shopware Store API request failed with status %d. Response: %s%s',
|
'Shopware Store API request failed with status %d. Response: %s%s',
|
||||||
$statusCode,
|
$statusCode,
|
||||||
$preview,
|
$preview,
|
||||||
$utf8Failure ? ' Hint: The request body was valid JSON; this Shopware error usually means Shopware failed while encoding response/product data as UTF-8.' : ''
|
$hint
|
||||||
),
|
),
|
||||||
$statusCode,
|
$statusCode,
|
||||||
$serverFailure,
|
$serverFailure,
|
||||||
$utf8Failure,
|
$utf8Failure,
|
||||||
$serverFailure || $utf8Failure
|
$serverFailure || $utf8Failure || $accessKeyFailure
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -42,6 +42,10 @@ final class StoreApiException extends RuntimeException
|
|||||||
|
|
||||||
public function isSystemFailure(): bool
|
public function isSystemFailure(): bool
|
||||||
{
|
{
|
||||||
return $this->serverFailure || $this->utf8Failure;
|
if ($this->serverFailure || $this->utf8Failure) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->statusCode === 401 || $this->statusCode === 403;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user