HTTP 200, а заказ не появился: где заканчивается успех API
Зелёный HTTP-ответ подтверждает только один участок интеграции. Разбираем, как отличить приём запроса, постановку в очередь, работу обработчика и фактическое изменение заказа.

В этой статье
Интеграция отправила заказ, получила HTTP 200 и записала в журнал «успешно». В учётной системе заказа нет. Кто прав? Технически — возможно, оба журнала: первый подтверждает ответ API, а второй показывает фактическое состояние данных. Между ними может находиться валидация, очередь, обработчик и ещё одна транзакция.
Основной ориентир простой: каждому подтверждению нужно дать точное имя. «Запрос принят», «сообщение сохранено в очереди», «обработчик завершился» и «заказ создан с нужными полями» — четыре разных результата. Если назвать их одним словом «успех», расследование обычно начинается с обсуждения зелёного индикатора. Индикатор при этом ни в чём не виноват: ему просто поручили слишком много.
Один ответ подтверждает один участок
На маленьком примере магазин передаёт заказ во внешнюю систему. API проверяет формат, присваивает операции идентификатор и кладёт сообщение в очередь. Отдельный обработчик читает его, сопоставляет покупателя и товары, затем сохраняет документ. Клиент получает ответ раньше, чем закончится последний шаг.
В такой схеме HTTP-ответ описывает результат разговора с API, но не обязан описывать будущий результат фоновой работы. Стандарт HTTP различает несколько смыслов: 200 означает успешное выполнение запроса в соответствии с контрактом метода; 201 — создание ресурса; 202 — принятие в обработку, которая ещё не завершена. Для 202 стандарт рекомендует описать в ответе текущее состояние и указать способ наблюдать за операцией.
Поэтому код нельзя выбирать как декоративный цвет. Если заказ уже создан и ответ содержит его идентификатор, контракт может сообщать о завершённой операции. Если сохранена только задача для фонового обработчика, ответ должен честно отражать незавершённость и возвращать идентификатор операции. Если же система отвечает 200 в любом случае, включая ошибку в теле, клиенту приходится угадывать смысл по строке сообщения. Такой протокол работает ровно до первого нового текста ошибки.

Контракт ответа важнее слова «успешно»
У ответа интеграции должна быть проверяемая структура. Она не обязана быть большой, но обязана отвечать на вопросы клиента без чтения человеческой фразы. Для синхронной операции нужен идентификатор созданного или изменённого объекта. Для асинхронной — идентификатор операции и отдельный способ узнать её состояние. Для отказа — стабильный тип ошибки и детали, достаточные для решения.
- какой результат достигнут: завершено, принято в обработку или отклонено;
- какая операция или сущность создана и по какому постоянному идентификатору её искать;
- какая версия или исходное событие обработаны;
- можно ли безопасно повторить запрос после тайм-аута;
- где увидеть окончательный результат и диагностические сведения.
HTTP-код даёт общую семантику, а тело уточняет предметный результат. Для машинно-читаемых ошибок существует стандарт Problem Details: он отделяет тип проблемы, краткое название, статус, подробность и конкретный экземпляр ошибки. Использовать именно этот формат необязательно для каждой внутренней интеграции, но принцип полезен: программа должна различать причины без поиска слова «ошибка» в произвольном тексте.
Есть и обратная крайность — всегда возвращать ошибку, пока фоновой процесс не закончит всю работу. Тогда медленный импорт удерживает соединение, клиент не знает, ждать ли дальше, а промежуточный сервер может завершить запрос по тайм-ауту. Асинхронная обработка сама по себе нормальна. Проблема начинается, когда незавершённую операцию выдают за готовый бизнес-результат.
Очередь добавляет ещё две границы
Появление очереди делает систему устойчивее к кратким пикам, но добавляет новые подтверждения. На примере RabbitMQ брокер отправляет подтверждение издателю сообщения: оно означает, что брокер принял ответственность за сообщение. Подтверждение потребителя относится к другому участку: обработчик сообщает брокеру, что доставка обработана. Официальная документация прямо разделяет эти механизмы — они не знают о результате друг друга.
И даже подтверждение потребителя не всегда доказывает правильный заказ. Обработчик может подтвердить сообщение слишком рано, до фиксации транзакции. Может сохранить документ, но пропустить одну строку из-за неверного сопоставления. Может записать состояние «готов», хотя связанный файл не загрузился. Транспортная надёжность сохраняет сообщение; бизнес-проверка подтверждает смысл. Эти задачи связаны, но не взаимозаменяемы.
Практически у операции появляются как минимум три наблюдаемых идентификатора: входной запрос, сообщение очереди и бизнес-объект. Их удобно связывать одним корреляционным идентификатором, не подменяя им постоянный ID заказа. Тогда по одной операции можно найти исходный запрос, попытки обработки и итоговый объект, а не сопоставлять журналы по времени с точностью «примерно после обеда».
Повтор после тайм-аута — отдельный тест
Клиент отправил заказ и не получил ответ: соединение оборвалось через десять секунд. Нельзя определить, произошёл ли сбой до записи, после записи или только при доставке ответа. Самый естественный следующий шаг — повторить запрос. Для операции создания это и есть крайний случай, который отличает аккуратный контракт от генератора дублей.
HTTP считает метод идемпотентным, когда несколько одинаковых запросов имеют тот же задуманный эффект, что и один. Но обычный POST не становится безопасным для повтора только потому, что в нём отправлен тот же JSON. Интеграции нужен собственный устойчивый ключ операции или внешний идентификатор заказа. При повторе сервер должен вернуть ранее достигнутый результат либо продолжить ту же операцию, а не создать соседний документ.
Тут есть подвох: ключ повторяемости не равен хешу всего тела. Время отправки, порядок полей или служебная подпись могут измениться, хотя бизнес-команда осталась той же. И наоборот, один и тот же ключ нельзя принимать с другим составом заказа молча. Контракт должен определить срок хранения ключа, реакцию на изменившиеся данные и ответ при уже завершённой операции.
Правильные запросы могут прийти в неправильном порядке
Добавим ещё одно условие. Сначала магазин отправляет создание заказа, затем — оплату и изменение адреса. Очередь повторяет одно сообщение, два обработчика работают с разной скоростью, а событие оплаты достигает получателя раньше создания. Все запросы по отдельности корректны, но итог зависит от порядка.
Решение выбирают по модели данных. Иногда получатель откладывает событие, пока не появится базовый объект. Иногда операция содержит номер версии, и система отклоняет устаревшее изменение. Иногда состояние строится из журнала событий, где порядок является частью контракта. Универсального флага нет, зато есть обязательный вопрос при приёмке: что произойдёт, если события придут повторно, с задержкой или в обратной последовательности?
Особенно опасно молча считать старое событие успешным. HTTP-ответ может быть положительным, очередь очищена, исключений нет — а адрес покупателя откатился к предыдущей версии. Поэтому бизнес-результат проверяют не только по существованию заказа, но и по значимым полям, версии и связанным сущностям.
Приёмка интеграции по цепочке подтверждений
Для приёмки не нужен искусственный сбой каждого сервера. Достаточно заранее описать ожидаемые результаты и пройти безопасные сценарии в тестовой среде. Последовательность проверки может выглядеть так:
Отправить минимальный корректный заказ и записать идентификатор операции из ответа.
Проверить, что статус ответа соответствует стадии: объект уже создан или работа только принята.
Найти итоговый заказ по постоянному идентификатору и сверить товары, суммы, покупателя, оплату и связи.
Повторить ту же бизнес-операцию после имитации неопределённого ответа и убедиться, что второй заказ не появился.
Передать два последовательных изменения в обратном порядке и проверить правило версий или ожидания.
Отправить ошибочные данные и убедиться, что отказ различим программой, а не только понятен человеку из свободного текста.
Связать журналы API, очереди и обработчика одним идентификатором и проверить, что в них нет секретов и лишних персональных данных.
Эта проверка не требует обещать доставку «ровно один раз». В распределённой системе полезнее проектировать повторяемую обработку и наблюдаемый итог: сообщение может прийти снова, но состояние заказа останется тем, которое определено контрактом.
Что считать успехом
Успех API заканчивается там, где заканчивается его контракт. Если ответ подтверждает только приём задачи, он не обязан доказывать создание заказа — но обязан честно сообщить стадию и дать способ увидеть продолжение. Если обещано завершённое изменение, получатель должен вернуть идентификатор результата, а приёмка — сверить фактическое состояние.
Хорошая интеграция не пытается одним зелёным статусом закрыть весь путь. Она оставляет проверяемый след на каждой границе: запрос, очередь, обработчик, бизнес-объект. Тогда тайм-аут означает неопределённость конкретного участка, повтор не создаёт дубль, а расследование начинается с идентификатора операции, а не с вопроса, у кого в журнале строчка зеленее.
The integration sent the order, received an HTTP 200, and logged "success." The accounting system shows no order. Who is right? Technically, both logs could be correct: the first confirms the API response, while the second reflects the actual data state. Between them may lie validation, a queue, a handler, and another transaction.
The main guideline is simple: give each confirmation a precise name. "Request accepted," "message saved to queue," "handler completed," and "order created with correct fields" are four distinct outcomes. If you label them all "success," the investigation usually begins with debating the green indicator. The indicator is not at fault; it was simply assigned too much responsibility.
One response confirms one segment
In a small example, a shop sends an order to an external system. The API checks the format, assigns an operation ID, and places the message in a queue. A separate handler reads it, matches the customer and items, then saves the document. The client receives the response before the final step completes.
In this scheme, an HTTP response describes the result of a conversation with the API but does not necessarily describe the future result of background work. The HTTP standard distinguishes several meanings: 200 indicates successful execution of the request according to the method contract; 201 indicates resource creation; 202 indicates acceptance for processing, which is not yet complete. For 202, the standard recommends describing the current state in the response and specifying a way to monitor the operation.
Therefore, a status code cannot be chosen as a decorative color. If the order has already been created and the response contains its identifier, the contract can report a completed operation. If only a task for a background processor is saved, the response must honestly reflect the incompleteness and return the operation identifier. If the system responds with 200 in any case, including an error in the body, the client must guess the meaning from the message string. Such a protocol works only until the first new error message appears.

The response contract is more important than the word "success"
The integration response must have a verifiable structure. It does not need to be large, but it must answer the client's questions without requiring a human to read a natural language phrase. For a synchronous operation, an identifier for the created or modified object is required. For an asynchronous operation, an operation identifier and a separate method to check its status are required. For a failure, a stable error type and details sufficient for resolution are required.
- what result was achieved: completed, accepted for processing, or rejected;
- which operation or entity was created and by which permanent identifier it can be found;
- which version or original event was processed;
- whether the request can be safely retried after a timeout;
- where to view the final result and diagnostic information.
The HTTP status code provides general semantics, while the body clarifies the domain-specific result. For machine-readable errors, the Problem Details standard exists: it separates the error type, short title, status, detail, and instance. Using this exact format is not mandatory for every internal integration, but the principle is useful: a program must distinguish causes without searching for the word "error" in arbitrary text.
There is also the opposite extreme: always returning an error until the background process completes all work. In that case, a slow import holds the connection, the client does not know whether to wait further, and the intermediate server may terminate the request due to a timeout. Asynchronous processing itself is normal. The problem begins when an incomplete operation is presented as a completed business result.
A queue adds two more boundaries
The introduction of a queue makes the system more resilient to short spikes but introduces new confirmations. Using RabbitMQ as an example, the broker sends a confirmation to the message publisher: this means the broker has accepted responsibility for the message. The consumer confirmation belongs to a different area: the handler informs the broker that delivery has been processed. Official documentation explicitly separates these mechanisms—they are unaware of each other's results.
Even a consumer confirmation does not always prove a correct order. The handler may confirm the message too early, before the transaction is committed. It may save the document but skip a line due to incorrect mapping. It may write a status as "ready" even though the associated file did not upload. Transport reliability preserves the message; business validation confirms the meaning. These tasks are related but not interchangeable.
Almost every operation has at least three observable identifiers: the incoming request, the queue message, and the business object. It is convenient to link them with a single correlation ID without replacing it with the permanent order ID. This way, for a single operation, you can find the original request, processing attempts, and the final object, rather than matching logs by time with an accuracy of 'sometime after lunch'.
Retry after timeout is a separate test
The client sent an order and received no response: the connection dropped after ten seconds. It is impossible to determine whether the failure occurred before writing, after writing, or only during response delivery. The most natural next step is to retry the request. For a creation operation, this is the edge case that distinguishes a careful contract from a duplicate generator.
HTTP considers a method idempotent when multiple identical requests have the same intended effect as a single one. However, a standard POST does not become safe to retry just because the same JSON was sent. Integrations require their own durable operation key or an external order ID. On retry, the server must return the previously achieved result or continue the same operation, not create a neighboring document.
There is a catch: the repeatability key does not equal the hash of the entire body. The send time, field order, or service signature may change even if the business context remains the same. Conversely, the same key cannot be accepted with a different order composition silently. The contract must define the key retention period, the reaction to changed data, and the response for an already completed operation.
Correct requests may arrive in the wrong order
Let us add another condition. First, the store sends the order creation, then payment and address changes. The queue repeats a single message, two handlers work at different speeds, and the payment event reaches the recipient before the creation event. All requests are individually correct, but the outcome depends on the order.
The solution is chosen based on the data model. Sometimes the recipient delays the event until the base object appears. Sometimes the operation contains a version number, and the system rejects outdated changes. Sometimes the state is built from an event log where order is part of the contract. There is no universal flag, but there is a mandatory question during acceptance testing: what happens if events arrive again, with a delay, or in reverse sequence?
It is especially dangerous to silently treat an old event as successful. An HTTP response may be positive, the queue may be cleared, and no exceptions may occur—yet the buyer's address has reverted to a previous version. Therefore, the business result is verified not only by the existence of the order but also by significant fields, versions, and related entities.
Acceptance testing of the integration chain via confirmations
Acceptance testing does not require artificially triggering a failure on every server. It is sufficient to predefine expected outcomes and run safe scenarios in a test environment. The verification sequence might look like this:
Send a minimal valid order and record the operation ID from the response.
Verify that the response status matches the stage: the object is already created or the work has only been accepted.
Find the final order by the permanent ID and compare the items, amounts, buyer, payment, and links.
Repeat the same business operation after simulating an uncertain response and ensure that a second order did not appear.
Submit two sequential changes in reverse order and verify the versioning rule or wait logic.
Send invalid data and ensure that the rejection is distinguishable by the program, not just understandable to a human from free text.
Link the API, queue, and handler logs with a single ID and verify that they contain no secrets or unnecessary personal data.
This check does not require promising delivery "exactly once." In a distributed system, it is more useful to design for repeatable processing and an observable outcome: a message may arrive again, but the order state will remain as defined by the contract.
What counts as success
API success ends where its contract ends. If the response only confirms task acceptance, it is not required to prove order creation—but it must honestly report the stage and provide a way to see the continuation. If a completed change is promised, the receiver must return the result identifier, and acceptance testing must verify the actual state.
A good integration does not try to close the entire flow with a single green status. It leaves a verifiable trail at every boundary: request, queue, handler, business object. In this case, a timeout indicates uncertainty in a specific segment, a retry does not create a duplicate, and an investigation starts with the operation ID, not with the question of whose log entry looks greener.




Обсуждение 0
Делись опытом и задавай вопросы. Комментарии без ссылок появляются после проверки редактором.
Пока никто не написал. Начни обсуждение.