Баг появляется только у одного пользователя. Найди причину
В продакшене все стабильно, но один пользователь постоянно ловит ошибку. У тебя есть кусок кода, несколько фактов и логи. Ваша задача - найти причину и предложить решение.
Ситуация
Ты поддерживаешь веб-сервис. В целом система работает нормально: жалоб нет, метрики ровные, ошибок мало. Но в поддержку регулярно пишет один и тот же пользователь: «Не могу оформить заказ. Всегда ошибка».
Самое неприятное: баг появляется только у него. Ни у коллег, ни у тестировщика, ни у других клиентов проблема не воспроизводится. Пользователь не бот, заказов у него много, платёжная карта валидная, и он уверяет, что ничего «особенного» не делает.
Твоя задача: по входным данным ниже понять, что именно ломается, почему это проявляется только у одного пользователя и как исправить правильно.
Дано
Есть endpoint, который создаёт заказ. Код упрощён, но логика сохранена. Бэкенд на Node.js, база PostgreSQL, сериализация JSON стандартная.
/**
* POST /api/order
* body: { items: [{ id, qty }], promoCode?: string }
*/
export async function createOrder(req, res) {
try {
const userId = req.user.id; // строка UUID
const { items, promoCode } = req.body;
// 1) Проверяем промокод (если есть)
if (promoCode) {
const promo = await db.query(
"SELECT id, is_active FROM promos WHERE code = $1",
[promoCode.trim()]
);
if (!promo.rows[0] || !promo.rows[0].is_active) {
return res.status(400).json({ error: "Invalid promo" });
}
}
// 2) Считаем сумму
const total = items.reduce((sum, it) => sum + it.qty * 100, 0);
// 3) Создаём заказ
const order = await db.query(
"INSERT INTO orders(user_id, total) VALUES($1, $2) RETURNING id",
[userId, total]
);
return res.json({ ok: true, orderId: order.rows[0].id });
} catch (e) {
console.error("createOrder error:", e);
return res.status(500).json({ error: "Server error" });
}
}Наблюдение:
Для большинства пользователей всё ок. Ошибка 500 появляется только у одного пользователя, и только когда он вводит промокод. Без промокода заказы у него проходят.
Логи
Вот лог сервера, который повторяется у этого пользователя. Он прислал один и тот же промокод, но «валидный», и раньше он уже им пользовался.
createOrder error: error: invalid byte sequence for encoding "UTF8": 0x00
at Parser.parseErrorMessage (...)
at Parser.handlePacket (...)
at Parser.parse (...)Дополнительный факт:
Когда ты копируешь промокод из тикета поддержки и вставляешь в Postman, у тебя всё проходит. Но когда этот пользователь вводит промокод у себя — ошибка 500 воспроизводится стабильно.
Вопросы к тебе
1) Что означает ошибка invalid byte sequence for encoding "UTF8": 0x00 в контексте PostgreSQL?
2) Почему баг проявляется только у одного пользователя и только при вводе промокода?
3) Какой минимальный фикс можно внедрить прямо сейчас, чтобы убрать 500 и дать пользователю понятный ответ?
4) Какое «правильное» решение на будущее ты бы предложил, чтобы подобные кейсы не повторялись?
Подсказки (не читай, если хочешь решить сам)
Подсказка №1: байт 0x00 — это нулевой байт (NUL). Он часто появляется при копировании из некоторых приложений, автозаполнении, специфических клавиатурах или при вставке из буфера с форматированием.
Подсказка №2: строка может выглядеть нормально на экране, но содержать «невидимые» символы: нулевой байт, неразрывный пробел, управляющие символы.
Разбор решения
1) Что означает ошибка?
PostgreSQL хранит текст в кодировке UTF-8 и не принимает строки, содержащие нулевой байт (0x00) внутри текстовых полей. Это не «плохая буква», а бинарный управляющий символ, который ломает представление строки как корректного текста в БД/драйвере.
2) Почему только у одного пользователя?
Потому что именно у него промокод попадает на сервер с «невидимым мусором». Он может:
— копировать промокод из заметок/мессенджера, где внутри строки есть NUL или другой управляющий символ;
— пользоваться автозаменой/клавиатурой/менеджером паролей, который вставляет строку с «нулём»;
— вводить промокод через специфическое поле/браузер/расширение, которое добавляет скрытый символ.
Почему ты не можешь воспроизвести, копируя код из тикета? Потому что в тикет чаще попадает «очищенный» текст: канал поддержки, веб-форма или сама система трека может автоматически выкинуть NUL, либо этот символ просто не переносится при копировании.
3) Минимальный фикс «прямо сейчас»
Нельзя отдавать 500 из-за пользовательского ввода. Нужно валидировать и нормализовать промокод до запроса в БД. Самое простое: удалить управляющие символы, включая NUL, и дополнительно ограничить допустимый набор символов.
function normalizePromoCode(raw) {
if (typeof raw !== "string") return "";
// Убираем нулевой байт и другие управляющие символы (C0 + DEL)
const cleaned = raw.replace(/[\u0000-\u001F\u007F]/g, "").trim();
return cleaned;
}
function isPromoCodeValidFormat(code) {
// Пример: промокоды только латиница/цифры/дефис, 3..32
return /^[A-Z0-9-]{3,32}$/i.test(code);
}И применение в обработчике:
const promoCodeRaw = req.body.promoCode;
const promoCode = normalizePromoCode(promoCodeRaw);
if (promoCodeRaw && !promoCode) {
return res.status(400).json({ error: "Promo code contains invalid characters" });
}
if (promoCode) {
if (!isPromoCodeValidFormat(promoCode)) {
return res.status(400).json({ error: "Invalid promo code format" });
}
const promo = await db.query(
"SELECT id, is_active FROM promos WHERE code = $1",
[promoCode]
);
if (!promo.rows[0] || !promo.rows[0].is_active) {
return res.status(400).json({ error: "Invalid promo" });
}
}Это уберёт 500 и превратит ситуацию в контролируемую ошибку 400 с понятным текстом. Пользователь сможет ввести промокод заново или вставить корректно.
4) Правильное решение на будущее
Минимальный фикс — хорошо, но «правильно» сделать системно:
— Нормализация и валидация ввода на уровне DTO/схемы (например, через Zod/Joi/class-validator), чтобы грязные данные не доходили до бизнес-логики.
— Единые правила формата промокодов: допустимые символы, длина, регистр. Например, хранить в базе промокоды в верхнем регистре и сравнивать тоже в верхнем.
— Логирование «подозрительных» строк безопасным способом: не печатать сырой ввод, а логировать длину и hex-представление управляющих символов, чтобы не засорять логи и не хранить чувствительные данные.
— На фронте — подсказка/маска: запрещать ввод недопустимых символов и показывать ошибку сразу, ещё до отправки.
Важная мысль: если баг проявляется «только у одного пользователя», это почти всегда не магия, а особенность данных/окружения. Твоя работа — найти различие и сделать систему устойчивой к таким случаям.
Больше интересных новостей
Баг появляется только у одного пользователя. Найди причину
Взлетит ли самолет на ленте транспортера?
Шахматная доска и кости домино
Задачка «Пять с половиной программистов»