fix sse error handling if shop api error part 1

This commit is contained in:
team2
2026-04-25 20:16:40 +02:00
parent 93ff6262cc
commit cf970d2b49
3 changed files with 204 additions and 70 deletions

View File

@@ -383,6 +383,18 @@ final class ShopSearchService
if ($safeResults !== null) { if ($safeResults !== null) {
return $safeResults; return $safeResults;
} }
$minimalResults = $this->retryWithMinimalCriteria(
query: $query,
commerceIntent: $commerceIntent,
originalPrompt: $originalPrompt,
usesHistoryContext: $usesHistoryContext,
previousException: $e
);
if ($minimalResults !== null) {
return $minimalResults;
}
} }
$this->recordFailedSearch($e); $this->recordFailedSearch($e);
@@ -458,6 +470,61 @@ final class ShopSearchService
} }
} }
/**
* @return ShopProductResult[]|null
*/
private function retryWithMinimalCriteria(
CommerceSearchQuery $query,
string $commerceIntent,
string $originalPrompt,
bool $usesHistoryContext,
StoreApiException $previousException
): ?array {
$this->logger->warning('Shop search retrying with minimal UTF-8-safe criteria', [
'commerceIntent' => $commerceIntent,
'originalPrompt' => $originalPrompt,
'normalizedPrompt' => $query->normalizedPrompt,
'searchText' => $query->searchText,
'usesHistoryContext' => $usesHistoryContext,
'previousStatusCode' => $previousException->getStatusCode(),
'previousUtf8Failure' => $previousException->isUtf8Failure(),
'previousExceptionMessage' => $previousException->getMessage(),
]);
try {
$minimalCriteria = $this->criteriaBuilder->buildMinimal($query, $this->maxResults);
$response = $this->storeApiClient->searchProducts($minimalCriteria);
return $this->mapAndLogSearchResponse(
response: $response,
query: $query,
commerceIntent: $commerceIntent,
originalPrompt: $originalPrompt,
usesHistoryContext: $usesHistoryContext,
usedSafeCriteria: true
);
} catch (
ClientExceptionInterface |
RedirectionExceptionInterface |
ServerExceptionInterface |
TransportExceptionInterface |
\RuntimeException $minimalException
) {
$this->recordFailedSearch($minimalException);
$this->logger->warning('Shop search minimal UTF-8-safe criteria retry failed', [
'commerceIntent' => $commerceIntent,
'originalPrompt' => $originalPrompt,
'normalizedPrompt' => $query->normalizedPrompt,
'searchText' => $query->searchText,
'usesHistoryContext' => $usesHistoryContext,
'exceptionClass' => $minimalException::class,
'exceptionMessage' => $minimalException->getMessage(),
]);
return null;
}
}
/** /**
* @param array<mixed> $response * @param array<mixed> $response
* @return ShopProductResult[] * @return ShopProductResult[]

View File

@@ -36,7 +36,32 @@ final class ShopwareCriteriaBuilder
query: $query, query: $query,
limit: $limit, limit: $limit,
grouping: $grouping, grouping: $grouping,
includeRichTextFields: false includeRichTextFields: false,
includeMediaFields: false,
includeSeoUrls: true,
includeManufacturer: true
);
}
/**
* Builds the smallest useful Store API payload for UTF-8 recovery.
* It intentionally omits rich text, custom fields and media metadata,
* because those fields are the most common source of invalid response encoding.
*/
public function buildMinimal(
CommerceSearchQuery $query,
?int $limit = 25,
?bool $grouping = true
): array
{
return $this->buildCriteria(
query: $query,
limit: $limit,
grouping: $grouping,
includeRichTextFields: false,
includeMediaFields: false,
includeSeoUrls: true,
includeManufacturer: false
); );
} }
@@ -44,7 +69,10 @@ final class ShopwareCriteriaBuilder
CommerceSearchQuery $query, CommerceSearchQuery $query,
?int $limit, ?int $limit,
?bool $grouping, ?bool $grouping,
bool $includeRichTextFields bool $includeRichTextFields,
bool $includeMediaFields = true,
bool $includeSeoUrls = true,
bool $includeManufacturer = true
): array { ): array {
$productIncludes = [ $productIncludes = [
'id', 'id',
@@ -52,26 +80,28 @@ final class ShopwareCriteriaBuilder
'productNumber', 'productNumber',
'available', 'available',
'calculatedPrice', 'calculatedPrice',
'seoUrls',
'manufacturer',
'translated.name', 'translated.name',
'cover',
]; ];
if ($includeSeoUrls) {
$productIncludes[] = 'seoUrls';
}
if ($includeManufacturer) {
$productIncludes[] = 'manufacturer';
}
if ($includeMediaFields) {
$productIncludes[] = 'cover';
}
if ($includeRichTextFields) { if ($includeRichTextFields) {
$productIncludes[] = 'description'; $productIncludes[] = 'description';
$productIncludes[] = 'customFields'; $productIncludes[] = 'customFields';
} }
$criteria = [ $includes = [
'page' => 1,
'limit' => max(1, $limit),
'total-count-mode' => 0,
'includes' => [
'product' => $productIncludes, 'product' => $productIncludes,
'product_manufacturer' => [
'name',
],
'calculated_price' => [ 'calculated_price' => [
'unitPrice', 'unitPrice',
'totalPrice', 'totalPrice',
@@ -79,25 +109,46 @@ final class ShopwareCriteriaBuilder
'listPrice', 'listPrice',
'regulationPrice' 'regulationPrice'
], ],
'seo_url' => [ ];
if ($includeManufacturer) {
$includes['product_manufacturer'] = [
'name',
];
}
if ($includeSeoUrls) {
$includes['seo_url'] = [
'seoPathInfo', 'seoPathInfo',
], ];
'product_media' => [ }
if ($includeMediaFields) {
$includes['product_media'] = [
'id', 'id',
'media' 'media'
], ];
'media' => [ $includes['media'] = [
'id', 'id',
'url', 'url',
'thumbnails', 'thumbnails',
'alt', 'alt',
'title' 'title'
] ];
], }
'associations' => [
'manufacturer' => new \stdClass(), $associations = [];
'seoUrls' => new \stdClass(),
'cover' => [ if ($includeManufacturer) {
$associations['manufacturer'] = new \stdClass();
}
if ($includeSeoUrls) {
$associations['seoUrls'] = new \stdClass();
}
if ($includeMediaFields) {
$associations['cover'] = [
'associations' => [ 'associations' => [
'media' => [ 'media' => [
'associations' => [ 'associations' => [
@@ -105,8 +156,15 @@ final class ShopwareCriteriaBuilder
] ]
] ]
] ]
] ];
] }
$criteria = [
'page' => 1,
'limit' => max(1, $limit),
'total-count-mode' => 0,
'includes' => $includes,
'associations' => $associations,
]; ];
if ($grouping) { if ($grouping) {

View File

@@ -33,13 +33,13 @@ final readonly class StoreApiClient
$url = rtrim($this->baseUrl, '/') . '/store-api/search'; $url = rtrim($this->baseUrl, '/') . '/store-api/search';
$sanitizedCriteria = $this->sanitizeValue($criteria); $sanitizedCriteria = $this->sanitizeValue($criteria);
try {
$body = json_encode( $body = json_encode(
$sanitizedCriteria, $sanitizedCriteria,
JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE | JSON_INVALID_UTF8_SUBSTITUTE JSON_THROW_ON_ERROR | JSON_UNESCAPED_SLASHES | JSON_INVALID_UTF8_SUBSTITUTE
); );
} catch (\JsonException $e) {
if (!is_string($body)) { throw new RuntimeException('Failed to encode Store API criteria as valid JSON.', 0, $e);
throw new RuntimeException('Failed to encode Store API criteria.');
} }
$response = $this->httpClient->request('POST', $url, [ $response = $this->httpClient->request('POST', $url, [
@@ -83,9 +83,10 @@ final readonly class StoreApiClient
return new StoreApiException( return new StoreApiException(
sprintf( sprintf(
'Shopware Store API request failed with status %d. Response: %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.' : ''
), ),
$statusCode, $statusCode,
$serverFailure, $serverFailure,
@@ -126,26 +127,34 @@ final readonly class StoreApiClient
private function sanitizeString(string $value): string private function sanitizeString(string $value): string
{ {
if (preg_match('//u', $value) === 1) { if (preg_match('//u', $value) !== 1) {
return $value;
}
if (function_exists('mb_convert_encoding')) { if (function_exists('mb_convert_encoding')) {
$value = mb_convert_encoding($value, 'UTF-8', 'UTF-8'); $value = mb_convert_encoding($value, 'UTF-8', 'UTF-8');
} }
if (preg_match('//u', $value) === 1) { if (preg_match('//u', $value) !== 1 && function_exists('iconv')) {
return $value;
}
if (function_exists('iconv')) {
$converted = @iconv('UTF-8', 'UTF-8//IGNORE', $value); $converted = @iconv('UTF-8', 'UTF-8//IGNORE', $value);
if (is_string($converted) && $converted !== '') { if (is_string($converted)) {
return $converted; $value = $converted;
}
} }
} }
if (preg_match('//u', $value) !== 1) {
return ''; return '';
} }
if (class_exists('Normalizer')) {
$normalized = \Normalizer::normalize($value, \Normalizer::FORM_C);
if (is_string($normalized)) {
$value = $normalized;
}
}
// Keep tab/newline/carriage-return, strip other control characters that can
// make downstream JSON handling and logs harder to diagnose.
return (string) preg_replace('/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/u', '', $value);
}
} }