Баг з'являється лише в одного користувача. Знайди причину
У продакшені все стабільно, але один користувач постійно ловить помилку. У тебе є шматок коду, кілька фактів та логи. Ваше завдання – знайти причину та запропонувати рішення.
Ситуація
Ти підтримуєш веб-сервіс. Загалом система працює нормально: скарг немає, метрики рівні, помилок мало. Але в підтримку регулярно пише один і той самий користувач: «Не можу оформити замовлення. Завжди помилка».
Найнеприємніше: баг з’являється лише у нього. Ні у колег, ні у тестувальника, ні у інших клієнтів проблема не відтворюється. Користувач не бот, замовлень у нього багато, платіжна картка валідна, і він запевняє, що нічого «особливого» не робить.
Твоє завдання: за вхідними даними нижче зрозуміти, що саме ламається, чому це проявляється лише в одного користувача і як виправити правильно.
Дано
Є 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-подання керувальних символів, щоб не засмічувати логи й не зберігати чутливі дані.
— На фронті — підказка/маска: забороняти введення недопустимих символів і показувати помилку одразу, ще до відправлення.
Важлива думка: якщо баг проявляється «лише в одного користувача», це майже завжди не магія, а особливість даних/оточення. Твоя робота — знайти відмінність і зробити систему стійкою до таких випадків.
Більше цікавих новин
Решаем логические задачки
Логическая задача про стопку монет
Решаем задачку про бракованные батарейки
3 логические задачи для настоящего программиста