AACsearch
Поисковый API

Что делать при ошибках и лимитах запросов

Как обработать ошибки поиска в коде, что означают коды ошибок, как обойти ограничение запросов.

Ошибки и ограничения запросов

Когда что-то не так с вашим запросом (неверный ключ, некорректный синтаксис, превышен лимит запросов), AACsearch вернёт ошибку с кодом.

Как выглядит ошибка

{
  "error": "rate_limit_exceeded",
  "message": "Превышено 60 запросов в минуту, повторите через 15 секунд",
  "retryable": true,
  "requestId": "req_01HX9JKP7QZK7M5T2N4F8R6Y3W",
  "docsUrl": "https://docs.aacsearch.com/troubleshooting/rate-limits"
}
ПолеСтабильно?Используйте для
errorда — машиночитаемый, стабильный между версиямиswitch/case в обработчике ошибок
messageнет — для людей, может менятьсявывод в логи / dashboards для инженеров
retryableда — booleanподключите к retry-логике (не парсите код вручную)
requestIdда — opaque ULIDпередайте в support чтобы найти запрос в логах
docsUrlда — присутствует для документированных ошибокссылка из вашего UI ошибок

requestId также возвращается в заголовке ответа X-Request-Id — на случай если тело ответа не парсится (например 504 от upstream-прокси).

Типичные ошибки и решения

❌ Ошибки в вашем коде (4xx) — не повторяйте

ОшибкаПроблемаКак исправить
unauthorizedНеверный или истёкший API-ключПроверьте ключ в панели управления
forbiddenКлюч не имеет права на этот индексИспользуйте правильный ключ или добавьте область
invalid_filterНеверный синтаксис в filterByПроверьте синтаксис фильтров (см. раздел выше)
invalid_sortНеверный синтаксис в sortByИспользуйте правильное поле и направление
invalid_requestОтсутствует обязательное полеДобавьте indexSlug и q
index_not_foundИндекс с этим названием не существуетПроверьте название индекса (indexSlug)

Действие: исправьте запрос, не повторяйте слепо.

⏳ Превышены лимиты (429) — подождите и повторите

ОшибкаПроблемаЧто делать
rate_limit_exceededСлишком много запросов за минуту (> 60)Подождите (секунды в заголовке Retry-After), потом повторите
quota_exceededИсчерпана месячная квота поисковых единицОбновите план в панели (Settings → Billing) или подождите до 1-го числа месяца

Действие: подождите необходимое время, повторите запрос.

🔥 Ошибки сервера (5xx) — повторите с задержкой

ОшибкаПроблемаЧто делать
search_failedСервер AACsearch недоступенПовторите через 1 сек.; если ещё раз не удалось → ошибка
service_unavailableAPI AACsearch на техническом обслуживанииПовторите с экспоненциальной задержкой (1s, 2s, 4s)

Действие: повторите с задержкой, не более 3 попыток.

Ограничение частоты запросов

Лимит: 60 запросов в минуту на один API-ключ.

Если вы отправляете запросы слишком часто, получите ошибку:

{
  "error": "rate_limit_exceeded",
  "message": "Превышено 60 запросов в минуту. Повторите через 15 секунд."
}

Как узнать статус лимита

В каждом ответе AACsearch добавляет заголовки:

X-RateLimit-Limit: 60            // Максимум запросов за минуту
X-RateLimit-Remaining: 42        // Запросов осталось в текущей минуте
X-RateLimit-Reset: 1717201234    // Когда сбросится (Unix время)
Retry-After: 15                  // Если лимит превышен: подождите столько секунд

Используйте X-RateLimit-Remaining чтобы предугадать, когда приближается лимит.

Как избежать превышения лимита

  • Кэш результатов: не делайте одинаковые поиски дважды
  • Группируйте запросы: используйте мультипоиск для нескольких поисков сразу
  • Дебаунс автодополнения: не ищите на каждый символ, подождите паузу
  • Избегайте параллельных запросов: отправляйте поиски один за другим, не все сразу

Пример: обработка лимита в коде

try {
  const results = await client.search({ q: "shoes" });
} catch (err) {
  if (err.code === "rate_limit_exceeded") {
    console.log("Лимит исчерпан, подожду 15 секунд...");
    setTimeout(() => {
      // Повторить запрос
    }, 15000);
  }
}

Месячная квота

Каждый план имеет месячный лимит на поиски (в "единицах поиска").

Когда исчерпается квота:

  • Ошибка: quota_exceeded
  • Решение: обновите план (Settings → Billing) или подождите до 1-го числа следующего месяца
  • Квота сбрасывается автоматически 1-го числа каждого календарного месяца

Как узнать, сколько квоты осталось

В панели управления (Search → Overview) видна полоска использованной квоты.

Обработка ошибок в коде

Используйте готовые библиотеки для автоматических повторов:

// JavaScript с библиотекой p-retry
import pRetry from "p-retry";

const results = await pRetry(
  () => client.search({ q: "headphones" }),
  {
    retries: 3,           // Максимум 3 попытки
    factor: 2,            // Экспоненциальная задержка: 1s, 2s, 4s
    minTimeout: 1000,     // Минимум 1 секунда между попытками
  }
);
# Python с retry
from retry import retry

@retry(tries=3, delay=1, backoff=2)
def search():
    return client.search(q="headphones")

results = search()

Поведение модулей CMS при повторах

Буфер приёма данных AACsearch рассчитан на повторные попытки:

  • Запросы синхронизации коннектора (sync/full, sync/delta) ставят документы в очередь SearchIngestBuffer
  • Фоновый воркер повторяет неудавшиеся строки с экспоненциальной задержкой
  • Модулю CMS не нужно реализовывать повтор для отдельных документов — буфер берёт это на себя
  • Модуль CMS должен повторять сам HTTP-запрос при ответах 5xx (но не на 4xx)

Отладка при разработке

При ошибках 502 во время разработки:

  1. Проверьте, работает ли AACSearch: curl http://localhost:8108/health
  2. Проверьте логи API на наличие исходной ошибки AACSearch (она сопоставляется перед отправкой ответа клиенту)
  3. Проверьте AACSEARCH_HOST, AACSEARCH_PORT, AACSEARCH_API_KEY в .env.local

При ошибках 401:

  1. Убедитесь, что срок действия ключа не истёк
  2. Убедитесь, что префикс ключа соответствует операции (ss_search_* для поиска, ss_connector_* для приёма данных)
  3. Для ограниченных токенов: проверьте поле expiresAt в декодированной нагрузке

Request ID и trace ID

Каждый ответ содержит два корреляционных идентификатора — передавайте их в поддержку, и мы сможем найти запрос в логах без переписки.

ЗаголовокПоле телаОткуда
X-Request-IdrequestIdГенерируется на edge API на каждый запрос. Присутствует всегда.
X-Trace-IdtraceId (только если включён distributed tracing)OpenTelemetry trace ID, прокидывается через worker → AACSearch → ответ

Захватите их клиентом из SDK:

import { SearchClient } from "@aacsearch/client";

try {
	const result = await client.search({ q: "shoes" });
} catch (err) {
	if (err instanceof AacSearchError) {
		console.error("aacsearch error", {
			code: err.code,
			requestId: err.requestId, // для тикетов в поддержку
			traceId: err.traceId, // для дашбордов distributed tracing
		});
	}
}

Серверные обработчики должны логировать их при каждом запросе — успешном и провальном:

const result = await client.search(opts);
logger.info({
	event: "search.ok",
	requestId: result.requestId,
	searchTimeMs: result.searchTimeMs,
});

Так когда клиент сообщит "поиск тормозил в 14:32" — вы найдёте requestId в логах и совпадение AACsearch-запроса. С нашей стороны мы сможем join по этому ID.

SDK error class

TypeScript SDK выставляет типизированный AacSearchError, оборачивающий каждый ответ не 2xx:

import { AacSearchError } from "@aacsearch/client";

try {
	await client.search({ q: "shoes" });
} catch (err) {
	if (err instanceof AacSearchError) {
		err.code; // string — код ошибки из тела, стабильный
		err.message; // string — для людей
		err.status; // number — HTTP status (0 для сетевых ошибок)
		err.retryable; // boolean — из тела, fallback к status >= 500
		err.requestId; // string | undefined
		err.traceId; // string | undefined
		err.docsUrl; // string | undefined
		err.response; // Response | undefined — сырой fetch Response если доступен
	}
}

Та же форма в Python (SdkError) и PHP (AacSearchException).

Рецепты UI-состояний

Конкретные React-компоненты для четырёх самых частых состояний ошибок. Все headless — встройте в свою дизайн-систему.

Невалидный ключ

function SearchInvalidKey({ requestId }: { requestId?: string }) {
	return (
		<div role="alert" className="search-error">
			<h3>Поиск настроен некорректно</h3>
			<p>Ваш API-ключ невалиден, истёк или был отозван.</p>
			<p>
				Если вы владелец сайта — выпустите новый ключ в панели AACsearch. Иначе попробуйте позже.
			</p>
			{requestId && <small>Reference: {requestId}</small>}
		</div>
	);
}

Quota exceeded

function SearchQuotaExceeded({ resetDate }: { resetDate: Date }) {
	return (
		<div role="alert" className="search-error">
			<h3>Поиск временно недоступен</h3>
			<p>Сайт исчерпал месячный лимит поиска.</p>
			<p>Сервис восстановится {resetDate.toLocaleDateString()}.</p>
			<p>
				<a href="https://app.aacsearch.com/billing">Сменить тариф</a>
			</p>
		</div>
	);
}

Нет результатов

function NoResults({ query }: { query: string }) {
	return (
		<div className="no-results">
			<h3>Ничего не найдено по &quot;{query}&quot;</h3>
			<p>Попробуйте более короткий запрос, другие ключевые слова или просмотрите наши категории ниже.</p>
			<a href="/c/all">Все товары →</a>
		</div>
	);
}

Для богатого UI пустых результатов с did-you-mean, популярными категориями и fallback-бестселлерами — см. рецепт no-results.

Ошибка сервера / сети

function SearchUnavailable({
	requestId,
	onRetry,
}: {
	requestId?: string;
	onRetry: () => void;
}) {
	return (
		<div role="alert" className="search-error">
			<h3>Поиск временно недоступен</h3>
			<p>Не удалось достучаться до сервиса поиска. Попробуйте через минуту.</p>
			<button onClick={onRetry}>Повторить</button>
			{requestId && <small>Reference: {requestId}</small>}
		</div>
	);
}

Связывание

Используйте код ошибки SDK для диспетчеризации:

function SearchResults({ query }: { query: string }) {
	const { data, error, refetch } = useQuery({
		queryKey: ["search", query],
		queryFn: () => client.search({ q: query }),
	});

	if (error instanceof AacSearchError) {
		switch (error.code) {
			case "unauthorized":
			case "invalid_or_revoked_key":
				return <SearchInvalidKey requestId={error.requestId} />;
			case "quota_exceeded":
				return <SearchQuotaExceeded resetDate={nextMonthFirst()} />;
			case "rate_limit":
			case "search_failed":
			case "service_unavailable":
				return <SearchUnavailable requestId={error.requestId} onRetry={refetch} />;
			default:
				return <SearchUnavailable requestId={error.requestId} onRetry={refetch} />;
		}
	}

	if (data?.found === 0) return <NoResults query={query} />;
	return <ResultGrid hits={data?.hits ?? []} />;
}

Полный диагностический поток на каждый код ошибки — см. хаб диагностики.

Связанные

On this page