ГлавнаяБлог5 типичных багов в Open Source и как их не допустить
Алгоритмы

5 типичных багов в Open Source и как их не допустить

Разбираем 5 частых ошибок в коде на Python и других языках, которые проходят код-ревью. Узнайте, как их находить и исправлять, и проверьте свои проекты.

Al
Редакция Algolitalgolit.ru
8 мин чтения27 июня 2026 г.

5 типичных багов в Open Source и как их не допустить

Вы когда-нибудь отправляли пул-реквест, который проходил ревью, но потом оказывалось, что фича никогда не работала? Или ловили падение на пустом вводе, хотя тесты были зелёными? Я собрал пять повторяющихся паттернов ошибок из реальных PR в LangChain, Vite, bat, mistune и других проектах. Разберём, почему они проходят ревью и как их отлавливать до того, как они попадут в production.

1. Аргумент, который никто не устанавливает

В LangChain файловый инструмент для Anthropic падал при каждом переименовании. Обработчик _handle_rename читал путь так:

def _handle_rename(self, args):
    src = args["old_path"]  # KeyError: 'old_path'
    dst = args["new_path"]

Но код, который собирает args, никогда не кладёт туда old_path. Он хранит исходный путь под ключом path, а целевой — под new_path. Все соседние обработчики (_handle_view, _handle_create и др.) читают args["path"]. Только rename тянулся к несуществующему ключу, вызывая KeyError.

Исправление тривиально — читать тот ключ, который реально генерируется:

src = args["path"]
dst = args["new_path"]

Почему это прошло ревью? Был написан юнит-тест, который вызывал обработчик напрямую, передавая ему фантомный ключ:

_handle_rename({"old_path": "a.txt", "new_path": "b.txt"})  # зелёный, но система так не работает

Тест был зелёным, потому что конструировал вход, который функция хотела, а не тот, что реально приходит. Счастливый путь, построенный на ручных данных, может скрыть фичу, которая ни разу не работала.

Проверка: Когда функция читает args["x"], найдите, где собирается args. Если нигде не устанавливается "x" — это баг. Пишите тесты против реального вызывающего кода, а не против ожидаемой формы аргументов.

2. Ленивое значение по умолчанию, которое проглатывает реальное

У этой ошибки два лица. Классическое — проверка на истинность там, где нужно проверять на наличие значения:

// отбрасывает легитимное DEFAULT 0
const clause = defaultValue ? `DEFAULT ${defaultValue}` : '';

0, '' и false — все реальные значения, и все три пропускают ветку. DEFAULT 0 молча превращается в отсутствие значения по умолчанию. Исправление — задать правильный вопрос:

const clause = defaultValue !== undefined ? `DEFAULT ${defaultValue}` : '';

Другое лицо — потеря точности при форматировании. В библиотеке indicati HumanFloatCount форматировал с {:.0}, округляя до нуля знаков и отбрасывая дробную часть. 1234.9 выводилось как 1,234. Та же болезнь: «удобный» путь по умолчанию тихо отбрасывает информацию.

Почему это проходит ревью? Ложное значение — это не то же самое, что отсутствующее. Удобный путь по умолчанию выглядит корректно на всех значениях, которые вы случайно протестировали. Вы тестировали 5, но не 0. Вы тестировали 1200, но не 1234.9. Баг живёт на том входе, который вы не удосужились набрать.

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

3. Арифметика беззнаковых типов без защиты от переполнения

В bat раскладка списка языков делается так:

let desired_width = config.term_width - longest - separator.len();

Запустите bat --list-languages --terminal-width 1 — и term_width окажется меньше вычитаемого. На типе usize нет отрицательной стороны. В debug-сборке — паника, в release — переполнение до огромного числа и неправильная раскладка. Исправление — saturating_sub:

let desired_width = config.term_width.saturating_sub(longest).saturating_sub(separator.len());

Я столкнулся с той же проблемой u32 в imageproc (параметр edge_radius) и похожей — в декодере ICO библиотеки image: он отвергал изображения с более чем 256 цветовыми плоскостями даже в lenient-режиме, в то время как соседнее поле уже было ослаблено. Не арифметика, но та же семья — краевой случай, проскочивший мимо проверки из-за рассинхронизации параллельных полей.

Почему это проходит ревью? «Эта ширина не может быть 1», плюс беззнаковые типы не предупреждают, когда вы ошибаетесь. А когда добавляете флаг strict/lenient, легко подключить его к одной проверке и забыть про соседнюю.

Проверка: Любое a - b на беззнаковом типе, связанное с пользовательским размером, — используйте saturating_sub и тестируйте при 0 и 1. Каждый новый флаг строгости — проверьте все валидации в этой структуре.

4. Уголок Unicode, до которого ASCII не добирается

Правила разметки выглядят просто, пока кто-то не передаст символ, который вы не набираете руками. В mistune неправильно обрабатывался случай *foo***bar*. CommonMark требует специальной обработки, когда длина разделителей кратна 3, а переписанный inline-движок проверял не ту длину. Простой *foo* работал, поэтому фича ушла. Сломанный случай — это накопленные разделители, которые вы никогда не напишете намеренно, но генератор — запросто.

В wenmode фланкинговая логика (определение, может ли * открывать или закрывать выделение) обрабатывала комбинирующие знаки и символы форматирования как пунктуацию. Выделение вокруг акцентированного текста вело себя так, будто в середине слова стоит пунктуация.

Почему это проходит ревью? ASCII проходит, поэтому кажется, что всё готово. Угловые случаи спецификации проявляются только на входе, который вы не набираете.

Проверка: Дифференциальное тестирование против эталонной реализации. Для mistune я сравнивал с markdown-it-py и commonmark.py — дифференциальный фаззинг сократил расхождения с 26 до 0. Не нужно выводить спецификацию в голове — нужна вторая реализация, готовая с вами не согласиться.

5. Некорректный вход, переданный напрямую декодеру

В Vite HTML-мидлвар декодировал URL запроса без защиты:

const pathname = decodeURIComponent(url);

Запрос /%c0.html вызывает URIError: URI malformed, что валит мидлвар вместо того, чтобы вернуть 404. Исправление — обёртка, уже существовавшая на соседнем пути:

let pathname;
try {
    pathname = decodeURIComponent(url);
} catch {
    return next();
}

Почему это проходит ревью? «Это URL, URL-ы валидны». Всё, что приходит по сети, может быть некорректным. decodeURIComponent, JSON.parse и atob выбрасывают исключение на плохом входе, а не возвращают код ошибки, который вы могли бы проверить.

Проверка: Каждое декодирование или парсинг ввода, который вы не создавали, оборачивайте в try/catch с определённым fallthrough. Затем киньте пару заведомо сломанных строк и убедитесь, что код гнётся, а не ломается.

Практический вывод: что делать прямо сейчас

Возьмите свой проект и проверьте его по этим пяти пунктам:

  • Найдите функции, которые читают из словаря по ключу, которого может не быть.
  • Проверьте все проверки на истинность — не отбрасывают ли они 0, "", false.
  • Замените вычитание на беззнаковых типах на saturating_sub.
  • Напишите дифференциальный тест для любого парсера или разметки.
  • Оберните все вызовы decodeURIComponent, JSON.parse и atob в try/catch.

Эти пять паттернов покрывают большую часть багов, которые проходят ревью. Научитесь их замечать — и ваши PR станут чище, а проекты надёжнее.

#баги#код-ревью#Python#безопасность#Open Source
Al
Редакция Algolit

Пишем про алгоритмы, подготовку к собеседованиям и карьеру в IT — так, чтобы было понятно и полезно.

Хочешь закрепить знания на практике?

Решай задачи на Algolit — интерактивная платформа для обучения

Начать бесплатно →