Предотвратите случайную потерю данных при обновлениях с использованием семантики PATCH
Предотвратите случайную потерю данных при обновлениях, используя семантику PATCH, allowlist полей и понятные дефолты, чтобы отсутствующие поля не затирались.

Почему обновления в стиле «замены» случайно удаляют данные
Запрос на замену говорит серверу: «считай это тело запроса всей новой записью». То, что вы отправляете, становится новой правдой, а всё, чего нет — считается отсутствующим. Так поля и стираются.
Это обычно происходит, когда эндпоинт ведёт себя как PUT (полная замена), хотя все называют его «обновление». Клиент присылает частичную полезную нагрузку, бэкенд мапит её в модель и сохраняет. Если поле отсутствует, в некоторых ветках кода его устанавливают в пустое значение (""), null или дефолт. В результате это выглядит как тихое удаление.
Пример: пользователь редактирует профиль и меняет только отображаемое имя. Форма отправляет { "displayName": "Sam" }. Если сервер заменяет весь профиль, поля вроде phone, address или marketingOptIn внезапно могут стать null или false, хотя пользователь их не трогал.
Это часто проявляется, когда клиенты не (или не могут) отправить всю запись, например:
- Веб‑формы, которые отправляют только видимые поля
- Мобильные приложения, которые шлют «только изменённые поля» ради экономии трафика
- Админ‑панели, где некоторые поля скрыты во вкладках или по правам
Это легко пропустить в тестах. Тест обновляет одно поле и проверяет, что оно изменилось, но не убеждается, что все остальные поля остались прежними. Пользователи замечают проблему позже, когда исчезает адрес, сбрасывается настройка уведомлений или перестаёт работать интеграция.
Первый шаг — обнаружить эндпоинты, где «обновить» на самом деле значит «заменить».
PUT vs PATCH: что меняется, а что остаётся
PUT и PATCH отвечают на разные вопросы.
PUT означает: «Вот весь ресурс. Замени то, что у тебя есть, на это». Если сервер трактует PUT как настоящую замену, всё, чего клиент не указал, может быть удалено или сброшено.
PATCH означает: «Вот конкретные изменения. Примените их поверх того, что уже есть». Он создан для частичных правок, когда клиент отправляет только те поля, которые хочет изменить.
Это важно, потому что многие клиенты ведут себя как формы редактирования. Мобильное приложение может прислать только { "displayName": "Mina" }. Если ваш эндпоинт ожидает полноценный объект (PUT), но получает частичный, вы можете стереть поля вроде bio, photoUrl или timezone.
Простое правило, предотвращающее большинство сюрпризов — явно определить, что значит «отсутствует»:
- Поле отсутствует: оставить сохранённое значение как есть.
- Поле присутствует со значением: обновить его.
- Поле присутствует со значением
null: очистить, но только если очистка явно разрешена.
Если вы используете PUT, не трактуйте отсутствующие поля как «удалить их». Считайте отсутствие полей ошибкой, чтобы клиенты были вынуждены отправлять полное представление.
Когда полная замена (PUT) всё ещё уместна
Полная замена может работать, когда клиент действительно владеет документом целиком и надёжно отправляет все поля каждый раз. Например, внутренний админ‑инструмент для маленькой записи настроек или процесс синхронизации, который всегда имеет полный снимок.
Если вы не можете это гарантировать (большинство публичных API не могут), используйте PATCH для правок и зафиксируйте правила, чтобы клиенты не гадали.
Выберите и задокументируйте правила обновления
Большинство багов с потёртыми полями возникают из‑за несогласованности команды: сервер думает «замена», а клиент шлёт «только изменённые поля». Перед изменением кода решите, что означает каждый эндпоинт.
Для каждого эндпоинта обновления опишите:
- Режим обновления: replace или patch
- Отсутствующие поля: игнорировать или отклонять
- Явный
null: разрешён для очистки или отклоняется - Владение: какие поля может редактировать клиент, а какие — принадлежат серверу
- Валидация: что проверяется при каждом обновлении
Различие «отсутствует vs null» — та самая ловушка, в которую попадают команды. Если клиент не отправляет phone, обычно вы хотите оставить его неизменным. Если клиент присылает "phone": null, это может означать «очистить», но только если вы хотите позволить такое.
Согласованность между вебом, мобильным приложением и админкой важна. Разные клиенты часто присылают разные формы полезной нагрузки, и один клиент в режиме замены может стереть данные, созданные другим.
Быстрая проверка интуицией: выберите одно поле (например timezone) и опишите, что происходит при (1) отсутствии, (2) null, и (3) пустой строке. Если команда не может быстро ответить, правила недостаточно ясны.
Используйте allowlist полей, чтобы контролировать, что можно обновлять
Allowlist полей означает, что сервер принимает изменения только для конкретных, перечисленных полей. Всё остальное блокируется или отклоняется.
Это помогает двумя способами:
- Предотвращает случайные записи в поля, которые UI не должен менять.
- Не даёт клиентам обновлять чувствительные поля, которыми управляет сервер.
Поля, принадлежащие серверу, почти никогда не должны быть записываемыми через обычный эндпоинт «обновить профиль/настройки», например:
- role или permissions
- флаги статуса аккаунта
- суммы биллинга
createdAt/updatedAt- внутренние флаги вроде
isAdminилиriskScore
Отклоняйте неизвестные поля, а не принимайте их молча. Молчаливое принятие скрывает опечатки, устаревших клиентов и неожиданные формы полезной нагрузки.
Вложенные allowlist для сложных объектов
Если вы принимаете вложенные объекты вроде address или settings, применяйте то же правило внутри них. Разрешите верхнеуровневый ключ, затем перечислите допустимые вложенные ключи. Так settings.theme будет редактируемым, а settings.isAdmin — заблокированным.
Пошагово: реализуем безопасные частичные обновления
Безопасное частичное обновление — это «примените эти изменения», а не «замените запись». Самый надёжный паттерн: загрузить текущее состояние, применить только те изменения, которые прислал клиент (и которые допустимы), провести валидацию, затем сохранить.
Практический поток реализации
Повторяемая последовательность выглядит так:
- Получить текущую запись из базы данных (и проверить владение пользователем/тенантом).
- Собрать объект
changesиз тела запроса, используя allowlist. - Проверить изменения (типы, форматы, ограничения по длине, enum’ы).
- Применять только поля, присутствующие в запросе. Не записывайте дефолты для отсутствующих полей.
- Сохранить и вернуть обновлённую запись, чтобы клиент мог синхронизироваться.
Это избегает частой ошибки, когда клиент присылает два поля, а сервер перетирает десять других пустыми значениями.
Логирование без утечки данных
Когда что‑то идёт не так, нужна видимость, но без сохранения чувствительных значений. Логируйте метаданные, например:
- id записи (и id пользователя/тенанта)
- какие имена полей изменились
- ошибки валидации
Логируйте «updated: displayName, avatarUrl», а не само отображаемое имя.
Обращайтесь с отсутствием, null и дефолтами без сюрпризов
Большинство багов «мои данные стерлись» сводятся к одному недопониманию: сервер не может отличить «пользователь этого не трогал» от «пользователь хочет очистить это».
Трактуйте полезную нагрузку как инструкции:
- Отсутствие значит «оставить как есть».
nullзначит «очистить», но только для полей, где очистка имеет смысл.
Также решите, как обрабатывать пустые значения. Пустая строка — не то же самое, что отсутствие, а пустой массив — не то же самое, что null. Если пользователь удалил все теги, "tags": [] должно установить теги в пустой набор. Если клиент присылает "tags": null, решите, значит ли это «удалить теги» или «некорректный ввод», и придерживайтесь этого.
Избегайте применения дефолтов времени создания в потоках обновления. Дефолты относятся к созданию. При обновлениях дефолты часто вредят.
Защита от потерянных обновлений и гонок
Даже с семантикой PATCH два редактирования всё ещё могут перезаписать друг друга. Риск — во времени.
Пример: пользователь открыл «Редактировать профиль» на ноутбуке и телефоне. Телефон обновляет displayName и сохраняет. Но ноутбук, всё ещё показывающий старые данные, позже обновляет bio. Без проверки свежести второй сохранение может отменить части первого.
Используйте оптимистичный контроль конкурентности, чтобы сервер мог отклонять устаревшие изменения:
- Поле версии: храните целое число вроде
profileVersion; обновляйте только если значение совпадает. - Проверка
updatedAt: клиент отправляет метку времени последнего просмотра; сервер обновляет только если не изменилось. - ETag + If-Match: клиент доказывает, что редактирует последнюю версию.
При конфликте возвращайте понятную ошибку (обычно HTTP 409 или 412) и предложите клиенту перезагрузить данные.
Частые ошибки, которые стирают поля
Большинство потери данных при обновлениях — это не проблема базы, а проблема контракта API: сервер считает отсутствующие поля как «удалите их».
Типичные причины:
- Использование семантики PUT с клиентом, который присылает только изменённые поля
- Сохранение полного объекта, построенного из устаревшего состояния клиента
- Обновление вложенных объектов целиком вместо патчинга детей (замена
addressстираетaddress.line2, если клиент прислал толькоaddress.city) - Заполнение отсутствующих полей дефолтами при валидации или нормализации
Более безопасное мышление простое:
- Отсутствие: оставить как есть
- Null: очистить (только когда разрешено)
- Неизвестное: отклонить
Быстрые проверки перед релизом
Перед выпуском эндпоинта обновления пройдитесь по одной рисковой проверке: не изменяет ли обновление одного поля случайно другие?
Краткий чеклист:
- Убедитесь, что все пути обновления следуют одинаковым правилам для отсутствующих и
null. - Применяйте allowlist на сервере (не только в UI).
- Тестируйте, что отсутствующие поля не меняют сохранённые данные (обновите только
displayName, проверьте, чтоemail,phone,addressостались идентичными). - Проверьте, что
nullочищает только те поля, для которых это явно разрешено. - Добавьте одну проверку конкурентности, чтобы два редактирования не перезаписывали друг друга.
Быстрый сценарий, который ловит многое: возьмите реальную запись со стейджинга с множеством заполненных полей, отправьте обновление только с одним полем, затем снова получите запись и сравните. Любое неожиданное изменение — сигнал тревоги.
Пример: при редактировании профиля стираются непредставленные поля
Типичный баг выглядит безобидно: пользователь меняет фото профиля, нажимает Сохранить, а позже замечает, что номер телефона исчез. Ничего не удаляли намеренно. Поле было перезаписано.
Как это происходит: экран профиля позволяет поменять только фото, поэтому клиент шлёт только это поле. Сервер считает запрос полной заменой и записывает новую запись, используя лишь присланное.
До: обновление в стиле замены (стирает поля)
Existing record in the database:
{
"id": "u_123",
"displayName": "Sam",
"phone": "+1-555-0100",
"photoUrl": "https://cdn.example/old.png"
}
Client sends:
{ "photoUrl": "https://cdn.example/new.png" }
Server does (conceptually):
profile = request.body
save(profile)
Result: phone disappears because it wasn’t included.
После: семантика PATCH + allowlist полей (поля сохраняются)
Вместо замены всей записи, считайте полезную нагрузку изменениями и принимайте только те поля, которые предназначены для редактирования этим эндпоинтом.
allowed = ["photoUrl"]
changes = pick(request.body, allowed)
profile = loadProfile(userId)
profile = merge(profile, changes)
save(profile)
Теперь меняется только photoUrl. Всё остальное остаётся как есть.
Следующие шаги: аудит ваших эндпоинтов обновления и исправление рискованных
Найдите каждый эндпоинт, который может менять сохранённые данные (профили, настройки, биллинг, «обновить статус»). Для каждого сравните, что есть в хранилище, что присылает клиент и что пишет сервер. Если сервер может записать больше полей, чем содержит запрос — у вас риск.
Практический чеклист аудита:
- Найдите обработчики, которые перезаписывают целые записи из тела запроса.
- Сделайте каждый эндпоинт либо истинной заменой (и отклоняйте частичные полезные нагрузки), либо истинным патчем (применяйте только разрешённые и присутствующие поля).
- Добавьте allowlist на эндпоинт и отклоняйте неожиданные поля.
- Стандартизируйте правила отсутствия vs null, чтобы клиенты вели себя согласованно.
- Проверьте фоновые задания и админ‑инструменты, а не только публичные API.
Если вы унаследовали кодовую базу, сгенерированную AI, эндпоинты обновления часто ломаются, потому что генерируемые обработчики по умолчанию делают «замену». FixMyMess (fixmymess.ai) фокусируется на диагностике и исправлении таких проблем в продакшене: ужесточение семантики обновлений, добавление allowlist и усиление валидации, чтобы реальные пользовательские данные не стирались.
Часто задаваемые вопросы
Should I use PUT or PATCH for updates?
Используйте PATCH для частичных правок, чтобы отсутствующие поля оставались неизменными. Оставляйте PUT только для действительно полных замен, когда клиент гарантированно отправляет весь ресурс целиком каждый раз.
Why do fields get wiped when I “update” only one field?
Потому что обновление в стиле замены принимает тело запроса за новую полную запись. Любое поле, которое вы не отправите, может быть перезаписано в null, пустое значение или на дефолт, что выглядит как тихое удаление.
What’s the safest way to treat missing fields vs null?
Выберите одно понятное правило и выполняйте его на сервере: отсутствие — значит «оставить как есть», а null — значит «очистить», но только для тех полей, где очистка разрешена. Если вы не можете безопасно поддерживать очистку, отклоняйте null для этого поля.
Can web forms cause accidental data loss even if the user didn’t touch those fields?
Да, это часто происходит. Многие формы отправляют только видимые поля, поэтому скрытые или размещённые в вкладках поля не попадут в полезную нагрузку. Если бэкенд делает замену записи, эти непредставленные поля могут быть сброшены.
How do I prevent clients from updating fields they shouldn’t touch?
Используйте field allowlist (разрешённый список полей) для каждого эндпоинта и применяйте изменения только к ключам, которые и разрешены, и присутствуют в запросе. Неизвестные поля лучше отклонять, чтобы опечатки и устаревшие клиенты не меняли данные молча.
What’s a safe server-side pattern for partial updates?
Загрузите текущую запись, соберите объект changes, выбрав только разрешённые ключи из запроса, проверьте изменения, затем объедините и сохраните. Избегайте конструирования полной модели исключительно из тела запроса.
How do I handle nested objects like address without wiping subfields?
Не принимайте вложенные объекты целиком, если только вы не договорились об этом контрактом. Патчьте вложенные ключи по отдельности (например, address.city), чтобы отправка одного вложенного поля не затирала соседей вроде address.line2.
Why are defaults dangerous in update endpoints?
Дефолты принадлежат потокам создания, а не обновления. Если применять дефолты при апдейтах, отсутствующие поля могут «получить» значения по умолчанию и перезаписать реальные сохранённые данные.
How do I stop two edits from overwriting each other (race conditions)?
Используйте оптимистичную контроль версий, чтобы устаревшие клиенты не могли перезаписать новые изменения. Номер версии, проверка updatedAt или ETag/If-Match позволяют серверу отклонить устаревший апдейт с явным ответом о конфликте.
What’s the quickest test to catch “wiped field” bugs before shipping?
Возьмите реальную запись со стейджинга с множеством заполненных полей, отправьте апдейт, меняющий только одно поле, затем заново получите запись и сравните. Если что‑то ещё изменилось, ваш путь обновления делает замену или неправильно применяет дефолты.