it Задачи Баг появляется только у одного пользователя. Найди причину
Баг появляется только у одного пользователя. Найди причину

Баг появляется только у одного пользователя. Найди причину

679
05 февраля 2026 в 14:15

В продакшене все стабильно, но один пользователь постоянно ловит ошибку. У тебя есть кусок кода, несколько фактов и логи. Ваша задача - найти причину и предложить решение.

Ситуация

Ты поддерживаешь веб-сервис. В целом система работает нормально: жалоб нет, метрики ровные, ошибок мало. Но в поддержку регулярно пишет один и тот же пользователь: «Не могу оформить заказ. Всегда ошибка».


Самое неприятное: баг появляется только у него. Ни у коллег, ни у тестировщика, ни у других клиентов проблема не воспроизводится. Пользователь не бот, заказов у него много, платёжная карта валидная, и он уверяет, что ничего «особенного» не делает.


Твоя задача: по входным данным ниже понять, что именно ломается, почему это проявляется только у одного пользователя и как исправить правильно.


Дано

Есть 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-представление управляющих символов, чтобы не засорять логи и не хранить чувствительные данные.
На фронте — подсказка/маска: запрещать ввод недопустимых символов и показывать ошибку сразу, ещё до отправки.


Важная мысль: если баг проявляется «только у одного пользователя», это почти всегда не магия, а особенность данных/окружения. Твоя работа — найти различие и сделать систему устойчивой к таким случаям.

Больше интересных новостей

Комментарии
Добавить комментарий

Пока комментариев нет