Batch API Returned 200: How Not to Lose Errors for Individual Items
Check every element of the batch response: detect missing results, avoid resubmitting successful SKUs, and safely resume only temporarily failed operations.

In this article
Пакетный запрос может получить HTTP 200, хотя часть товаров отклонена, один результат потерян, а ещё один нужно повторить. Поэтому верхний код ответа подтверждает только приём и разбор пакета по правилам конкретного API. Готовность обмена проверяют по результату каждого элемента: все отправленные идентификаторы должны встретиться в ответе ровно один раз, а успехи, постоянные отказы и временные сбои нужно разнести по разным веткам.
Ниже — воспроизводимая модель без сети, файлов и рабочего магазина. Она показывает два края, из-за которых красивый зелёный ответ быстро становится бухгалтерией: отсутствующий результат блокирует приёмку всего пакета, а повтор временно неудавшейся позиции использует прежний operation_id и не создаёт второе изменение.
Почему HTTP 200 недостаточно
RFC 9110 определяет 200 как успешное выполнение самого HTTP-запроса; смысл содержимого зависит от метода. Конкретный пакетный контракт может использовать успешный верхний ответ для контейнера, внутри которого отдельные операции завершились по-разному.
Microsoft Graph приводит документированный пример: разбор JSON-пакета обычно возвращает 200, но каждый вложенный ответ имеет собственный id и status; порядок вложенных ответов может отличаться от порядка запросов. Это пример конкретного API, а не универсальная схема для 1С-Битрикс или любого поставщика.
Код 207 Multi-Status тоже не является волшебной заменой: RFC 4918 определяет его для WebDAV и всё равно требует читать тело с результатами отдельных ресурсов. Использовать 200, 207 или другой верхний статус нужно ровно так, как описывает контракт метода; клиенту в любом случае нужен проверяемый результат каждой операции.
Минимальный контракт результата
Состояние элемента | Что подтверждено | Действие клиента |
|---|---|---|
applied | Изменение применено либо повтор распознан | Не отправлять заново без новой бизнес-команды |
rejected | Запрос понят, но данные или права не подходят | Исправить причину; автоматический повтор того же тела не поможет |
retryable | Временный сбой по правилам API | Повторить только этот элемент после допустимой паузы |
нет результата | Судьба элемента неизвестна | Не считать пакет завершённым; выполнить сверку по operation_id |
Названия состояний здесь учебные. В рабочем API это могут быть HTTP-коды вложенных ответов, собственные коды или отдельные объекты ошибок. Важен инвариант: один отправленный operation_id — один явно классифицированный результат.
Стенд и начальные условия
CPython 3.12, только стандартная библиотека; проверка выполнена 30 октября 2026 года.
Изолированная модель работает только в памяти: нет сети, файлов, базы данных и настоящего каталога.
В пакете четыре торговые позиции с кодами SKU-RED-S, SKU-RED-M, SKU-BLUE-S и SKU-BLUE-M; изменяется только числовой остаток stock.
operation_id op-101…op-104 создаётся один раз для каждой команды и не меняется при сетевом повторе.
Учебные состояния applied, rejected и retryable заданы самим примером; они не приписываются конкретной версии 1С-Битрикс.
Сохраните пример в новом временном каталоге как batch_result_lab.py. Он не выполняет операций записи или удаления вне самого файла, созданного пользователем.
from __future__ import annotations
from dataclasses import dataclass
@dataclass(frozen=True)
class Update:
operation_id: str
sku: str
stock: int
@dataclass(frozen=True)
class Result:
operation_id: str
status: str
error_code: str | None = None
def validate_batch(requests: list[Update], results: list[Result]) -> dict[str, Result]:
requested = {item.operation_id for item in requests}
if len(requested) != len(requests):
raise ValueError("duplicate operation_id in request")
indexed: dict[str, Result] = {}
for item in results:
if item.operation_id not in requested:
raise ValueError(f"unknown result: {item.operation_id}")
if item.operation_id in indexed:
raise ValueError(f"duplicate result: {item.operation_id}")
if item.status not in {"applied", "rejected", "retryable"}:
raise ValueError(f"unknown status: {item.status}")
indexed[item.operation_id] = item
missing = requested - indexed.keys()
if missing:
raise ValueError(f"missing results: {sorted(missing)}")
return indexed
class FakeCatalogApi:
def __init__(self) -> None:
self.stock: dict[str, int] = {}
self.applied_operations: set[str] = set()
self.calls = 0
def retry(self, update: Update) -> Result:
self.calls += 1
if update.operation_id in self.applied_operations:
return Result(update.operation_id, "applied", "duplicate_ack")
self.stock[update.sku] = update.stock
self.applied_operations.add(update.operation_id)
return Result(update.operation_id, "applied")
requests = [
Update("op-101", "SKU-RED-S", 7),
Update("op-102", "SKU-RED-M", -1),
Update("op-103", "SKU-BLUE-S", 4),
Update("op-104", "SKU-BLUE-M", 0),
]
incomplete = [
Result("op-101", "applied"),
Result("op-102", "rejected", "stock_must_be_non_negative"),
Result("op-103", "retryable", "supplier_temporarily_unavailable"),
]
try:
validate_batch(requests, incomplete)
except ValueError as error:
print(f"incomplete: {error}")
complete = incomplete + [Result("op-104", "applied")]
indexed = validate_batch(requests, complete)
retry_ids = [key for key, value in indexed.items() if value.status == "retryable"]
rejected_ids = [key for key, value in indexed.items() if value.status == "rejected"]
print(f"accepted=2 rejected={rejected_ids} retry={retry_ids}")
api = FakeCatalogApi()
retry_request = next(item for item in requests if item.operation_id == retry_ids[0])
first = api.retry(retry_request)
second = api.retry(retry_request)
assert first.status == "applied"
assert second.error_code == "duplicate_ack"
assert api.stock == {"SKU-BLUE-S": 4}
assert api.calls == 2 and len(api.applied_operations) == 1
print(
"retry: "
f"first={first.status} second={second.error_code} "
f"calls={api.calls} unique_changes={len(api.applied_operations)}"
)
Команда запуска:
python3 batch_result_lab.py
Полученный вывод:
incomplete: missing results: ['op-104']
accepted=2 rejected=['op-102'] retry=['op-103']
retry: first=applied second=duplicate_ack calls=2 unique_changes=1
Процесс завершился с кодом 0. Проверки assert дополнительно подтверждают, что два вызова с op-103 дают одно уникальное изменение остатка: первый применяет значение 4 для SKU-BLUE-S, второй получает подтверждение уже принятой операции.
Что подтвердил прогон
Проверка | Ожидаемый результат | Наблюдение |
|---|---|---|
В ответе нет op-104 | Пакет нельзя принять как полный | Валидатор остановился с missing results |
Два элемента applied | Не повторять успешные позиции | В очередь повтора попал только op-103 |
op-102 rejected | Постоянный отказ отделён от временного | Причина stock_must_be_non_negative сохранена отдельно |
op-103 retryable | Повторить с прежним operation_id | Первый повтор установил остаток товара |
Ответ на повтор потерян | Ещё один повтор не меняет данные второй раз | duplicate_ack; два вызова, одно уникальное изменение |
Как принимать пакет в интеграции магазина
Сохранить состав отправленного пакета: operation_id, SKU или другой устойчивый идентификатор торговой позиции, поле, значение и версию контракта.
После ответа сравнить множества идентификаторов. Неизвестный, повторяющийся или отсутствующий результат — отдельная ошибка протокола, а не повод угадать состояние.
Зафиксировать applied как завершённые операции и больше не включать их в технический повтор.
Для rejected сохранить машинный код и безопасные детали: неверный SKU, недопустимое значение, отсутствие права или конфликт версии требуют разных исправлений.
Повторять только retryable по правилам поставщика: с допустимой паузой, ограниченным бюджетом и прежним operation_id.
После неоднозначного тайм-аута сначала запросить состояние операции, если API это позволяет. Новый operation_id может превратить повтор в новую команду.
Счётчики пакета полезно показывать раздельно: отправлено 4, применено 2, отклонено 1, ожидает повтора 1. Формулировка «успешно 4 из 4» допустима только после проверки бизнес-результата всех четырёх операций. Размер массива, HTTP 200 и отсутствие исключения у клиента этого не доказывают.
Где проходит граница с 1С-Битрикс
Пример не проверяет штатный обмен с 1С и не обещает формат ответа какой-либо редакции 1С-Битрикс: Управление сайтом. Перед внедрением нужно открыть документацию конкретного модуля и версии, уточнить направление обмена, способ идентификации товара и торгового предложения, поля цены и остатка, права пользователя, максимальный размер пакета и правила повтора.
Для каталога отдельно проверьте, что SKU однозначно указывает нужную торговую позицию, а не только родительский товар. Ошибка права на изменение цены не должна маскироваться под временную недоступность. Изменение остатка на ноль — содержательное значение, а не пустой результат. Если метод не поддерживает ключ идемпотентности или поиск операции, безопасную схему повтора придётся проектировать на стороне интеграции.
Крайние случаи, которые стоит добавить на стенде
вложенные результаты пришли в другом порядке;
один operation_id встречается дважды или в ответе появился неизвестный идентификатор;
часть позиций получила отказ из-за прав, часть — из-за неверного SKU;
ответ потерян после фактического применения изменения;
цена или остаток изменились между чтением и записью, и API поддерживает версию либо условие If-Match;
пакет принят, но последующая асинхронная обработка ещё не завершена.
Критерий готовности простой: для каждого отправленного элемента известен ровно один проверяемый исход; успешные элементы не повторяются, постоянные отказы попадают на исправление, временные сбои повторяются с тем же идентификатором, а неизвестная судьба не превращается в выдуманный успех. Верхний HTTP 200 после этого остаётся полезным транспортным фактом — просто больше не пытается изображать весь отчёт.
Для настройки и диагностики обмена на копии проекта полезно заранее подготовить согласованный набор SKU и ожидаемых значений. Если нужен разбор конкретного маршрута 1С-Битрикс и внешней системы, следующий шаг — сопоставить этот набор с журналами задания и фактическим состоянием каталога.
A batch request may return HTTP 200 even if some items were rejected, one result was lost, and another requires retrying. Therefore, the top-level status code only confirms that the package was received and parsed according to the rules of the specific API. Readiness for exchange is verified by the result of each element: all sent identifiers must appear in the response exactly once, and successes, permanent failures, and temporary errors must be routed to separate branches.
Below is a reproducible model without a network, files, or a live store. It shows two edge cases where a successful response quickly turns into accounting issues: a missing result blocks acceptance of the entire batch, and retrying a temporarily failed item uses the original operation_id and does not create a second change.
Why HTTP 200 Is Not Enough
RFC 9110 defines 200 as a successful execution of the HTTP request itself; the meaning of the content depends on the method. A specific batch contract may use a successful upper response as a container within which individual operations complete differently.
Microsoft Graph provides a documented example: parsing a JSON batch typically returns 200, but each nested response has its own id and status; the order of nested responses may differ from the order of requests. This is an example of a specific API, not a universal scheme for 1C-Bitrix or any provider.
Status code 207 Multi-Status is also not a magic replacement: RFC 4918 defines it for WebDAV and still requires reading the body with results for individual resources. Use 200, 207, or another top-level status exactly as the method contract describes; the client still needs a verifiable result for each operation.
Minimal result contract
Item status | What is confirmed | Client action |
|---|---|---|
applied | Change applied or retry recognized | Do not resend without a new business command |
rejected | Request understood, but data or permissions do not match | Fix the cause; automatically retrying the same payload will not help |
retryable | Temporary failure per API rules | Retry only this item after an acceptable pause |
no result | The fate of the element is unknown | Do not consider the package complete; perform a check by operation_id |
The state names here are illustrative. In a production API, these may be HTTP codes from nested responses, custom codes, or separate error objects. The invariant is key: one sent operation_id yields one explicitly classified result.
Test environment and initial conditions
CPython 3.12, standard library only; check performed on October 30, 2026.
The isolated model operates only in memory: no network, files, database, or real directory.
The package contains four product variants with SKU codes SKU-RED-S, SKU-RED-M, SKU-BLUE-S, and SKU-BLUE-M; only the numeric stock remainder changes.
operation_id op-101…op-104 is created once for each command and does not change during network retries.
The sample defines the applied, rejected, and retryable states; they are not tied to a specific 1C-Bitrix version.
Save the sample in a new temporary directory as batch_result_lab.py. It performs no write or delete operations outside the user-created file.
from __future__ import annotations
from dataclasses import dataclass
@dataclass(frozen=True)
class Update:
operation_id: str
sku: str
stock: int
@dataclass(frozen=True)
class Result:
operation_id: str
status: str
error_code: str | None = None
def validate_batch(requests: list[Update], results: list[Result]) -> dict[str, Result]:
requested = {item.operation_id for item in requests}
if len(requested) != len(requests):
raise ValueError("duplicate operation_id in request")
indexed: dict[str, Result] = {}
for item in results:
if item.operation_id not in requested:
raise ValueError(f"unknown result: {item.operation_id}")
if item.operation_id in indexed:
raise ValueError(f"duplicate result: {item.operation_id}")
if item.status not in {"applied", "rejected", "retryable"}:
raise ValueError(f"unknown status: {item.status}")
indexed[item.operation_id] = item
missing = requested - indexed.keys()
if missing:
raise ValueError(f"missing results: {sorted(missing)}")
return indexed
class FakeCatalogApi:
def __init__(self) -> None:
self.stock: dict[str, int] = {}
self.applied_operations: set[str] = set()
self.calls = 0
def retry(self, update: Update) -> Result:
self.calls += 1
if update.operation_id in self.applied_operations:
return Result(update.operation_id, "applied", "duplicate_ack")
self.stock[update.sku] = update.stock
self.applied_operations.add(update.operation_id)
return Result(update.operation_id, "applied")
requests = [
Update("op-101", "SKU-RED-S", 7),
Update("op-102", "SKU-RED-M", -1),
Update("op-103", "SKU-BLUE-S", 4),
Update("op-104", "SKU-BLUE-M", 0),
]
incomplete = [
Result("op-101", "applied"),
Result("op-102", "rejected", "stock_must_be_non_negative"),
Result("op-103", "retryable", "supplier_temporarily_unavailable"),
]
try:
validate_batch(requests, incomplete)
except ValueError as error:
print(f"incomplete: {error}")
complete = incomplete + [Result("op-104", "applied")]
indexed = validate_batch(requests, complete)
retry_ids = [key for key, value in indexed.items() if value.status == "retryable"]
rejected_ids = [key for key, value in indexed.items() if value.status == "rejected"]
print(f"accepted=2 rejected={rejected_ids} retry={retry_ids}")
api = FakeCatalogApi()
retry_request = next(item for item in requests if item.operation_id == retry_ids[0])
first = api.retry(retry_request)
second = api.retry(retry_request)
assert first.status == "applied"
assert second.error_code == "duplicate_ack"
assert api.stock == {"SKU-BLUE-S": 4}
assert api.calls == 2 and len(api.applied_operations) == 1
print(
"retry: "
f"first={first.status} second={second.error_code} "
f"calls={api.calls} unique_changes={len(api.applied_operations)}"
)
Launch command:
python3 batch_result_lab.py
Output received:
incomplete: missing results: ['op-104']
accepted=2 rejected=['op-102'] retry=['op-103']
retry: first=applied second=duplicate_ack calls=2 unique_changes=1
The process completed with code 0. The assert checks further confirm that the two calls with op-103 produce a single unique inventory change: the first applies the value 4 for SKU-BLUE-S, and the second confirms the already accepted operation.
What the run confirmed
Check | Expected result | Observation |
|---|---|---|
No op-104 in response | Package cannot be accepted as complete | Validator stopped with missing results |
Two elements applied | Do not repeat successful items | Only op-103 entered the retry queue |
op-102 rejected | Permanent rejection is separated from temporary failure | The reason stock_must_be_non_negative is stored separately |
op-103 is retryable | Retry with the original operation_id | The first retry set the product stock |
The response to the retry was lost | Another repetition does not change the data the second time | duplicate_ack; two calls, one unique change |
How to accept a package in a store integration
Preserve the contents of the sent package: operation_id, SKU, or another stable identifier for the product variant, the field, its value, and the contract version.
After the response, compare the sets of identifiers. An unknown, repeated, or missing result is a separate protocol error, not a reason to guess the state.
Mark applied operations as completed and exclude them from further technical retries.
For rejected items, retain the machine code and safe details: an invalid SKU, an unacceptable value, missing permissions, or a version conflict require different fixes.
Retry only retryable operations according to the provider's rules: with an acceptable pause, a limited budget, and the same operation_id.
After an ambiguous timeout, first request the operation status if the API allows it. A new operation_id can turn a retry into a new command.
It is useful to display package counters separately: 4 sent, 2 applied, 1 rejected, 1 awaiting retry. The phrasing "4 out of 4 successful" is acceptable only after verifying the business result of all four operations. Array size, HTTP 200, and the absence of an exception on the client side do not prove this.
Where the boundary with 1C-Bitrix lies
This example does not verify standard exchange with 1C and does not promise the response format for any edition of 1C-Bitrix: Site Management. Before implementation, you must open the documentation for the specific module and version, clarify the exchange direction, product and product variant identification method, price and stock fields, user permissions, maximum package size, and retry rules.
For the catalog, separately verify that the SKU unambiguously points to the required product variant, not just the parent product. A permission error for price changes must not be masked as temporary unavailability. Setting stock to zero is a meaningful value, not an empty result. If the method does not support an idempotency key or operation search, a safe retry scheme must be designed on the integration side.
Edge cases that should be added to the staging environment
nested results arrived in a different order;
one operation_id appears twice, or an unknown identifier appeared in the response;
some items were rejected due to permissions, others due to an incorrect SKU;
the response was lost after the change was actually applied;
the price or stock changed between reading and writing, and the API supports versioning or the If-Match condition;
the package was accepted, but subsequent asynchronous processing is not yet complete.
Simple readiness criterion: for each submitted item, exactly one verifiable outcome is known; successful items are not repeated, permanent failures go to correction, temporary failures are retried with the same identifier, and an unknown fate does not turn into a fabricated success. An upper HTTP 200 remains a useful transport fact—simply no longer attempts to represent the entire report.
To configure and diagnose the exchange on a project copy, it is useful to prepare in advance a consistent set of SKUs and expected values. If you need to analyze a specific 1C-Bitrix and external system route, the next step is to match this set with the job logs and the actual state of the catalog.





Discussion 0
Share your experience and ask questions. Comments without links appear after editorial review.
No comments yet. Start the discussion.